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 +53 -0
- data/lib/seekrit/interactor.rb +106 -0
- data/lib/seekrit/store.rb +124 -0
- metadata +83 -0
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
|
+
|