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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/Gemfile +8 -15
  4. data/Guardfile +6 -0
  5. data/LICENSE.txt +4 -2
  6. data/Rakefile +2 -56
  7. data/keyrack.gemspec +22 -104
  8. data/lib/keyrack.rb +9 -1
  9. data/lib/keyrack/database.rb +64 -98
  10. data/lib/keyrack/event.rb +13 -0
  11. data/lib/keyrack/exceptions.rb +4 -0
  12. data/lib/keyrack/group.rb +225 -0
  13. data/lib/keyrack/migrator.rb +45 -0
  14. data/lib/keyrack/runner.rb +98 -44
  15. data/lib/keyrack/site.rb +92 -0
  16. data/lib/keyrack/ui/console.rb +234 -95
  17. data/lib/keyrack/utils.rb +0 -18
  18. data/lib/keyrack/version.rb +3 -0
  19. data/test/fixtures/database-3.dat +0 -0
  20. data/test/helper.rb +11 -1
  21. data/test/integration/test_interactive_console.rb +139 -0
  22. data/test/unit/store/test_filesystem.rb +21 -0
  23. data/test/unit/store/test_ssh.rb +32 -0
  24. data/test/unit/test_database.rb +201 -0
  25. data/test/unit/test_event.rb +41 -0
  26. data/test/unit/test_group.rb +703 -0
  27. data/test/unit/test_migrator.rb +105 -0
  28. data/test/unit/test_runner.rb +407 -0
  29. data/test/unit/test_site.rb +181 -0
  30. data/test/unit/test_store.rb +13 -0
  31. data/test/unit/test_utils.rb +8 -0
  32. data/test/unit/ui/test_console.rb +495 -0
  33. metadata +98 -94
  34. data/Gemfile.lock +0 -45
  35. data/TODO +0 -4
  36. data/VERSION +0 -1
  37. data/features/console.feature +0 -103
  38. data/features/step_definitions/keyrack_steps.rb +0 -61
  39. data/features/support/env.rb +0 -16
  40. data/test/fixtures/aes +0 -0
  41. data/test/fixtures/config.yml +0 -4
  42. data/test/fixtures/id_rsa +0 -54
  43. data/test/keyrack/store/test_filesystem.rb +0 -25
  44. data/test/keyrack/store/test_ssh.rb +0 -36
  45. data/test/keyrack/test_database.rb +0 -111
  46. data/test/keyrack/test_runner.rb +0 -111
  47. data/test/keyrack/test_store.rb +0 -15
  48. data/test/keyrack/test_utils.rb +0 -41
  49. data/test/keyrack/ui/test_console.rb +0 -308
@@ -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
@@ -0,0 +1,3 @@
1
+ module Keyrack
2
+ VERSION = "0.3.0"
3
+ end
@@ -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