keyrack 0.3.0.pre → 0.3.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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +8 -15
- data/Guardfile +6 -0
- data/LICENSE.txt +4 -2
- data/Rakefile +2 -56
- data/keyrack.gemspec +22 -104
- data/lib/keyrack.rb +9 -1
- data/lib/keyrack/database.rb +64 -98
- data/lib/keyrack/event.rb +13 -0
- data/lib/keyrack/exceptions.rb +4 -0
- data/lib/keyrack/group.rb +225 -0
- data/lib/keyrack/migrator.rb +45 -0
- data/lib/keyrack/runner.rb +98 -44
- data/lib/keyrack/site.rb +92 -0
- data/lib/keyrack/ui/console.rb +234 -95
- data/lib/keyrack/utils.rb +0 -18
- data/lib/keyrack/version.rb +3 -0
- data/test/fixtures/database-3.dat +0 -0
- data/test/helper.rb +11 -1
- data/test/integration/test_interactive_console.rb +139 -0
- data/test/unit/store/test_filesystem.rb +21 -0
- data/test/unit/store/test_ssh.rb +32 -0
- data/test/unit/test_database.rb +201 -0
- data/test/unit/test_event.rb +41 -0
- data/test/unit/test_group.rb +703 -0
- data/test/unit/test_migrator.rb +105 -0
- data/test/unit/test_runner.rb +407 -0
- data/test/unit/test_site.rb +181 -0
- data/test/unit/test_store.rb +13 -0
- data/test/unit/test_utils.rb +8 -0
- data/test/unit/ui/test_console.rb +495 -0
- metadata +98 -94
- data/Gemfile.lock +0 -45
- data/TODO +0 -4
- data/VERSION +0 -1
- data/features/console.feature +0 -103
- data/features/step_definitions/keyrack_steps.rb +0 -61
- data/features/support/env.rb +0 -16
- data/test/fixtures/aes +0 -0
- data/test/fixtures/config.yml +0 -4
- data/test/fixtures/id_rsa +0 -54
- data/test/keyrack/store/test_filesystem.rb +0 -25
- data/test/keyrack/store/test_ssh.rb +0 -36
- data/test/keyrack/test_database.rb +0 -111
- data/test/keyrack/test_runner.rb +0 -111
- data/test/keyrack/test_store.rb +0 -15
- data/test/keyrack/test_utils.rb +0 -41
- data/test/keyrack/ui/test_console.rb +0 -308
data/lib/keyrack/utils.rb
CHANGED
@@ -7,23 +7,5 @@ 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
|
28
10
|
end
|
29
11
|
end
|
Binary file
|
data/test/helper.rb
CHANGED
@@ -10,7 +10,7 @@ rescue Bundler::BundlerError => e
|
|
10
10
|
exit e.status_code
|
11
11
|
end
|
12
12
|
require 'test/unit'
|
13
|
-
require 'mocha'
|
13
|
+
require 'mocha/setup'
|
14
14
|
|
15
15
|
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
16
16
|
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
@@ -34,3 +34,13 @@ class Test::Unit::TestCase
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
end
|
37
|
+
|
38
|
+
class SequenceHelper < Object
|
39
|
+
def initialize(name)
|
40
|
+
@seq = Mocha::Sequence.new(name)
|
41
|
+
end
|
42
|
+
|
43
|
+
def <<(object)
|
44
|
+
object.in_sequence(@seq)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'pty'
|
3
|
+
require 'stringio'
|
4
|
+
|
5
|
+
class TestInteractiveConsole < Test::Unit::TestCase
|
6
|
+
def setup
|
7
|
+
@keyrack_dir = Dir::Tmpname.create('keyrack') { }
|
8
|
+
Dir.mkdir(@keyrack_dir)
|
9
|
+
@all_data = ""
|
10
|
+
end
|
11
|
+
|
12
|
+
def teardown
|
13
|
+
FileUtils.rm_rf(@keyrack_dir)
|
14
|
+
if @pid
|
15
|
+
begin
|
16
|
+
Process.kill("TERM", @pid)
|
17
|
+
rescue Errno::ESRCH
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def assert_output_matches(pattern, sentinel = "\r\n", timeout = 1)
|
23
|
+
assert_match pattern, get_output(sentinel, timeout)
|
24
|
+
end
|
25
|
+
|
26
|
+
def assert_output_equals(expected, timeout = 1)
|
27
|
+
assert_equal expected, get_output(expected, timeout)
|
28
|
+
end
|
29
|
+
|
30
|
+
def get_output(sentinel = "\r\n", timeout = 1)
|
31
|
+
output = ""
|
32
|
+
until output.end_with?(sentinel)
|
33
|
+
begin
|
34
|
+
data = @out.read_nonblock(1)
|
35
|
+
output << data
|
36
|
+
@all_data << data
|
37
|
+
rescue IO::WaitReadable
|
38
|
+
if IO.select([@out], [], [], timeout).nil?
|
39
|
+
flunk "output timed out"
|
40
|
+
else
|
41
|
+
retry
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
output
|
46
|
+
end
|
47
|
+
alias :eat_output :get_output
|
48
|
+
|
49
|
+
def run_keyrack
|
50
|
+
@out, @in, @pid = PTY.spawn(%{bundle exec ruby -Ilib bin/keyrack -d "#{@keyrack_dir}"})
|
51
|
+
end
|
52
|
+
|
53
|
+
def send_input(string, hidden = false)
|
54
|
+
@in.puts(string)
|
55
|
+
|
56
|
+
# Eat the echo
|
57
|
+
chomp = hidden ? "\r\n\r\n" : "\r\n"
|
58
|
+
get_output(string + chomp)
|
59
|
+
end
|
60
|
+
|
61
|
+
def clipboard
|
62
|
+
Clipboard.paste
|
63
|
+
end
|
64
|
+
|
65
|
+
test "keyrack session" do
|
66
|
+
run_keyrack
|
67
|
+
|
68
|
+
assert_output_matches /first time/
|
69
|
+
assert_output_equals "New passphrase: "
|
70
|
+
send_input "secret", true
|
71
|
+
|
72
|
+
assert_output_equals "Confirm passphrase: "
|
73
|
+
send_input "secret", true
|
74
|
+
|
75
|
+
menu = get_output("? ")
|
76
|
+
assert_match /Choose storage type:/, menu
|
77
|
+
assert_match /filesystem/, menu
|
78
|
+
assert_match /ssh/, menu
|
79
|
+
send_input "filesystem"
|
80
|
+
|
81
|
+
menu = get_output("? ")
|
82
|
+
assert_match /Keyrack Main Menu/, menu
|
83
|
+
assert_match /Mode: copy/, menu
|
84
|
+
assert_match "[n]ew", menu
|
85
|
+
send_input "n"
|
86
|
+
|
87
|
+
assert_output_equals "Label: "
|
88
|
+
send_input "Foo"
|
89
|
+
assert_output_equals "Username: "
|
90
|
+
send_input "dude"
|
91
|
+
assert_output_equals "Generate password? [ync] "
|
92
|
+
send_input "n"
|
93
|
+
assert_output_equals "Password: "
|
94
|
+
send_input "secret", true
|
95
|
+
assert_output_equals "Password (again): "
|
96
|
+
send_input "secret", true
|
97
|
+
|
98
|
+
menu = get_output("? ")
|
99
|
+
assert_match "1. Foo [dude]", menu
|
100
|
+
send_input "1"
|
101
|
+
assert_equal "secret", clipboard
|
102
|
+
|
103
|
+
menu = get_output("? ")
|
104
|
+
assert_match "[g]roup", menu
|
105
|
+
send_input "g"
|
106
|
+
assert_output_equals "Group: "
|
107
|
+
send_input "Stuff"
|
108
|
+
|
109
|
+
menu = get_output("? ")
|
110
|
+
assert_match "Stuff", menu.lines.first
|
111
|
+
assert_match "[g]roup", menu
|
112
|
+
send_input "g"
|
113
|
+
assert_output_equals "Group: "
|
114
|
+
send_input "More Stuff"
|
115
|
+
|
116
|
+
menu = get_output("? ")
|
117
|
+
assert_match "More Stuff", menu.lines.first
|
118
|
+
assert_match "[u]p", menu
|
119
|
+
send_input "u"
|
120
|
+
|
121
|
+
menu = get_output("? ")
|
122
|
+
assert_match "Stuff", menu.lines.first
|
123
|
+
assert_match "[t]op", menu
|
124
|
+
send_input "t"
|
125
|
+
|
126
|
+
menu = get_output("? ")
|
127
|
+
assert_match "Keyrack Main Menu", menu.lines.first
|
128
|
+
assert_match "[s]ave", menu
|
129
|
+
send_input "s"
|
130
|
+
assert_output_equals "Keyrack password: "
|
131
|
+
send_input "secret"
|
132
|
+
|
133
|
+
menu = get_output("? ", 10)
|
134
|
+
assert_match "[q]uit", menu
|
135
|
+
send_input "q"
|
136
|
+
|
137
|
+
Process.waitpid(@pid)
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestFilesystem < Test::Unit::TestCase
|
4
|
+
def test_read
|
5
|
+
path = fixture_path('foo.txt')
|
6
|
+
store = Keyrack::Store::Filesystem.new('path' => path)
|
7
|
+
assert_equal File.read(path), store.read
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_write
|
11
|
+
path = get_tmpname
|
12
|
+
store = Keyrack::Store::Filesystem.new('path' => path)
|
13
|
+
store.write("foobar")
|
14
|
+
assert_equal "foobar", File.read(path)
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_read_returns_nil_for_non_existant_file
|
18
|
+
store = Keyrack::Store::Filesystem.new('path' => 'blargityblargh')
|
19
|
+
assert_nil store.read
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestSSH < Test::Unit::TestCase
|
4
|
+
def test_read
|
5
|
+
store = Keyrack::Store::SSH.new('host' => 'example.com', 'user' => 'dude', 'port' => 22, 'path' => 'foo.txt')
|
6
|
+
scp = mock('scp session')
|
7
|
+
scp.expects(:download!).with("foo.txt").returns("foo")
|
8
|
+
session = mock('ssh session', :scp => scp)
|
9
|
+
Net::SSH.expects(:start).with('example.com', 'dude', :port => 22).yields(session)
|
10
|
+
assert_equal "foo", store.read
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_write
|
14
|
+
store = Keyrack::Store::SSH.new('host' => 'example.com', 'user' => 'dude', 'path' => 'foo.txt')
|
15
|
+
scp = mock('scp session')
|
16
|
+
session = mock('ssh session', :scp => scp)
|
17
|
+
Net::SSH.expects(:start).with('example.com', 'dude', :port => 22).yields(session)
|
18
|
+
scp.expects(:upload!).with do |local, remote|
|
19
|
+
local.is_a?(StringIO) && local.read == "foo" && remote == "foo.txt"
|
20
|
+
end
|
21
|
+
store.write("foo")
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_read_returns_nil_for_non_existant_file
|
25
|
+
store = Keyrack::Store::SSH.new('host' => 'example.com', 'user' => 'dude', 'port' => 22, 'path' => 'foo.txt')
|
26
|
+
scp = mock('scp session')
|
27
|
+
scp.expects(:download!).with("foo.txt").raises(Net::SCP::Error)
|
28
|
+
session = mock('ssh session', :scp => scp)
|
29
|
+
Net::SSH.expects(:start).with('example.com', 'dude', :port => 22).yields(session)
|
30
|
+
assert_nil store.read
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestDatabase < Test::Unit::TestCase
|
4
|
+
class TestReload < self
|
5
|
+
def setup
|
6
|
+
super
|
7
|
+
@database = Keyrack::Database.new(@key, @store, @encrypt_options, @decrypt_options)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.database_test(test_description, reload, &block)
|
12
|
+
if reload
|
13
|
+
TestReload.class_eval do
|
14
|
+
test(test_description, &block)
|
15
|
+
end
|
16
|
+
else
|
17
|
+
test(test_description, &block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def setup
|
22
|
+
@path = get_tmpname
|
23
|
+
@store = Keyrack::Store['filesystem'].new('path' => @path)
|
24
|
+
|
25
|
+
@encrypt_options = { :maxmem => 0, :maxmemfrac => 0.05, :maxtime => 0.1 }
|
26
|
+
@decrypt_options = { :maxmem => 0, :maxmemfrac => 0.10, :maxtime => 1.0 }
|
27
|
+
@key = "secret"
|
28
|
+
@database = Keyrack::Database.new(@key, @store, @encrypt_options, @decrypt_options)
|
29
|
+
@twitter = Keyrack::Site.new('Twitter', 'dude', 'p4ssword')
|
30
|
+
@database.top_group.add_site(@twitter)
|
31
|
+
assert @database.save(@key)
|
32
|
+
end
|
33
|
+
|
34
|
+
def decrypt(data, key = @key, options = @decrypt_options)
|
35
|
+
Scrypty.decrypt(data, key, *options.values_at(:maxmem, :maxmemfrac, :maxtime))
|
36
|
+
end
|
37
|
+
|
38
|
+
test "encrypting database" do
|
39
|
+
encrypted_data = File.read(@path)
|
40
|
+
yaml = decrypt(encrypted_data, @key)
|
41
|
+
data = YAML.load(yaml)
|
42
|
+
expected = {
|
43
|
+
'groups' => {
|
44
|
+
'top' => {
|
45
|
+
'name' => 'top',
|
46
|
+
'sites' => [
|
47
|
+
{
|
48
|
+
'name' => 'Twitter',
|
49
|
+
'username' => 'dude',
|
50
|
+
'password' => 'p4ssword'
|
51
|
+
}
|
52
|
+
],
|
53
|
+
'groups' => {}
|
54
|
+
}
|
55
|
+
},
|
56
|
+
'version' => Keyrack::Database::VERSION
|
57
|
+
}
|
58
|
+
assert_equal(expected, data)
|
59
|
+
end
|
60
|
+
|
61
|
+
test "auto-migrating database from version 3" do
|
62
|
+
store = Keyrack::Store['filesystem'].new('path' => fixture_path('database-3.dat'))
|
63
|
+
database = Keyrack::Database.new('foobar', store)
|
64
|
+
assert database.dirty?
|
65
|
+
end
|
66
|
+
|
67
|
+
[true, false].each do |reload|
|
68
|
+
database_test "database is dirty after adding site to top group", reload do
|
69
|
+
assert !@database.dirty?
|
70
|
+
site = Keyrack::Site.new('Foo', 'foo', 'bar')
|
71
|
+
@database.top_group.add_site(site)
|
72
|
+
assert @database.dirty?
|
73
|
+
end
|
74
|
+
|
75
|
+
database_test "database is dirty after removing site from top group", reload do
|
76
|
+
assert !@database.dirty?
|
77
|
+
@database.top_group.remove_site(@twitter)
|
78
|
+
assert @database.dirty?
|
79
|
+
end
|
80
|
+
|
81
|
+
database_test "database is dirty after changing username", reload do
|
82
|
+
assert !@database.dirty?
|
83
|
+
twitter = @database.top_group.site(0)
|
84
|
+
twitter.username = 'bro'
|
85
|
+
assert @database.dirty?
|
86
|
+
end
|
87
|
+
|
88
|
+
database_test "database is dirty after changing password", reload do
|
89
|
+
assert !@database.dirty?
|
90
|
+
twitter = @database.top_group.site(0)
|
91
|
+
twitter.password = 'secret'
|
92
|
+
assert @database.dirty?
|
93
|
+
end
|
94
|
+
|
95
|
+
database_test "database is dirty after adding subgroup", reload do
|
96
|
+
assert !@database.dirty?
|
97
|
+
group = Keyrack::Group.new('Foo')
|
98
|
+
@database.top_group.add_group(group)
|
99
|
+
assert @database.dirty?
|
100
|
+
end
|
101
|
+
|
102
|
+
database_test "database is dirty after removing subgroup", reload do
|
103
|
+
group = Keyrack::Group.new('Foo')
|
104
|
+
@database.top_group.add_group(group)
|
105
|
+
assert @database.save(@key)
|
106
|
+
|
107
|
+
assert !@database.dirty?
|
108
|
+
@database.top_group.remove_group('Foo')
|
109
|
+
assert @database.dirty?
|
110
|
+
end
|
111
|
+
|
112
|
+
database_test "database is dirty after adding site to subgroup", reload do
|
113
|
+
assert !@database.dirty?
|
114
|
+
group = Keyrack::Group.new('Foo')
|
115
|
+
@database.top_group.add_group(group)
|
116
|
+
assert @database.save(@key)
|
117
|
+
|
118
|
+
assert !@database.dirty?
|
119
|
+
site = Keyrack::Site.new('Bar', 'bar', 'baz')
|
120
|
+
group.add_site(site)
|
121
|
+
assert @database.dirty?
|
122
|
+
end
|
123
|
+
|
124
|
+
database_test "database is dirty after removing site from subgroup", reload do
|
125
|
+
assert !@database.dirty?
|
126
|
+
group = Keyrack::Group.new('Foo')
|
127
|
+
@database.top_group.add_group(group)
|
128
|
+
assert @database.save(@key)
|
129
|
+
|
130
|
+
assert !@database.dirty?
|
131
|
+
site = Keyrack::Site.new('Bar', 'bar', 'baz')
|
132
|
+
group.add_site(site)
|
133
|
+
assert @database.save(@key)
|
134
|
+
|
135
|
+
assert !@database.dirty?
|
136
|
+
group.remove_site(site)
|
137
|
+
assert @database.dirty?
|
138
|
+
end
|
139
|
+
|
140
|
+
database_test "database is dirty after adding group to subgroup", reload do
|
141
|
+
assert !@database.dirty?
|
142
|
+
group = Keyrack::Group.new('Foo')
|
143
|
+
@database.top_group.add_group(group)
|
144
|
+
assert @database.save(@key)
|
145
|
+
|
146
|
+
assert !@database.dirty?
|
147
|
+
subgroup = Keyrack::Group.new('Bar')
|
148
|
+
group.add_group(subgroup)
|
149
|
+
assert @database.dirty?
|
150
|
+
end
|
151
|
+
|
152
|
+
database_test "database is dirty after changing group name", reload do
|
153
|
+
assert !@database.dirty?
|
154
|
+
group = Keyrack::Group.new('Foo')
|
155
|
+
@database.top_group.add_group(group)
|
156
|
+
assert @database.save(@key)
|
157
|
+
|
158
|
+
assert !@database.dirty?
|
159
|
+
group.name = "Bar"
|
160
|
+
assert @database.dirty?
|
161
|
+
end
|
162
|
+
|
163
|
+
database_test "large number of entries", reload do
|
164
|
+
site_name = "abcdefg"; username = "1234567"; password = "zyxwvut" * 2
|
165
|
+
500.times do |i|
|
166
|
+
site = Keyrack::Site.new(site_name, username, password)
|
167
|
+
@database.top_group.add_site(site)
|
168
|
+
site_name = site_name.next
|
169
|
+
username = username.next
|
170
|
+
password = password.next
|
171
|
+
end
|
172
|
+
assert @database.save(@key)
|
173
|
+
assert_equal 501, @database.top_group.sites.length
|
174
|
+
end
|
175
|
+
|
176
|
+
database_test "saving requires same password as creation", reload do
|
177
|
+
site = Keyrack::Site.new("Foo", 'foo', 'bar')
|
178
|
+
@database.top_group.add_site(site)
|
179
|
+
|
180
|
+
assert !@database.save("bogus")
|
181
|
+
end
|
182
|
+
|
183
|
+
database_test "changing database password successfully", reload do
|
184
|
+
assert @database.change_password(@key, "new-secret")
|
185
|
+
|
186
|
+
site = Keyrack::Site.new("Foo", 'bar', 'baz')
|
187
|
+
@database.top_group.add_site(site)
|
188
|
+
|
189
|
+
assert @database.save("new-secret")
|
190
|
+
end
|
191
|
+
|
192
|
+
database_test "attempting to change database password with wrong existing password", reload do
|
193
|
+
assert !@database.change_password("bogus", "new-secret")
|
194
|
+
|
195
|
+
site = Keyrack::Site.new("Foo", 'foo', 'bar')
|
196
|
+
@database.top_group.add_site(site)
|
197
|
+
|
198
|
+
assert !@database.save("new-secret")
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|