keyrack 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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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
- 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
- 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
- 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
- Lx1r9VKx4nqGm2SSM5SxbTJL+iHa7u7SFgb5zJUUGOO2iMMorpVfV4zpAd6TKsex
16
- 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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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
- 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
- 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
- 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
- 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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