keyrack 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Gemfile CHANGED
@@ -4,7 +4,7 @@ source :rubygems
4
4
  # gem "activesupport", ">= 2.3.5"
5
5
  gem 'net-scp', :require => 'net/scp'
6
6
  gem 'highline'
7
- gem 'clipboard'
7
+ gem 'viking-copier', :require => 'copier'
8
8
 
9
9
  # Add dependencies to develop your gem here.
10
10
  # Include everything needed to run rake, tests, features, etc.
@@ -13,4 +13,5 @@ group :development do
13
13
  gem "jeweler", "~> 1.5.1"
14
14
  gem "rcov", ">= 0"
15
15
  gem "mocha", :require => false
16
+ gem "cucumber"
16
17
  end
data/Gemfile.lock CHANGED
@@ -1,17 +1,23 @@
1
1
  GEM
2
2
  remote: http://rubygems.org/
3
3
  specs:
4
- clipboard (0.9.2)
5
- ffi
6
- zucker (>= 8)
7
- ffi (1.0.1)
8
- rake (>= 0.8.7)
4
+ builder (3.0.0)
5
+ cucumber (0.10.0)
6
+ builder (>= 2.1.2)
7
+ diff-lcs (~> 1.1.2)
8
+ gherkin (~> 2.3.2)
9
+ json (~> 1.4.6)
10
+ term-ansicolor (~> 1.0.5)
11
+ diff-lcs (1.1.2)
12
+ gherkin (2.3.3)
13
+ json (~> 1.4.6)
9
14
  git (1.2.5)
10
15
  highline (1.6.1)
11
- jeweler (1.5.1)
16
+ jeweler (1.5.2)
12
17
  bundler (~> 1.0.0)
13
18
  git (>= 1.2.5)
14
19
  rake
20
+ json (1.4.6)
15
21
  mocha (0.9.10)
16
22
  rake
17
23
  net-scp (1.0.4)
@@ -19,16 +25,18 @@ GEM
19
25
  net-ssh (2.0.23)
20
26
  rake (0.8.7)
21
27
  rcov (0.9.9)
22
- zucker (8)
28
+ term-ansicolor (1.0.5)
29
+ viking-copier (1.2)
23
30
 
24
31
  PLATFORMS
25
32
  ruby
26
33
 
27
34
  DEPENDENCIES
28
35
  bundler (~> 1.0.0)
29
- clipboard
36
+ cucumber
30
37
  highline
31
38
  jeweler (~> 1.5.1)
32
39
  mocha
33
40
  net-scp
34
41
  rcov
42
+ viking-copier
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.3
1
+ 0.2.0
@@ -0,0 +1,39 @@
1
+ Feature: Console runner
2
+ I want to run Keyrack from the console
3
+
4
+ Scenario: starting for the first time with a filesystem store
5
+ * I run keyrack interactively
6
+ * I wait a few seconds
7
+ * the output should contain "New passphrase:"
8
+ * I type "secret"
9
+ * the output should contain "Confirm passphrase:"
10
+ * I type "secret"
11
+ * I wait a few seconds
12
+ * the output should contain "Choose storage type:"
13
+ * I type "filesystem"
14
+ * the output should contain "n. Add new"
15
+ * I type "n" to add a new entry
16
+ * the output should contain "Label:"
17
+ * I type "Twitter"
18
+ * the output should contain "Username:"
19
+ * I type "dudeguy"
20
+ * the output should contain "Generate password?"
21
+ * I type "n" for no
22
+ * the output should contain "Password:"
23
+ * I type "kittens"
24
+ * the output should contain "Password (again):"
25
+ * I type "kittens"
26
+ * the output should contain "1. Twitter"
27
+ * the output should also contain "s. Save"
28
+ * I type "s" to save the database
29
+ * I type "q" to quit
30
+ * I wait a few seconds
31
+ * I run keyrack interactively again
32
+ * I wait a few seconds
33
+ * the output should contain "Keyrack password:"
34
+ * I type "secret"
35
+ * I wait a few seconds
36
+ * the output should contain "1. Twitter"
37
+ * I type "1" for Twitter
38
+ * my clipboard should contain "kittens"
39
+ * I type "q" to quit
@@ -0,0 +1,45 @@
1
+ Before do
2
+ @aruba_io_wait_seconds = 2
3
+ @fake_home = Dir::Tmpname.create('keyrack') { }
4
+ Dir.mkdir(@fake_home)
5
+ @old_home = ENV['HOME']
6
+ ENV['HOME'] = @fake_home
7
+ end
8
+
9
+ After do
10
+ ENV['HOME'] = @old_home
11
+ FileUtils.rm_rf(@fake_home)
12
+ end
13
+
14
+ When /I run keyrack interactively/ do
15
+ @out, @in, @pid = PTY.spawn("bundle exec ruby -Ilib bin/keyrack")
16
+ end
17
+
18
+ Then /the output should contain "([^"]+)"/ do |expected|
19
+ if @slept
20
+ @slept = false
21
+ else
22
+ sleep 1
23
+ end
24
+ @output = @out.read_nonblock(255)
25
+ @output.should include(expected)
26
+ end
27
+
28
+ Then /the output should also contain "([^"]+)"/ do |expected|
29
+ @output.should include(expected)
30
+ end
31
+
32
+ When /I type "([^"]+)"/ do |text|
33
+ @in.puts(text)
34
+ end
35
+
36
+ When /I wait a few seconds/ do
37
+ sleep 5
38
+ @slept = true
39
+ end
40
+
41
+ Then /my clipboard should contain "([^"]+)"/ do |expected|
42
+ sleep 1
43
+ result = %x{xclip -selection clipboard -o}.chomp
44
+ result.should == expected
45
+ end
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'tempfile'
11
+ require 'fileutils'
12
+ require 'pty'
13
+ require 'yaml'
@@ -1,10 +1,9 @@
1
1
  module Keyrack
