seekrit 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
|