seekrit 0.2.1

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/bin/seekrit ADDED
@@ -0,0 +1,53 @@
1
+ #! /usr/bin/ruby1.8
2
+ require 'seekrit/store'
3
+ require 'seekrit/interactor'
4
+ require 'highline/import'
5
+ require 'fileutils'
6
+
7
+ DATAFILE = ENV['HOME'] + '/.config/seekrit/secrets'
8
+
9
+ def interactor(&blk)
10
+ err = nil
11
+ FileUtils.mkdir_p(File.dirname(DATAFILE))
12
+ File.open(DATAFILE, "a+") do |file|
13
+ 3.times do
14
+ begin
15
+ password = ask('Enter password: '){ |q| q.echo = false }
16
+ store = Seekrit::Store.new(password, file)
17
+ yield Seekrit::Interactor.new(store)
18
+ return
19
+ rescue Seekrit::DecryptionError => err
20
+ end
21
+ end
22
+ puts err.message
23
+ exit 1
24
+ end
25
+ end
26
+
27
+ command = ARGV.shift
28
+ names = ARGV
29
+ case command
30
+ when 'show', 's'
31
+ interactor{ |i| i.show names }
32
+ when 'edit', 'e'
33
+ interactor{ |i| i.edit names }
34
+ when 'list', 'l'
35
+ interactor{ |i| i.list names }
36
+ when 'delete'
37
+ interactor{ |i| i.delete names }
38
+ when 'rename'
39
+ interactor{ |i| i.rename names.shift, names.shift }
40
+ when 'export'
41
+ interactor{ |i| i.export names.shift }
42
+ when 'import'
43
+ interactor{ |i| i.import names.shift }
44
+ else
45
+ puts "Unknown command '#{command}'", '' if command
46
+ puts <<END
47
+ list List all entries.
48
+ show name(s) Show matching entries.
49
+ edit name(s) Create or modify entries.
50
+ delete name(s) Delete entries.
51
+ rename old_name new_name Rename entry.
52
+ END
53
+ end
@@ -0,0 +1,106 @@
1
+ require 'tempfile'
2
+
3
+ module Seekrit
4
+ class Interactor
5
+ EDITOR = ENV['EDITOR'] || 'vi'
6
+ RANDOM_SOURCE = '/dev/urandom'
7
+
8
+ attr_reader :store
9
+
10
+ def initialize(store)
11
+ @store = store
12
+ end
13
+
14
+ def show(names)
15
+ names.each do |name|
16
+ data = store[name]
17
+ puts(name, data, '')
18
+ end
19
+ end
20
+
21
+ def edit(names)
22
+ names.each do |name|
23
+ comment = "\# #{name}\n"
24
+ data = comment + (store[name] || '')
25
+ external_editor(data) do |data|
26
+ data.sub!(/\A#{Regexp.escape(comment)}/, '')
27
+ store[name] = data
28
+ store.save
29
+ end
30
+ end
31
+ end
32
+
33
+ def delete(names)
34
+ names.each do |name|
35
+ store.delete(name)
36
+ end
37
+ store.save
38
+ end
39
+
40
+ def list(patterns)
41
+ if patterns.empty?
42
+ regexp = /.*/
43
+ else
44
+ regexp = /#{ patterns.map{ |p| Regexp.escape(p) }.join('|') }/
45
+ end
46
+ store.keys.sort_by{ |a| a.upcase }.each do |name|
47
+ puts name if name =~ regexp
48
+ end
49
+ end
50
+
51
+ def rename(old_name, new_name)
52
+ store.rename(old_name, new_name)
53
+ store.save
54
+ end
55
+
56
+ def export(filename)
57
+ File.open(filename, 'w') do |io|
58
+ store.export io
59
+ end
60
+ end
61
+
62
+ def import(filename)
63
+ File.open(filename, 'r') do |io|
64
+ store.import io
65
+ store.save
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def shred(filename, cycles=1)
72
+ data_length = File.stat(filename).size
73
+ File.open(filename, 'wb') do |io|
74
+ cycles.times do
75
+ io.rewind
76
+ io << random_bytes(data_length)
77
+ io.flush
78
+ end
79
+ end
80
+ end
81
+
82
+ def random_bytes(num_bytes)
83
+ File.open(RANDOM_SOURCE){ |io| io.read(num_bytes) }
84
+ end
85
+
86
+ def puts(*args)
87
+ $stderr.puts(*args)
88
+ end
89
+
90
+ def external_editor(data, &blk)
91
+ tempfile = Tempfile.new('seekrit')
92
+ tempfile << data
93
+ tempfile.close
94
+ mtime = File.stat(tempfile.path).mtime
95
+ system( EDITOR + ' ' + tempfile.path )
96
+ if File.stat(tempfile.path).mtime == mtime
97
+ puts('No modifications.')
98
+ else
99
+ yield File.read(tempfile.path)
100
+ end
101
+ shred(tempfile.path)
102
+ end
103
+
104
+ end
105
+ end
106
+
@@ -0,0 +1,124 @@
1
+ require 'digest/sha2'
2
+ require 'openssl'
3
+ require 'yaml'
4
+
5
+ module Seekrit
6
+ class DecryptionError < RuntimeError
7
+ end
8
+
9
+ class Store
10
+ CIPHER = 'aes-256-cbc'
11
+
12
+ attr_reader :secrets
13
+
14
+ def initialize(password, file, cipher=CIPHER)
15
+ @password = password
16
+ @cipher = cipher
17
+ @file = file
18
+ @secrets = load_data(file)
19
+ end
20
+
21
+ def keys
22
+ secrets.keys
23
+ end
24
+
25
+ def [](name)
26
+ secrets[name]
27
+ end
28
+
29
+ def []=(name, value)
30
+ secrets[name] = value
31
+ end
32
+
33
+ def delete(name)
34
+ secrets.delete(name)
35
+ end
36
+
37
+ def rename(oldname, newname)
38
+ self[newname] = self[oldname]
39
+ delete(oldname)
40
+ end
41
+
42
+ def save
43
+ @file.rewind
44
+ secrets.sort_by{ |k,_| k }.each do |name, value|
45
+ @file << escape(name) << "\t" << hexdump(encrypt(value)) << "\n"
46
+ end
47
+ end
48
+
49
+ def export(io)
50
+ secrets.sort_by{ |k,_| k }.each do |name, value|
51
+ io << escape(name) << "\t" << escape(value) << "\n"
52
+ end
53
+ end
54
+
55
+ def import(io)
56
+ secrets.clear
57
+ while line = io.gets
58
+ name, data = line.chomp.split(/\t/, 2).map{ |a| unescape(a) }
59
+ self[name] = data
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ def crypt_key
66
+ Digest::SHA256.digest(@password)
67
+ end
68
+
69
+ def escape(value)
70
+ value.
71
+ gsub(/\\/, "\\\\\\\\").
72
+ gsub(/\t/, "\\\\t").
73
+ gsub(/\n/, "\\\\n")
74
+ end
75
+
76
+ def unescape(value)
77
+ value.
78
+ gsub(/\\n/, "\n").
79
+ gsub(/\\t/, "\t").
80
+ gsub(/\\\\/, "\\\\")
81
+ end
82
+
83
+ def load_data(file)
84
+ data = {}
85
+ while line = file.gets
86
+ a, b = line.split(/\t/, 2)
87
+ data[unescape(a)] = decrypt(hexload(b))
88
+ end
89
+ data
90
+ end
91
+
92
+ def hexdump(binary)
93
+ binary.unpack('C*').map{ |a| "%02x" % a }.join
94
+ end
95
+
96
+ def hexload(hex)
97
+ hex.scan(/../).map{ |a| a.to_i(16) }.pack('C*')
98
+ end
99
+
100
+ def encrypt(data)
101
+ cipher = OpenSSL::Cipher::Cipher.new(@cipher)
102
+ cipher.encrypt
103
+ cipher.key = crypt_key
104
+ cipher.iv = iv = cipher.random_iv
105
+ ciphertext = cipher.update(data)
106
+ ciphertext << cipher.final
107
+ return iv + ciphertext
108
+ end
109
+
110
+ def decrypt(data)
111
+ cipher = OpenSSL::Cipher::Cipher.new(@cipher)
112
+ iv = data[0, cipher.iv_len]
113
+ ciphertext = data[cipher.iv_len..-1]
114
+ cipher.decrypt
115
+ cipher.key = crypt_key
116
+ cipher.iv = iv
117
+ plaintext = cipher.update(ciphertext)
118
+ plaintext << cipher.final
119
+ return plaintext
120
+ rescue => err
121
+ raise DecryptionError, err.message
122
+ end
123
+ end
124
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: seekrit
3
+ version: !ruby/object:Gem::Version
4
+ hash: 21
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 1
10
+ version: 0.2.1
11
+ platform: ruby
12
+ authors:
13
+ - Paul Battley
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-01-16 00:00:00 +00:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: highline
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ description: A password safe
36
+ email:
37
+ - pbattley@gmail.com
38
+ executables:
39
+ - seekrit
40
+ extensions: []
41
+
42
+ extra_rdoc_files: []
43
+
44
+ files:
45
+ - bin/seekrit
46
+ - lib/seekrit/store.rb
47
+ - lib/seekrit/interactor.rb
48
+ has_rdoc: true
49
+ homepage: http://github.com/threedaymonk/seekrit
50
+ licenses: []
51
+
52
+ post_install_message:
53
+ rdoc_options: []
54
+
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ hash: 3
63
+ segments:
64
+ - 0
65
+ version: "0"
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ requirements: []
76
+
77
+ rubyforge_project:
78
+ rubygems_version: 1.3.7
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: Password safe
82
+ test_files: []
83
+