2
2
  class Database
3
- def initialize(config)
4
- store_config = config['store'].dup
5
- @store = Store[store_config.delete('type')].new(store_config)
6
- key_path = File.expand_path(config['key'])
7
- @key = OpenSSL::PKey::RSA.new(File.read(key_path), config['password'])
3
+ def initialize(key, iv, store)
4
+ @key = key
5
+ @iv = iv
6
+ @store = store
8
7
  @data = decrypt
9
8
  @dirty = false
10
9
  end
@@ -27,14 +26,22 @@ module Keyrack
27
26
  end
28
27
 
29
28
  def save
30
- @store.write(@key.public_encrypt(Marshal.dump(@data)))
29
+ cipher = OpenSSL::Cipher::Cipher.new("AES-128-CBC")
30
+ cipher.encrypt; cipher.key = @key; cipher.iv = @iv
31
+ @store.write(cipher.update(Marshal.dump(@data)) + cipher.final)
31
32
  @dirty = false
32
33
  end
33
34
 
34
35
  private
35
36
  def decrypt
36
37
  data = @store.read
37
- data ? Marshal.load(@key.private_decrypt(data)) : {}
38
+ if data
39
+ cipher = OpenSSL::Cipher::Cipher.new("AES-128-CBC")
40
+ cipher.decrypt; cipher.key = @key; cipher.iv = @iv
41
+ Marshal.load(cipher.update(data) + cipher.final)
42
+ else
43
+ {}
44
+ end
38
45
  end
39
46
  end
40
47
  end
@@ -1,21 +1,55 @@
1
1
  module Keyrack
2
2
  class Runner
3
3
  def initialize(argv)
4
- options = {
5
- :config_path => "~/.keyrack/config"
6
- }
4
+ @config_path = "~/.keyrack"
7
5
  OptionParser.new do |opts|
8
- opts.on("-c", "--config [FILE]", "Specify configuration file (Default: #{options[:config_path]}") do |f|
9
- options[:config_path] = f
6
+ opts.on("-d", "--directory [PATH]", "Specify configuration path (Default: #{@config_path}") do |f|
7
+ @config_path = f
10
8
  end
11
9
  end.parse(argv)
12
-
13
- @options = YAML.load_file(File.expand_path(options[:config_path]))
10
+ @config_path = File.expand_path(@config_path)
14
11
  @ui = UI::Console.new
15
- password = @ui.get_password
16
- @database = Database.new(@options.merge('password' => password))
17
- @ui.database = @database
18
12
 
