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 +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
|
+
8s6OIKXWfXq6xrPRrJ3m2qU/Xd+N35Y9cCjr+nqnESWGBrT7ZLLIiS4xEMy2WwHB
|
6
|
+
N9kj0OekQxDz0yfCP/+CyCMvtFpJGwbqgOCJWXyDsmaQDVG3Z7MRehoGA2s9Zkab
|
7
|
+
2h4F6YE7iE3oVY/2Z86SBhk4KITyGi0Jb8/KHvoig+iF6rqAPos0I5Y3M1vVa9DJ
|
8
|
+
lFdyoTbFzx+mzbEXIefGOdcXinkfAoG7MchPiEypunrpEgYPddsON2b/dQfIBmEy
|
9
|
+
3+gN9FnbWH3LNTOShzl4l4v2aBfxXgg5bHPnKkJiUNRDlY6ozyQYfxxoOs4uL8mI
|
10
|
+
WnfmVGezxkD3YWHB+xwevLaIjChowSNP/q7sSHVOD/bVadmhd30I7CnhFITKi0Wp
|
11
|
+
vr0qbGpSrsWm7zCtSWcK0fFMGKea2UoH4lrfqJv95mOhZ+pmok35CxGUbG0S53rm
|
12
|
+
A4ggoIr3cYvRMPkp/Q9dZQxE6FHftXO9C+IvJc1CmtUJl+LR7aOtQy4pgPyRV83x
|
13
|
+
yfw2SrJ9ls0RZKm4lQUqlwDrru20hf8YiyeLPGlpnRpVVGkdNfhR3Q/e1Tl4qr/3
|
14
|
+
ITJ1uxopaQZtggR+9wlYG9oS5htJ9VZo+Jh8Oys5c0ZAPlzHQMuU3TQVeLuXIAQ6
|
15
|
+
zgLyThmKGgG3iBKG2FUZIpGX3WGIFgKwuwAQJXs0fthF5JJAuBXY5Jbvu3y1AQDC
|
16
|
+
rC9XgYr9PfL8o4zIXxxRvxUny2YO+XaKjgiOV9kKV5reUNJQW95sCvuZZyFh3STW
|
17
|
+
tywsdQ7ZzsHLJ0sun0Dj4GjbIfMb9l95x1baMIAcXqbqmS9c6Hd53gM2e+8d0Wja
|
18
|
+
tBb9f2rddcmoyMNB/MKTnCm1L7GjrHtQJXClCKlpyDgZrUbdHcRhq4UY8diVVdam
|
19
|
+
ZhIVRdMIYr70BUhpTH3gqBLQlNX0D6DItCNdI+cr9p2BUqM87xTeQM84HfKOyZZO
|
20
|
+
ZqgwZ5jhxjgyrQYADeBD6RqH7fcnitowksRGxZdnZQdm+U+SfQbfmA0zYCh+jgz3
|
21
|
+
fMVQ+9sjnGZzl0/Wn8Kg8K2jA33N96ou7X89wyiSLMvAW6G+/CaxQMEgTDy+mxES
|
22
|
+
BLv7h+LdGzOtRE8556hBmNs5ku8AoMmP5irvEkGJIjtB+JcmeIi+AD0hBSq7O4Gp
|
23
|
+
5OoyXEYp+cYLTQtyLEQ6/CnSEvjgzXRG2xKY9wxspXaJn/q7QwadkgltvjtUo0Iu
|
24
|
+
OXV6MJGQYkVkm5xf3tmvuQYQNV3NK/nQpDixhMJye19Kjr7jelRpcr+Y5VXelRP5
|
25
|
+
5CiqpqW0DtM1YWQBUr7p3ckft1DCYB48ARcEqmrgV7KASawZAh7Lun/SkiJ6SYl0
|
26
|
+
3Q4QVhQbOyx1S04UPRWzEqIBquHqMFzEjnzkQRFAxvnWJx3mnQudAi1yKtKZDpLL
|
27
|
+
3kuMXJ90hg+/VoEWehEG+VZXxPOgFf624Wv9SnEv2vtxZv4g8B+Ny9SXl/CzDiA0
|
28
|
+
GsbA9tOzJc9MmYCts19QQ85zsp9B1eJO7oxh5OETOe2FkivU/HqMOp0A3B3rEjY+
|
29
|
+
CS0g15bqEmCyFxvbBkR8aUZ6c5fHvNngvxYbkGVLyHpgIHmszGj1+gz/8DlEXldM
|
30
|
+
EE5gY6kZ/iH8ikTjhPCX3Cd8HybjOh1JQogTbOeOMGhkKtEEs+XyFZYP6OrC2Cuf
|
31
|
+
Yk2osm4J+g+mPXFjQU/Ie9f6Q8Bv0tShFHEh17N4/IJRQPCRhiIMfq/PaBp7Rt3m
|
32
|
+
RT3fmPl/g4WywBVfC8jv4ES+HA2xWC3cds8rOeh3mgmaimNRmPzpHVe/vyxSUqUh
|
33
|
+
PC8BXJ+wN+jNg718MYNneah1cC9hsrCMa3wU9dv59LGHsC0rNrbCqmmGMFZVKldV
|
34
|
+
4HbG77K6QyLjk8bNsVUtwV+0hClBAWHi6K5dAY7IOCdGcVgrZ+drFl1sDyM4vkZ+
|
35
|
+
kkBnNLrvczR3Mqh+Q901Ha1aepTyHbFN0EMK965Ni8PWw41cYXvgKzRDFJi1iuvx
|
36
|
+
hr3qu2icCOeVCLE/Vnk0w0t6pxr+1V0sxYx3weBXHDO3NS6EEFKdX8zoGZI17MSF
|
37
|
+
QVYq1Oko1LU44VvXfw18yS/dsu7nxC106uL9EPOjaN+wf0+V/i9s3AWnPCYftwA8
|
38
|
+
9l2EBuhElh8drZ2Kb0aL1i0VzB/1zYwDbNZnVhhRF2CAcXTmnpD6RXfgLUnnUVGa
|
39
|
+
nCPZ8/ks84rXjAxWNcLHSCdVWzczHDKz22VJFa+AIrqMxz77r+aEAzMvxHNAbs5v
|
40
|
+
E8cPNfmir56mOSD0TRrfPqKoCARLcmLd97RGikh3kw1BYVI02BAwQ3jABrW5v/53
|
41
|
+
fg8fMafNuQl3Xw4LkQ4VoKAwR4mi5JUN5ngeRDfQVmBs1GOVV981dHvrfrGa3lrR
|
42
|
+
xHTb7iYXU0XJ219yZHNtGzaTs6g+xvbMF9aELkfbIPGkwqMlKp/2OQmQqq171Nez
|
43
|
+
me/suc8ZzmZsVGVhW3a/uvB7+DLOgnO41AxQVe9DXO8aVjpjwdpXWmPt8sisgMov
|
44
|
+
uJFTUBsnG6rg2agxzWrQEQpBONkeY3CGk5P3JLIruhXhNCFs4pjff7xKrdppHmBe
|
45
|
+
ukm5z2jEbvbdt7VQ/UtGeWzUEoiRh2k7EK/EnK7j0NRsKMPe5DPsIncgWdB/krTn
|
46
|
+
iXzzvHY0iN6W0ZQELuN8wvt5lnnDmak21+Q/vaA04NHWBhznCIggZDl4+MIuYIBo
|
47
|
+
T+4OkwEIObuKOW06AI8qIaKVX24bmeZ2Fcj4dGMT0SHlXga6S4ouVUeZ1hOCGg9Z
|
48
|
+
4Nw53gzWaOIW3TntoNoieibjzHigUWghnXEpn1DB+dfGhTxDWpSZKRIkuBNPEVvn
|
49
|
+
FtXPwvUg0yrMp4JItSQ7PT2GMtar16qBc+0bgQlX53Vb5kW22rIoNq5zDwWcNB8s
|
50
|
+
0octG6yq6aEVId/3/2rrQi8izkoZXZe8FJgR02o9M4J/4HsJ74xbqnbuNE7FUZiZ
|
51
|
+
EX+2BKoNEShxujW+zd1NpSNSfGRAYuk0llUMsbRO8ZfNlZWj9fTNmPHshCWIvZ3a
|
52
|
+
yxTdjoC3SPtAUz+ZDznBnlIiXyU6XcUPEY+FBsN9K2S0Blb8fvc1cxjL1H1r5Pdj
|
53
|
+
yPJAIxlfePv1TRYzCf2kmdnIOq3yoCWAU6RxGq8TII6KGvKP14OtMHCyonDB2/UZ
|
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
|