team-secrets 0.1.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.
@@ -0,0 +1,448 @@
1
+ #!/usr/bin/env ruby
2
+ require 'rubygems'
3
+ require 'gli'
4
+ require 'io/console'
5
+
6
+ require 'yaml'
7
+ require 'fileutils'
8
+
9
+ require 'digest'
10
+ require 'openssl'
11
+
12
+ require_relative 'team-secrets/manifest_manager'
13
+ require_relative 'team-secrets/user_manager'
14
+ require_relative 'team-secrets/secret_manager'
15
+
16
+ include GLI::App
17
+
18
+ program_desc 'Secrets - sharing secrets secretly'
19
+
20
+ pre do |global_options,command,options,args|
21
+ config = File.read('config.yaml') if File.exists?('config.yaml')
22
+ config = YAML.load(config) || {}
23
+
24
+ unless options.key? :user && !options[:user].nil?
25
+ if config.key? :user
26
+ options[:user] = config[:user]
27
+ else
28
+ puts 'Your user name was not specified. Use the `-u` flag or put it in config.yaml.'
29
+ next false
30
+ end
31
+ else
32
+ options[:user] = options[:user].strip
33
+ end
34
+
35
+ unless options.key? :private && !options[:user].nil?
36
+ if config.key? :private
37
+ options[:private] = config[:private]
38
+ else
39
+ puts 'Your private key was not specified. Use the `-p` flag or put it in config.yaml.'
40
+ next false
41
+ end
42
+ end
43
+
44
+ true
45
+ end
46
+
47
+ valid_user_names = /\A[A-Z0-9_\.]+\Z/i
48
+
49
+ desc 'Start a new Secrets repository'
50
+ long_desc 'Create the necessary file structure to create a new Secrets repository'
51
+
52
+ skips_pre
53
+ command :init do |c|
54
+
55
+ c.desc 'Your username'
56
+ c.flag [:u,:user], type: String, must_match: valid_user_names
57
+
58
+ c.desc 'Path to public key (in PEM format)'
59
+ c.flag [:k,:key_file], type: String
60
+
61
+ c.action do |global_options,options,args|
62
+ raise 'OpenSSL must be installed and in the PATH' unless system("openssl version")
63
+
64
+ user_name = options[:user]
65
+
66
+ until user_name && (/\A.+\z/i =~ user_name)
67
+ default = `echo $USER`.chomp
68
+ print "Your username (no spaces) [#{default}]: "
69
+ user_name = STDIN.gets.chomp
70
+ user_name = default if user_name.empty?
71
+ end
72
+
73
+ key_file = options[:key_file]
74
+
75
+ until key_file && File.exists?(key_file)
76
+ print 'Path to public key: '
77
+ key_file = STDIN.gets.chomp
78
+ puts "File does not exist or cannot be acccessed." unless File.exists?(key_file)
79
+ end
80
+
81
+ puts "Creating users directory & users.yaml..."
82
+
83
+ users_file = UserManager.new
84
+ users_file.add user_name, key_file
85
+ # New master key
86
+ master_key = users_file.master_key
87
+ users_file.writeFile 'users.yaml'
88
+
89
+ puts "Creating template secrets.yaml..."
90
+
91
+ secrets_file = SecretManager.new
92
+ secrets_file.writeFile 'secrets.yaml'
93
+
94
+ puts "Writing manifest.yaml..."
95
+
96
+ manifest = ManifestManager.new master_key
97
+ manifest.update
98
+ manifest.writeFile 'manifest.yaml'
99
+
100
+ puts green('Done!')
101
+ puts 'Now, create a new repository with these files and commit. Your new secrets repo is ready to go.'
102
+ end
103
+ end
104
+
105
+ desc 'Manage users for this Secrets repository'
106
+ long_desc 'Add and remove users or servers who will be able to manage this Secrets repository'
107
+
108
+ command :user do |c|
109
+
110
+ c.desc 'Your user name'
111
+ c.flag [:u,:user], type: String, must_match: valid_user_names
112
+
113
+ c.desc 'Path to your private key'
114
+ c.flag [:p,:private], type: String
115
+
116
+ c.desc 'Add a new user'
117
+ c.command :add do |add|
118
+ add.action do |global_options,options,args|
119
+
120
+ print 'New user\'s name: '
121
+ new_user = STDIN.gets.chomp
122
+ raise 'A user name must be specified' if new_user.empty?
123
+
124
+ print 'Path to user\'s public key: '
125
+ key_file = STDIN.gets.chomp
126
+ raise "File does not exist or cannot be acccessed." unless File.exists?(key_file)
127
+
128
+ # Check signatures
129
+ # Use current user's private key to get master key from lock_box
130
+ # Use master key to get all secrets
131
+ # Add new user's record & public key
132
+ # Generate new master key
133
+ # Encrypt all secrets with new master key
134
+ # Encrypt master key with each user's public key, placing in lock_box
135
+ # Update signatures
136
+
137
+ end
138
+ end
139
+
140
+ c.desc 'List all users'
141
+ c.command :list do |add|
142
+ add.action do
143
+
144
+ # List all users
145
+ users = UserManager.new
146
+ users.loadFile 'users.yaml'
147
+
148
+ puts "#{users.all.length} users:\n"
149
+ users.all.each do |user|
150
+ puts user+"\n"
151
+ end
152
+
153
+ end
154
+ end
155
+
156
+ c.desc 'Remove a user'
157
+ c.command :rm do |add|
158
+ add.action do |global_options,options,args|
159
+
160
+ print 'User to remove: '
161
+ new_user = STDIN.gets.chomp
162
+ raise 'A user name must be specified' if new_user.empty?
163
+
164
+ # Check signatures
165
+ # Use current user's private key to get master key from lock_box
166
+ # Use master key to get all secrets
167
+ # Remove old user's record & public key
168
+ # Generate new master key
169
+ # Encrypt all secrets with new master key
170
+ # Encrypt master key with each user's public key, placing in lock_box
171
+ # Update signatures
172
+
173
+ master_key = load_master_key(options[:user], options[:private])
174
+
175
+ manifest = ManifestManager.new master_key
176
+ manifest.validate
177
+
178
+ secrets = SecretManager.new master_key
179
+ secrets.loadFile 'secrets.yaml'
180
+
181
+ users = UserManager.new master_key
182
+
183
+ remove_data = users.find options[:remove]
184
+
185
+ raise 'User not found for removal' if remove_data.nil?
186
+
187
+ users.remove remove_data[:user]
188
+ users.writeFile 'users.yaml'
189
+
190
+ master_key = users.master_key
191
+
192
+ secrets.rotateMasterKey master_key
193
+ secrets.writeFile 'secrets.yaml'
194
+
195
+ manifest.master_key = master_key
196
+ manifest.update
197
+ manifest.writeFile 'manifest.yaml'
198
+
199
+ print green('Success! ')
200
+ puts 'User removed.'
201
+
202
+ end
203
+ end
204
+
205
+ end
206
+
207
+ desc 'Manage the secrets in this Secrets repository'
208
+ long_desc 'Add, read and remove secrets that users can retrieve from this Secrets repository'
209
+
210
+ command :secret do |c|
211
+
212
+ c.desc 'Your user name'
213
+ c.arg_name 'user'
214
+ c.flag [:u,:user], type: String, must_match: valid_user_names
215
+
216
+ c.desc 'Path to your private key'
217
+ c.arg_name 'private'
218
+ c.flag [:p,:private], type: String
219
+
220
+ c.desc 'Name of this secret (e.g., SMTP_PASS)'
221
+ c.arg_name 'name'
222
+ c.flag [:n,:name], type: String
223
+
224
+ c.desc 'The optional account name (e.g., a username)'
225
+ c.arg_name 'account', :optional
226
+ c.flag [:a,:account], type: String
227
+
228
+ c.desc 'Optional tags (e.g., PROD)'
229
+ c.arg_name 'tags', :optional
230
+ c.flag [:t,:tags], type: String
231
+
232
+ c.desc 'Any optional notes'
233
+ c.arg_name 'notes', :optional
234
+ c.flag [:notes], type: String
235
+
236
+ c.desc 'Add a new secret'
237
+ c.command :add do |add|
238
+ add.action do |global_options,options,args|
239
+
240
+ if options[:name].nil?
241
+ print 'A name for this secret: '
242
+ options[:name] = STDIN.gets.chomp
243
+ raise 'A name must be specified' if options[:name].empty?
244
+ end
245
+
246
+ if options[:account].nil?
247
+ print 'The secret\'s account name (optional): '
248
+ options[:account] = STDIN.gets.chomp
249
+ end
250
+
251
+ if options[:tags].nil?
252
+ print 'Tags for this secret, space-separated (optional): '
253
+ options[:tags] = STDIN.gets.chomp
254
+ end
255
+
256
+ tags = parse_tags(options[:tags])
257
+
258
+ if args.empty?
259
+ print 'Secret to encrypt: '
260
+ secret = STDIN.noecho(&:gets)
261
+ secret = secret.chomp
262
+ raise 'No secret given' if secret.empty?
263
+ else
264
+ secret = args[0]
265
+ end
266
+
267
+ # Check signatures
268
+ # Use current user's private key to get master key from lock_box
269
+ # Add new secret's record
270
+ # Encrypt secret with master key
271
+ # Update signatures
272
+ puts
273
+
274
+ master_key = load_master_key(options[:user], options[:private])
275
+
276
+ manifest = ManifestManager.new master_key
277
+ manifest.validate
278
+
279
+ secrets = SecretManager.new master_key
280
+ secrets.loadFile 'secrets.yaml'
281
+ secrets.add options[:name].strip, secret, options[:account], tags
282
+ secrets.writeFile 'secrets.yaml'
283
+
284
+ manifest.update
285
+ manifest.writeFile 'manifest.yaml'
286
+
287
+ print green('Success! ')
288
+ puts 'New secret has been encrypted and added.'
289
+ end
290
+ end
291
+
292
+ c.desc 'List all secrets'
293
+ c.command :list do |add|
294
+ add.action do |global_options,options,args|
295
+
296
+ if options[:tags].nil?
297
+ print 'Tags for this secret, space-separated (optional): '
298
+ options[:tags] = STDIN.gets.chomp
299
+ end
300
+
301
+ tags = parse_tags(options[:tags])
302
+
303
+ # Check signatures
304
+ # List all secrets
305
+
306
+ master_key = load_master_key(options[:user], options[:private])
307
+
308
+ manifest = ManifestManager.new master_key
309
+ manifest.validate
310
+
311
+ secrets = SecretManager.new master_key
312
+ secrets.loadFile 'secrets.yaml'
313
+ all = secrets.getAll tags
314
+
315
+ puts 'All secrets: '
316
+
317
+ all.each {|tag| puts tag}
318
+ end
319
+ end
320
+
321
+ c.desc 'Reveal a secret'
322
+ c.command :show do |add|
323
+ add.action do |global_options,options,args|
324
+
325
+ if options[:name].nil?
326
+ print 'The name of the secret: '
327
+ options[:name] = STDIN.gets.chomp
328
+ raise 'A name must be specified' if options[:name].empty?
329
+ end
330
+
331
+ if options[:tags].nil?
332
+ print 'Tags for this secret, space-separated (optional): '
333
+ options[:tags] = STDIN.gets.chomp
334
+ end
335
+
336
+ tags = parse_tags(options[:tags])
337
+
338
+ # Check signatures
339
+ # Use current user's private key to get master key from lock_box
340
+ # Decrypt secret with master key
341
+
342
+ master_key = load_master_key(options[:user], options[:private])
343
+
344
+ manifest = ManifestManager.new master_key
345
+ manifest.validate
346
+
347
+ secrets = SecretManager.new master_key
348
+ secrets.loadFile 'secrets.yaml'
349
+ secret_data = secrets.find options[:name].strip, tags
350
+
351
+ secret_data.each do |key, value|
352
+ next if value.nil?
353
+ puts key.to_s + ': ' + value.inspect
354
+ end
355
+
356
+ end
357
+ end
358
+
359
+ c.desc 'Remove a secret'
360
+ c.command :rm do |add|
361
+ add.action do |global_options,options,args|
362
+
363
+ if options[:name].nil?
364
+ print 'The name of the secret: '
365
+ options[:name] = STDIN.gets.chomp
366
+ raise 'A name must be specified' if options[:name].empty?
367
+ end
368
+
369
+ if options[:tags].nil?
370
+ print 'Tags for this secret, space-separated (optional): '
371
+ options[:tags] = STDIN.gets.chomp
372
+ end
373
+
374
+ tags = parse_tags(options[:tags])
375
+
376
+ # Check signatures
377
+ # Use current user's private key to get master key from lock_box
378
+ # Remove secret from listing
379
+ # Update signatures
380
+
381
+ master_key = load_master_key(options[:user], options[:private])
382
+
383
+ manifest = ManifestManager.new master_key
384
+ manifest.validate
385
+
386
+ secrets = SecretManager.new master_key
387
+ secrets.loadFile 'secrets.yaml'
388
+ removed = secrets.remove options[:name].strip, tags
389
+
390
+ if removed == 0 then
391
+ puts 'No secrets matched criteria'
392
+ next
393
+ end
394
+
395
+ secrets.writeFile 'secrets.yaml'
396
+
397
+ manifest.update
398
+ manifest.writeFile 'manifest.yaml'
399
+
400
+ print green('Success! ')
401
+
402
+ if (removed == 1)
403
+ puts removed +' matching secret has been removed.'
404
+ else
405
+ puts removed +' matching secrets have been removed.'
406
+ end
407
+
408
+ end
409
+ end
410
+
411
+ end
412
+
413
+ on_error do |exception|
414
+ # Use GLI error handling for GLI exception
415
+ next true if exception.class.name.split("::").first == 'GLI'
416
+
417
+ $stderr.puts red(exception.message)
418
+ false # skip GLI's error handling
419
+ end
420
+
421
+ def load_master_key(user, private_key_file)
422
+ users = UserManager.new
423
+ users.loadFile 'users.yaml'
424
+ user_data = users.find user
425
+
426
+ raise "Your user account (#{user}) could not be found" if user_data.nil?
427
+
428
+ master_key = MasterKey.new MasterKey.hex_to_bin(user_data[:lock_box])
429
+ master_key.decryptWithPrivateKey File.read(private_key_file)
430
+ master_key
431
+ end
432
+
433
+ def parse_tags(tags)
434
+ return [] if tags.empty?
435
+ tags = tags.split
436
+ tags.keep_if {|tag| !tag.empty?}
437
+ tags.map(&:to_sym)
438
+ end
439
+
440
+ def green(string)
441
+ "\e[32m#{string}\e[0m"
442
+ end
443
+
444
+ def red(string)
445
+ "\e[31m#{string}\e[0m"
446
+ end
447
+
448
+ exit run(ARGV)
@@ -0,0 +1,57 @@
1
+ require 'team-secrets/file_manager'
2
+
3
+ describe FileManager do
4
+
5
+ it 'can load a file' do
6
+ fm = FileManager.new
7
+ fm.loadFile do
8
+ <<~YAML
9
+ :YAML: YAML Ain't Markup Language
10
+
11
+ :List:
12
+ - Item 1
13
+ - Item 2
14
+ - Item 3
15
+
16
+ YAML
17
+ end
18
+
19
+ correct = {
20
+ YAML: 'YAML Ain\'t Markup Language',
21
+ List: [
22
+ 'Item 1',
23
+ 'Item 2',
24
+ 'Item 3'
25
+ ]
26
+ }
27
+
28
+ expect(fm.data).to eq(correct)
29
+
30
+ end
31
+
32
+ it 'can export a file' do
33
+ fm = FileManager.new
34
+ fm.data = {
35
+ YAML: 'YAML Ain\'t Markup Language',
36
+ List: [
37
+ 'Item 1',
38
+ 'Item 2',
39
+ 'Item 3'
40
+ ]
41
+ }
42
+
43
+ correct = <<~YAML
44
+ ---
45
+ :YAML: YAML Ain't Markup Language
46
+ :List:
47
+ - Item 1
48
+ - Item 2
49
+ - Item 3
50
+ YAML
51
+
52
+ fm.writeFile do |result|
53
+ expect(result).to eq(correct)
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,70 @@
1
+ require 'team-secrets/manifest_manager'
2
+ require 'team-secrets/master_key'
3
+
4
+ describe ManifestManager do
5
+
6
+ context 'managing the manifest' do
7
+ master_key = MasterKey.generate
8
+
9
+ before(:all) do
10
+ Dir.chdir File.dirname(File.dirname(__FILE__))
11
+ Dir.mkdir 'tmp' unless File.exists? 'tmp'
12
+ Dir.chdir 'tmp'
13
+
14
+ File.delete 'manifest.yaml' if File.exists? 'manifest.yaml'
15
+
16
+ File.write('users.yaml', 'Sample 1')
17
+ File.write('secrets.yaml', 'Sample 2')
18
+ end
19
+
20
+ after(:all) do
21
+ File.delete 'manifest.yaml' if File.exists? 'manifest.yaml'
22
+ File.delete 'users.yaml' if File.exists? 'users.yaml'
23
+ File.delete 'secrets.yaml' if File.exists? 'secrets.yaml'
24
+ end
25
+
26
+ it 'can update and write the data' do
27
+
28
+ manifest = ManifestManager.new(master_key)
29
+
30
+ expect(File.exists?('manifest.yaml')).to eq(false)
31
+
32
+ manifest.update
33
+ manifest.writeFile('manifest.yaml')
34
+
35
+ expect(manifest.validate).to eq(true)
36
+ expect(File.exists?('manifest.yaml')).to eq(true)
37
+
38
+ written = File.read('manifest.yaml')
39
+ data_written = YAML.load(written)
40
+
41
+ expect(data_written[:users_file]).to be
42
+ expect(data_written[:users_file].length).to eq(2)
43
+ expect(data_written[:users_file][:path]).to eq('users.yaml')
44
+ expect(data_written[:users_file][:signature]).to match(/\A[0-9a-f]{10,}\z/)
45
+
46
+ expect(data_written[:secrets_file]).to be
47
+ expect(data_written[:secrets_file].length).to eq(2)
48
+ expect(data_written[:secrets_file][:path]).to eq('secrets.yaml')
49
+ expect(data_written[:secrets_file][:signature]).to match(/\A[0-9a-f]{10,}\z/)
50
+
51
+ end
52
+
53
+ it 'can validate the data' do
54
+
55
+ expect(File.exists?('manifest.yaml')).to eq(true)
56
+
57
+ manifest = ManifestManager.new(master_key)
58
+ manifest.loadFile('manifest.yaml')
59
+
60
+ expect(manifest.validate).to eq(true)
61
+
62
+ File.write('users.yaml', 'Sample 3')
63
+
64
+ expect { manifest.validate }.to raise_error(/signature does not match/)
65
+
66
+ end
67
+
68
+ end
69
+
70
+ end
@@ -0,0 +1,107 @@
1
+ require 'team-secrets/master_key'
2
+
3
+ describe MasterKey do
4
+
5
+ context 'doing hexidecimal conversion' do
6
+ it 'can convert a string to hexidecimal string' do
7
+ res = MasterKey.bin_to_hex('Hello!')
8
+ expect(res).to eq('48656c6c6f21')
9
+ end
10
+
11
+ it 'can convert a hexidecimal string to binary' do
12
+ res = MasterKey.hex_to_bin('576f726c6421')
13
+ expect(res).to eq('World!')
14
+ end
15
+
16
+ it 'can go to hexidecimal and back' do
17
+ res = MasterKey.bin_to_hex('Hello World!!')
18
+ back = MasterKey.hex_to_bin(res)
19
+
20
+ expect(back).to eq('Hello World!!')
21
+ end
22
+ end
23
+
24
+ context 'generating AES keys' do
25
+ it 'can generate a new AES key' do
26
+
27
+ nk = MasterKey.generate
28
+
29
+ expect(nk).to be_a_kind_of(MasterKey)
30
+ expect(nk.instance_variable_get(:@decrypted).length).to eq(MasterKey::CONFIG[:key_len])
31
+
32
+ end
33
+
34
+ it 'can generate a unique AES key' do
35
+
36
+ nk = MasterKey.generate
37
+ nk2 = MasterKey.generate
38
+
39
+ expect(nk.instance_variable_get(:@decrypted).length).to eq(MasterKey::CONFIG[:key_len])
40
+ expect(nk2.instance_variable_get(:@decrypted).length).to eq(MasterKey::CONFIG[:key_len])
41
+ expect(nk.instance_variable_get(:@decrypted)).to_not eq(nk2.instance_variable_get(:@decrypted))
42
+
43
+ end
44
+ end
45
+
46
+ context 'encryption with key pair' do
47
+ it 'can encrypt a new master key with a public key' do
48
+
49
+ mk = MasterKey.generate
50
+
51
+ pub_key = File.read File.expand_path('../support/test_key.pub.pem', __FILE__)
52
+ mk.encryptWithPublicKey(pub_key)
53
+
54
+ expect(mk.instance_variable_get(:@encrypted).length).to be > 50
55
+ expect(mk.instance_variable_get(:@decrypted)).to_not eq(mk.instance_variable_get(:@encrypted))
56
+
57
+ end
58
+
59
+ it 'can decrypt a master key with a private key' do
60
+
61
+ enc = '1f1656df3d2d4f9cd2376bc95f06d64003d0ba286699fde326df091f68ed8d2d287a4f09363c18061b124963ceeaa6136803859d9eaf296cf09011e9262efa5e3950b3cd947466115e251cb547afabb52bd0896fcf93c2796a4ce20795d2a5ea7f2eff6910cf7768f2df4a32ee6d95f8d43287e8af304be2648ebaa51f6d20572084e47b39b63a644546bf73fb28bf2610aeaaa68b9385ad90ec0aaf528019ca2e41553b3f2d722d928f930a54128d74c2a6871de3af9e09ff5e26c51a0740c289b49072c424e978e97b86e983792d54c58eb1a9e9821524d443ec6d01589c46260e09a77e6138ade975c75c0d8ec82480fe19514eab861e56dc1b2f756caef7'
62
+
63
+ mk = MasterKey.new(MasterKey.hex_to_bin(enc), true)
64
+
65
+ priv_key = File.read File.expand_path('../support/test_key', __FILE__)
66
+ mk.decryptWithPrivateKey(priv_key, '12345')
67
+
68
+ expect(mk.instance_variable_get(:@decrypted).length).to be(MasterKey::CONFIG[:key_len])
69
+ expect(mk.instance_variable_get(:@decrypted)).to_not eq(mk.instance_variable_get(:@encrypted))
70
+
71
+ end
72
+ end
73
+
74
+ context 'encryption functionality' do
75
+ it 'can encrypt a string' do
76
+
77
+ mk = MasterKey.new('1234'*8, false)
78
+ res = mk.encryptSecret('My Secret')
79
+
80
+ expect(res).to_not eq('My Secret')
81
+
82
+ end
83
+
84
+ it 'can not decrypt a string with the wrong key' do
85
+
86
+ mk = MasterKey.new('1234'*8, false)
87
+ res = mk.encryptSecret('My Secret 2')
88
+
89
+ mk2 = MasterKey.new('0000'*8, false)
90
+
91
+ expect { back = mk2.decryptSecret(res) }.to raise_error(/bad decrypt/)
92
+
93
+ end
94
+
95
+ it 'can encrypt and decrypt a string' do
96
+
97
+ mk = MasterKey.new('1234'*8, false)
98
+ res = mk.encryptSecret('My Secret 3')
99
+
100
+ mk2 = MasterKey.new('1234'*8, false)
101
+ back = mk2.decryptSecret(res)
102
+
103
+ expect(back).to eq('My Secret 3')
104
+
105
+ end
106
+ end
107
+ end