13
+ if Dir.exist?(@config_path)
14
+ @options = YAML.load_file(File.join(@config_path, "config"))
15
+ password = @ui.get_password
16
+ rsa_key = Utils.open_rsa_key(@options['rsa'], password)
17
+ aes_data = Utils.open_aes_data(@options['aes'], rsa_key)
18
+ else
19
+ Dir.mkdir(@config_path)
20
+ @options = {}
21
+ @ui.display_first_time_notice
22
+
23
+ # RSA
24
+ rsa_options = @ui.rsa_setup
25
+ rsa_key, rsa_pem = Utils.generate_rsa_key(rsa_options['password'])
26
+ rsa_path = File.expand_path(rsa_options['path'], @config_path)
27
+ File.open(rsa_path, 'w') { |f| f.write(rsa_pem) }
28
+ @options['rsa'] = rsa_path
29
+
30
+ # AES
31
+ aes_data = {
32
+ 'key' => Utils.generate_aes_key,
33
+ 'iv' => Utils.generate_aes_key
34
+ }
35
+ dump = Marshal.dump(aes_data)
36
+ aes_path = File.expand_path('aes', @config_path)
37
+ File.open(aes_path, 'w') { |f| f.write(rsa_key.public_encrypt(dump)) }
38
+ @options['aes'] = aes_path
39
+
40
+ # Store
41
+ store_options = @ui.store_setup
42
+ if store_options['type'] == 'filesystem'
43
+ store_options['path'] = File.expand_path(store_options['path'], @config_path)
44
+ end
45
+ @options['store'] = store_options
46
+
47
+ # Write out config
48
+ File.open(File.expand_path('config', @config_path), 'w') { |f| f.print(@options.to_yaml) }
49
+ end
50
+ store = Store[@options['store']['type']].new(@options['store'].reject { |k, _| k == 'type' })
51
+ @database = Database.new(aes_data['key'], aes_data['iv'], store)
52
+ @ui.database = @database
19
53
  main_loop
20
54
  end
21
55
 
@@ -38,7 +38,7 @@ module Keyrack
38
38
  :quit
39
39
  end
40
40
  else
41
- Clipboard.copy(entries[result.to_i - 1][:password])
41
+ Copier(entries[result.to_i - 1][:password])
42
42
  @highline.say("The password has been copied to your clipboard.")
43
43
  nil
44
44
  end
@@ -69,6 +69,40 @@ module Keyrack
69
69
  end
70
70
  result
71
71
  end
72
+
73
+ def display_first_time_notice
74
+ @highline.say("This looks like your first time using Keyrack. I'll need to ask you a few questions first.")
75
+ end
76
+
77
+ def rsa_setup
78
+ password = confirmation = nil
79
+ loop do
80
+ password = @highline.ask("New passphrase: ") { |q| q.echo = false }
81
+ confirmation = @highline.ask("Confirm passphrase: ") { |q| q.echo = false }
82
+ break if password == confirmation
83
+ @highline.say("Passphrases didn't match.")
84
+ end
85
+ { 'password' => password, 'path' => 'rsa' }
86
+ end
87
+
88
+ def store_setup
89
+ result = {}
90
+ result['type'] = @highline.choose do |menu|
91
+ menu.header = "Choose storage type:"
92
+ menu.choices("filesystem", "ssh")
93
+ end
94
+
95
+ case result['type']
96
+ when 'filesystem'
97
+ result['path'] = 'database'
98
+ when 'ssh'
99
+ result['host'] = @highline.ask("Host: ")
100
+ result['user'] = @highline.ask("User: ")
101
+ result['path'] = @highline.ask("Remote path: ")
102
+ end
103
+
104
+ result
105
+ end
72
106
  end
73
107
  end
74
108
  end
data/lib/keyrack/utils.rb CHANGED
@@ -7,5 +7,23 @@ module Keyrack
7
7
  end
8
8
  result
9
9
  end
10
+
11
+ def self.generate_rsa_key(password)
12
+ rsa = OpenSSL::PKey::RSA.new(4096)
13
+ cipher = OpenSSL::Cipher::Cipher.new('des3')
14
+ [rsa, rsa.to_pem(cipher, password)]
15
+ end
16
+
17
+ def self.generate_aes_key
18
+ SecureRandom.base64(128)[0..127]
19
+ end
20
+
21
+ def self.open_rsa_key(path, password)
22
+ OpenSSL::PKey::RSA.new(File.read(path), password)
23
+ end
24
+
25
+ def self.open_aes_data(path, rsa_key)
26
+ Marshal.load(rsa_key.private_decrypt(File.read(path)))
27
+ end
10
28
  end
