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 +2 -1
- data/Gemfile.lock +16 -8
- data/VERSION +1 -1
- data/features/console.feature +39 -0
- data/features/step_definitions/keyrack_steps.rb +45 -0
- data/features/support/env.rb +13 -0
- data/lib/keyrack/database.rb +14 -7
- data/lib/keyrack/runner.rb +44 -10
- data/lib/keyrack/ui/console.rb +35 -1
- data/lib/keyrack/utils.rb +18 -0
- data/lib/keyrack.rb +2 -1
- data/test/fixtures/aes +0 -0
- data/test/fixtures/id_rsa +50 -26
- data/test/helper.rb +2 -1
- data/test/keyrack/test_database.rb +20 -12
- data/test/keyrack/test_runner.rb +86 -19
- data/test/keyrack/test_utils.rb +31 -0
- data/test/keyrack/ui/test_console.rb +62 -9
- metadata +23 -7
- data/test/fixtures/id_rsa.pub +0 -1
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 '
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
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.
|
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
|
-
|
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
|
-
|
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
|
+
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'
|
data/lib/keyrack/database.rb
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
module Keyrack
|
2
2
|
class Database
|
3
|
-
def initialize(
|
4
|
-
|
5
|
-
@
|
6
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/keyrack/runner.rb
CHANGED
@@ -1,21 +1,55 @@
|
|
1
1
|
module Keyrack
|
2
2
|
class Runner
|
3
3
|
def initialize(argv)
|
4
|
-
|
5
|
-
:config_path => "~/.keyrack/config"
|
6
|
-
}
|
4
|
+
@config_path = "~/.keyrack"
|
7
5
|
OptionParser.new do |opts|
|
8
|
-
opts.on("-
|
9
|
-
|
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
|
|
data/lib/keyrack/ui/console.rb
CHANGED
@@ -38,7 +38,7 @@ module Keyrack
|
|
38
38
|
:quit
|
39
39
|
end
|
40
40
|
else
|
41
|
-
|
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
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:
|
3
|
+
DEK-Info: DES-EDE3-CBC,030F0E3D8BD7BF1A
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
5
|
+
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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|
|
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
|
-
@
|
8
|
-
|
9
|
-
|
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
|
-
|
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
|
data/test/keyrack/test_runner.rb
CHANGED
@@ -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' =>
|
8
|
-
'
|
21
|
+
'store' => { 'type' => 'filesystem', 'path' => store_path },
|
22
|
+
'rsa' => rsa_path, 'aes' => aes_path
|
9
23
|
}
|
10
|
-
|
11
|
-
|
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
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
data/test/keyrack/test_utils.rb
CHANGED
@@ -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
|
-
@
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
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
|
-
-
|
8
|
-
-
|
9
|
-
version: 0.
|
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:
|
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:
|
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: -
|
187
|
+
hash: -415657060610893124
|
172
188
|
segments:
|
173
189
|
- 0
|
174
190
|
version: "0"
|
data/test/fixtures/id_rsa.pub
DELETED
@@ -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
|