11
29
  end
data/lib/keyrack.rb CHANGED
@@ -1,9 +1,10 @@
1
1
  require 'openssl'
2
2
  require 'yaml'
3
3
  require 'optparse'
4
+ require 'securerandom'
4
5
  require 'net/scp'
5
6
  require 'highline'
6
- require 'clipboard'
7
+ require 'copier'
7
8
 
8
9
  module Keyrack
9
10
  end
data/test/fixtures/aes ADDED
Binary file
data/test/fixtures/id_rsa CHANGED
@@ -1,30 +1,54 @@
1
1
  -----BEGIN RSA PRIVATE KEY-----
2
2
  Proc-Type: 4,ENCRYPTED
3
- DEK-Info: AES-128-CBC,F09A7B088DE6A0D4DAED7A4E6EE0E655
3
+ DEK-Info: DES-EDE3-CBC,030F0E3D8BD7BF1A
4
4
 
5
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- lvZ6v+ScMY2nQtPvLbMfFilevzaOHSFT8WhVSmutHoBCVNvzbWWiKqTtEqw3XjgZ
13
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- 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
- burDVGTeN/qsWBDJl9flerBmEHdF8yGKppJSygiqRGTstjX6/pUBnUPv9mY6yhu5
23
- 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
- DelvO0fz77RF/4+w8Fms/7YO+4fs22AsOzozOSaK1nmMRaOBO+rFQAlfhvOibqn9
25
- vENbwDbJsdG8lFwV6RGQtz5r8MgOKyx5v3VOqVti2ET8OBUhPPbuj0Cw32F1xMRq
26
- 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
- FL87isDMBxw/KCCe4/EBpsGXcOCge3uNnA198rnpyaNwcmqL5AKHyQH/i3r5SZOT
28
- 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
- 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
30
54
  -----END RSA PRIVATE KEY-----
data/test/helper.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'tempfile'
2
+ require 'fileutils'
2
3
  require 'rubygems'
3
4
  require 'bundler'
4
5
  begin
@@ -29,7 +30,7 @@ class Test::Unit::TestCase
29
30
 
30
31
  def teardown
31
32
  if @tmpnames
32
- @tmpnames.each { |t| File.unlink(t) if File.exist?(t) }
33
+ @tmpnames.each { |t| FileUtils.rm_rf(t) }
33
34
  end
34
35
  end
35
36
  end
@@ -3,30 +3,28 @@ require 'helper'
3
3
  module Keyrack
4
4
  class TestDatabase < Test::Unit::TestCase
5
5
  def setup
6
+ @key = "abcdefgh" * 32
7
+ @iv = @key.reverse
8
+
6
9
  @path = get_tmpname
7
- @database = Keyrack::Database.new({
8
- 'store' => { 'type' => 'filesystem', 'path' => @path },
9
- 'key' => fixture_path('id_rsa'),
10
- 'password' => 'secret'
11
- })
10
+ @store = Store['filesystem'].new('path' => @path)
11
+
12
+ @database = Keyrack::Database.new(@key, @iv, @store)
12
13
  @database.add('Twitter', 'username', 'password')
13
14
  @database.save
14
15
  end
15
16
 
16
17
  def test_encrypts_database
17
- key = OpenSSL::PKey::RSA.new(File.read(fixture_path('id_rsa')), 'secret')
18
18
  encrypted_data = File.read(@path)
19
- marshalled_data = key.private_decrypt(encrypted_data)
19
+ cipher = OpenSSL::Cipher::Cipher.new("AES-128-CBC")
20
+ cipher.decrypt; cipher.key = @key; cipher.iv = @iv
21
+ marshalled_data = cipher.update(encrypted_data) + cipher.final
20
22
  data = Marshal.load(marshalled_data)
21
23
  assert_equal({'Twitter'=>{:username=>'username',:password=>'password'}}, data)
22
24
  end
23
25
 
24
26
  def test_reading_existing_database
25
- database = Keyrack::Database.new({
26
- 'store' => { 'type' => 'filesystem', 'path' => @path },
27
- 'key' => fixture_path('id_rsa'),
28
- 'password' => 'secret'
29
- })
27
+ database = Keyrack::Database.new(@key, @iv, @store)
30
28
  expected = {:username => 'username', :password => 'password'}
31
29
  assert_equal(expected, database.get('Twitter'))
32
30
  end
@@ -40,5 +38,15 @@ module Keyrack
40
38
  @database.add('Foo', 'bar', 'baz')
41
39
  assert @database.dirty?
42
40
  end
41
+
42
+ def test_large_number_of_entries
43
+ site = "abcdefg"; user = "1234567"; pass = "zyxwvut" * 2
44
+ 500.times do |i|
45
+ @database.add(site, user, pass)
46
+ site.next!; user.next!; pass.next!
47
+ end
48
+ @database.save
49
+ assert_equal 501, @database.sites.length
50
+ end
43
51
  end
44
52
  end
@@ -2,31 +2,98 @@ require 'helper'
2
2
 
3
3
  module Keyrack
4
4
  class TestRunner < Test::Unit::TestCase
5
+ def setup
6
+ @console = stub('console', {
7
+ :get_password => 'secret',
8
+ :database= => nil, :menu => nil,
9
+ :get_new_entry => {:site => "Foo", :username => "bar", :password => "baz"}
10
+ })
11
+ UI::Console.stubs(:new).returns(@console)
12
+ @database = stub('database', { :add => nil })
13
+ Database.stubs(:new).returns(@database)
14
+ end
15
+
5
16
  def test_console
17
+ store_path = 'foo/bar/hey/buddy'
18
+ rsa_path = 'omg/rsa/path'
19
+ aes_path = 'hey/its/some/aes/stuff'
6
20
  config = {
7
- 'store' => { 'type' => 'filesystem', 'path' => 'foobar' },
8
- 'key' => fixture_path('id_rsa')
21
+ 'store' => { 'type' => 'filesystem', 'path' => store_path },
22
+ 'rsa' => rsa_path, 'aes' => aes_path
9
23
  }
10
- config_path = get_tmpname
11
- File.open(config_path, 'w') { |f| f.print(config.to_yaml) }
24
+ keyrack_dir = get_tmpname
25
+ Dir.mkdir(keyrack_dir)
26
+ File.open(File.join(keyrack_dir, "config"), 'w') { |f| f.print(config.to_yaml) }
12
27
 
13
- console = mock('console')
14
- UI::Console.expects(:new).returns(console)
15
- database = mock('database')
28
+ UI::Console.expects(:new).returns(@console)
16
29
 
17
30
  seq = sequence('ui sequence')
18
- console.expects(:get_password).returns('secret').in_sequence(seq)
19
- Database.expects(:new).with(config.merge('password' => 'secret')).returns(database).in_sequence(seq)
20
- console.expects(:database=).with(database).in_sequence(seq)
21
- console.expects(:menu).returns(:new).in_sequence(seq)
22
- console.expects(:get_new_entry).returns({:site => "Foo", :username => "bar", :password => "baz"}).in_sequence(seq)
23
- database.expects(:add).with("Foo", "bar", "baz")
24
- console.expects(:menu).returns(nil).in_sequence(seq)
25
- console.expects(:menu).returns(:save).in_sequence(seq)
26
- database.expects(:save).in_sequence(seq)
27
- console.expects(:menu).returns(:quit).in_sequence(seq)
28
-
29
- runner = Runner.new(["-c", config_path])
31
+ @console.expects(:get_password).returns('secret').in_sequence(seq)
32
+ rsa = mock("rsa key")
33
+ Utils.expects(:open_rsa_key).with(rsa_path, 'secret').returns(rsa).in_sequence(seq)
34
+ aes = {'key' => '12345', 'iv' => '54321'}
35
+ Utils.expects(:open_aes_data).with(aes_path, rsa).returns(aes).in_sequence(seq)
36
+ store = mock('filesystem store')
37
+ Store::Filesystem.expects(:new).with('path' => store_path).returns(store).in_sequence(seq)
38
+ Database.expects(:new).with('12345', '54321', store).returns(@database).in_sequence(seq)
39
+ @console.expects(:database=).with(@database).in_sequence(seq)
40
+
41
+ @console.expects(:menu).returns(:new).in_sequence(seq)
42
+ @console.expects(:get_new_entry).returns({:site => "Foo", :username => "bar", :password => "baz"}).in_sequence(seq)
43
+ @database.expects(:add).with("Foo", "bar", "baz")
44
+ @console.expects(:menu).returns(nil).in_sequence(seq)
45
+ @console.expects(:menu).returns(:save).in_sequence(seq)
46
+ @database.expects(:save).in_sequence(seq)
47
+ @console.expects(:menu).returns(:quit).in_sequence(seq)
48
+
49
+ runner = Runner.new(["-d", keyrack_dir])
50
+ end
51
+
52
+ def test_console_first_run
53
+ keyrack_dir = get_tmpname
54
+ seq = sequence('ui sequence')
55
+
56
+ @console.expects(:display_first_time_notice).in_sequence(seq)
57
+
58
+ # RSA generation
59
+ rsa_path = 'id_rsa'
60
+ @console.expects(:rsa_setup).returns('password' => 'secret', 'path' => rsa_path).in_sequence(seq)
61
+ rsa = mock('rsa key')
62
+ Utils.expects(:generate_rsa_key).with('secret').returns([rsa, 'private key']).in_sequence(seq)
63
+
64
+ # AES generation
65
+ Utils.expects(:generate_aes_key).twice.returns('foobar', 'barfoo').in_sequence(seq)
66
+ dump = Marshal.dump('key' => 'foobar', 'iv' => 'barfoo')
67
+ rsa.expects(:public_encrypt).with(dump).returns("encrypted dump")
68
+
69
+ # Store setup
70
+ store_path = 'database'
71
+ @console.expects(:store_setup).returns('type' => 'filesystem', 'path' => store_path).in_sequence(seq)
72
+ store = mock('filesystem store')
73
+ Store::Filesystem.expects(:new).with('path' => store_path).returns(store).in_sequence(seq)
74
+
75
+ Database.expects(:new).with('foobar', 'barfoo', store).returns(@database).in_sequence(seq)
76
+ @console.expects(:database=).with(@database).in_sequence(seq)
77
+ @console.expects(:menu).returns(:quit).in_sequence(seq)
78
+
79
+ runner = Runner.new(["-d", keyrack_dir])
80
+
81
+ assert Dir.exist?(keyrack_dir)
82
+ expected_rsa_file = File.expand_path(rsa_path, keyrack_dir)
83
+ assert File.exist?(expected_rsa_file)
84
+ assert_equal 'private key', File.read(expected_rsa_file)
85
+
86
+ expected_aes_file = File.expand_path('aes', keyrack_dir)
87
+ assert File.exist?(expected_aes_file)
88
+ assert_equal 'encrypted dump', File.read(expected_aes_file)
89
+
90
+ expected_config_file = File.expand_path('config', keyrack_dir)
91
+ assert File.exist?(expected_config_file)
92
+ expected_config = {
93
+ 'rsa' => expected_rsa_file, 'aes' => expected_aes_file,
94
+ 'store' => { 'type' => 'filesystem', 'path' => store_path }
95
+ }
96
+ assert_equal expected_config, YAML.load_file(expected_config_file)
30
97
  end
31
98
  end
32
99
  end
@@ -6,5 +6,36 @@ module Keyrack
6
6
  result = Utils.generate_password
7
7
  assert_match result, /^[!-~]{8}$/
8
8
  end
9
+
10
+ def test_generate_rsa_key
11
+ rsa = mock('rsa')
12
+ OpenSSL::PKey::RSA.expects(:new).with(4096).returns(rsa)
13
+ cipher = mock('cipher')
14
+ OpenSSL::Cipher::Cipher.expects(:new).with('des3').returns(cipher)
15
+ rsa.expects(:to_pem).with(cipher, 'secret').returns('private key')
16
+
17
+ assert_equal([rsa, 'private key'], Utils.generate_rsa_key('secret'))
18
+ end
19
+
20
+ def test_generate_aes_key
21
+ SecureRandom.expects(:base64).with(128).returns("x" * 172)
22
+ result = Utils.generate_aes_key
23
+ assert_equal 128, result.length
24
+ end
25
+
26
+ def test_open_rsa_key
27
+ rsa_path = fixture_path('id_rsa')
28
+ rsa = mock('rsa')
29
+ OpenSSL::PKey::RSA.expects(:new).with(File.read(rsa_path), 'secret').returns(rsa)
30
+ assert_equal(rsa, Utils.open_rsa_key(rsa_path, 'secret'))
31
+ end
32
+
33
+ def test_open_aes_data
34
+ aes_path = fixture_path('aes')
35
+ aes = {'key' => '12345', 'iv' => '54321'}
36
+ rsa = mock('rsa')
37
+ rsa.expects(:private_decrypt).with(File.read(aes_path)).returns(Marshal.dump(aes))
38
+ assert_equal(aes, Utils.open_aes_data(aes_path, rsa))
39
+ end
9
40
  end
10
41
  end
@@ -4,14 +4,11 @@ module Keyrack
4
4
  module UI
5
5
  class TestConsole < Test::Unit::TestCase
6
6
  def setup
7
- @path = get_tmpname
8
- @database = Database.new({
9
- 'store' => { 'type' => 'filesystem', 'path' => @path },
10
- 'key' => fixture_path('id_rsa'),
11
- 'password' => 'secret'
12
- })
13
- @database.add('Twitter', 'username', 'password')
14
- @database.save
7
+ @database = stub('database', :sites => %w{Twitter}, :dirty? => false) do
8
+ stubs(:get).with('Twitter').returns({
9
+ :username => 'username', :password => 'password'
10
+ })
11
+ end
15
12
  end
16
13
 
17
14
  def test_select_entry_from_menu
@@ -27,7 +24,7 @@ module Keyrack
27
24
  question = mock('question')
28
25
  question.expects(:in=).with(%w{n q 1})
29
26
  highline.expects(:ask).yields(question).returns('1')
30
- Clipboard.expects(:copy).with('password')
27
+ console.expects(:Copier).with('password')
31
28
  highline.expects(:say).with("The password has been copied to your clipboard.")
32
29
  assert_nil console.menu
33
30
  end
@@ -145,6 +142,62 @@ module Keyrack
145
142
  highline.expects(:agree).with("Generated bluefoobar. Sound good? [yn] ").returns(true).in_sequence(seq)
146
143
  assert_equal({:site => "Foo", :username => "bar", :password => "foobar"}, console.get_new_entry)
147
144
  end
145
+
146
+ def test_display_first_time_notice
147
+ highline = mock('highline')
148
+ HighLine.expects(:new).returns(highline)
149
+ console = Console.new
150
+
151
+ highline.expects(:say).with("This looks like your first time using Keyrack. I'll need to ask you a few questions first.")
152
+ console.display_first_time_notice
153
+ end
154
+
155
+ def test_rsa_setup
156
+ highline = mock('highline')
157
+ HighLine.expects(:new).returns(highline)
158
+ console = Console.new
159
+
160
+ seq = sequence("rsa setup")
161
+ highline.expects(:ask).with("New passphrase: ").yields(mock{expects(:echo=).with(false)}).returns('huge').in_sequence(seq)
162
+ highline.expects(:ask).with("Confirm passphrase: ").yields(mock{expects(:echo=).with(false)}).returns('small').in_sequence(seq)
163
+ highline.expects(:say).with("Passphrases didn't match.").in_sequence(seq)
164
+ highline.expects(:ask).with("New passphrase: ").yields(mock{expects(:echo=).with(false)}).returns('huge').in_sequence(seq)
165
+ highline.expects(:ask).with("Confirm passphrase: ").yields(mock{expects(:echo=).with(false)}).returns('huge').in_sequence(seq)
166
+ expected = {'password' => 'huge', 'path' => 'rsa'}
167
+ assert_equal expected, console.rsa_setup
168
+ end
169
+
170
+ def test_store_setup_for_filesystem
171
+ highline = mock('highline')
172
+ HighLine.expects(:new).returns(highline)
173
+ console = Console.new
174
+
175
+ highline.expects(:choose).yields(mock {
176
+ expects(:header=).with("Choose storage type:")
177
+ expects(:choices).with("filesystem", "ssh")
178
+ }).returns("filesystem")
179
+
180
+ expected = {'type' => 'filesystem', 'path' => 'database'}
181
+ assert_equal expected, console.store_setup
182
+ end
183
+
184
+ def test_store_setup_for_ssh
185
+ highline = mock('highline')
186
+ HighLine.expects(:new).returns(highline)
187
+ console = Console.new
188
+
189
+ seq = sequence("store setup")
190
+ highline.expects(:choose).yields(mock {
191
+ expects(:header=).with("Choose storage type:")
192
+ expects(:choices).with("filesystem", "ssh")
193
+ }).returns("ssh").in_sequence(seq)
194
+ highline.expects(:ask).with("Host: ").returns("example.com").in_sequence(seq)
195
+ highline.expects(:ask).with("User: ").returns("dudeguy").in_sequence(seq)
196
+ highline.expects(:ask).with("Remote path: ").returns(".keyrack/database").in_sequence(seq)
197
+
198
+ expected = {'type' => 'ssh', 'host' => 'example.com', 'user' => 'dudeguy', 'path' => '.keyrack/database'}
199
+ assert_equal expected, console.store_setup
200
+ end
148
201
  end
149
202
  end
150
203
  end
metadata CHANGED
@@ -4,9 +4,9 @@ version: !ruby/object:Gem::Version
4
4
  prerelease: false
5
5
  segments:
6
6
  - 0
7
- - 1
8
- - 3
9
- version: 0.1.3
7
+ - 2
8
+ - 0
9
+ version: 0.2.0
10
10
  platform: ruby
11
11
  authors:
12
12
  - Jeremy Stephens
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-12-13 00:00:00 -06:00
17
+ date: 2011-01-04 00:00:00 -06:00
18
18
  default_executable: keyrack
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -44,7 +44,7 @@ dependencies:
44
44
  prerelease: false
45
45
  version_requirements: *id002
46
46
  - !ruby/object:Gem::Dependency
47
- name: clipboard
47
+ name: viking-copier
48
48
  requirement: &id003 !ruby/object:Gem::Requirement
49
49
  none: false
50
50
  requirements:
@@ -112,6 +112,19 @@ dependencies:
112
112
  type: :development
113
113
  prerelease: false
114
114
  version_requirements: *id007
115
+ - !ruby/object:Gem::Dependency
116
+ name: cucumber
117
+ requirement: &id008 !ruby/object:Gem::Requirement
118
+ none: false
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ segments:
123
+ - 0
124
+ version: "0"
125
+ type: :development
126
+ prerelease: false
127
+ version_requirements: *id008
115
128
  description: Simple password manager with local/remote database storage and RSA encryption.
116
129
  email: viking@pillageandplunder.net
117
130
  executables:
@@ -132,6 +145,9 @@ files:
132
145
  - Rakefile
133
146
  - VERSION
134
147
  - bin/keyrack
148
+ - features/console.feature
149
+ - features/step_definitions/keyrack_steps.rb
150
+ - features/support/env.rb
135
151
  - keyrack.gemspec
136
152
  - lib/keyrack.rb
137
153
  - lib/keyrack/database.rb
@@ -142,10 +158,10 @@ files:
142
158
  - lib/keyrack/ui.rb
143
159
  - lib/keyrack/ui/console.rb
144
160
  - lib/keyrack/utils.rb
161
+ - test/fixtures/aes
145
162
  - test/fixtures/config.yml
146
163
  - test/fixtures/foo.txt
147
164
  - test/fixtures/id_rsa
148
- - test/fixtures/id_rsa.pub
149
165
  - test/helper.rb
150
166
  - test/keyrack/store/test_filesystem.rb
151
167
  - test/keyrack/store/test_ssh.rb
@@ -168,7 +184,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
168
184
  requirements:
169
185
  - - ">="
170
186
  - !ruby/object:Gem::Version
171
- hash: -3357719399858262545
187
+ hash: -415657060610893124
172
188
  segments:
173
189
  - 0
174
190
  version: "0"
@@ -1 +0,0 @@
1
- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC5BJJ44xstXL/qsJzIDTr7NTX64g/Nvi2z5fDM05mYQ1+cbAm+s+xI0vBrzY7nU4eVK62h5HOVOh3BB9vGfF3rfycEwj1ZpcDuMAlwQnmnmwLXXwQJV0VQtUgvukBKVX/RwSPJPkGk0nEGvpBmtn8IjtlR+Ido2jeKyKHsUCygvExoVTvJprCGLYxAH7Wo6wtF2idWwmBY6ApFY8cdf0VVBwSsIGjRmnMXf6ggsP/eoYBmR7aD3nGvb4uE1P1XX5XgRAcd7gHbHFe9LVErVoAT734MGMaSQJ6vfBdjBe/BfoAaquGJcr/b+DLc++JpPbaRbDX7IUIlB1vqnkOcLOi5 viking@ip-10-195-17-174