trustworthy 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.
- data/README.md +35 -0
- data/bin/trustworthy +145 -0
- data/lib/trustworthy/crypto.rb +50 -0
- data/lib/trustworthy/key.rb +16 -0
- data/lib/trustworthy/master_key.rb +48 -0
- data/lib/trustworthy/random.rb +21 -0
- data/lib/trustworthy/settings.rb +63 -0
- data/lib/trustworthy/version.rb +3 -0
- data/lib/trustworthy.rb +13 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/trustworthy/crypto_spec.rb +42 -0
- data/spec/trustworthy/key_spec.rb +11 -0
- data/spec/trustworthy/master_key_spec.rb +87 -0
- data/spec/trustworthy/random_spec.rb +28 -0
- data/spec/trustworthy/settings_spec.rb +82 -0
- metadata +181 -0
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
## Trustworthy [](http://travis-ci.org/jtdowney/trustworthy)
|
2
|
+
|
3
|
+
Implements a special case (k = 2) of [Adi Shamir's](http://en.wikipedia.org/wiki/Adi_Shamir) [secret sharing algorithm](http://en.wikipedia.org/wiki/Shamir%27s_Secret_Sharing). This allows secret files to be encrypted on disk but loaded into a process on start if two of the keys are available.
|
4
|
+
|
5
|
+
### Uses
|
6
|
+
|
7
|
+
#### Generate a new master key
|
8
|
+
|
9
|
+
trustworthy init -c trustworthy.yml
|
10
|
+
|
11
|
+
This will create a new master key and ask for two users to be added who can decrypt the master key. The information about users and secrets is stored in trustworthy.yml.
|
12
|
+
|
13
|
+
#### Add an additional user to the master key
|
14
|
+
|
15
|
+
trustworthy add-user -c trustworthy.yml
|
16
|
+
|
17
|
+
The master key will be loaded so that a new user can be added to trustworthy.yml.
|
18
|
+
|
19
|
+
#### Add a secret
|
20
|
+
|
21
|
+
trustworthy add-secret -c trustworthy.yml -e ENCRYPTION_KEY -i encryption.key -o encryption.key.tw
|
22
|
+
|
23
|
+
Secrets are the main purpose behind trustworthy. This will encrypt the file using the master key and will load it into the given environment variable when executed.
|
24
|
+
|
25
|
+
#### Run a program under trustworthy
|
26
|
+
|
27
|
+
trustworthy exec -c trustworthy.yml rails console
|
28
|
+
|
29
|
+
The master key will be loaded so all secrets can be decrypted and stored in the environment. After that the given program will be spawned and inherit the environment.
|
30
|
+
|
31
|
+
### Reference
|
32
|
+
|
33
|
+
RSA Labs - [http://www.rsa.com/rsalabs/node.asp?id=2259](http://www.rsa.com/rsalabs/node.asp?id=2259)
|
34
|
+
ssss - [http://point-at-infinity.org/ssss/](http://point-at-infinity.org/ssss/)
|
35
|
+
Secret sharing on Wikipedia - [http://en.wikipedia.org/wiki/Secret_sharing](http://en.wikipedia.org/wiki/Secret_sharing)
|
data/bin/trustworthy
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'commander/import'
|
5
|
+
require 'trustworthy'
|
6
|
+
|
7
|
+
program :version, Trustworthy::VERSION
|
8
|
+
program :description, 'Trustworthy secret sharing'
|
9
|
+
|
10
|
+
command 'init' do |c|
|
11
|
+
c.syntax = 'trustworthy init [options]'
|
12
|
+
c.summary = 'Create a new master key'
|
13
|
+
c.description = 'Used to generate a setup a new master key for trustworthy'
|
14
|
+
c.example 'create a new master key named foo', 'trustworthy init -c foo.yml'
|
15
|
+
c.option '-k', '--keys NUMBER', Integer, 'Initial number of keys to create (must be 2 or more, defaults to 2)'
|
16
|
+
c.option '-c', '--config FILE', String, 'Configuration file'
|
17
|
+
c.action do |args, options|
|
18
|
+
options.default :keys => 2
|
19
|
+
|
20
|
+
raise Commander::Runner::InvalidCommandError.new('Must provide a config file') unless options.config
|
21
|
+
raise Commander::Runner::InvalidCommandError.new('Cannot generate fewer than two sub-keys') if options.keys < 2
|
22
|
+
|
23
|
+
say "Creating a new master key with #{options.keys} sub-keys."
|
24
|
+
|
25
|
+
master_key = Trustworthy::MasterKey.create
|
26
|
+
settings = Trustworthy::Settings.new
|
27
|
+
|
28
|
+
options.keys.times do
|
29
|
+
add_user_key(settings, master_key)
|
30
|
+
end
|
31
|
+
|
32
|
+
settings.write(options.config)
|
33
|
+
say "Created #{options.config}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
command 'add-user' do |c|
|
38
|
+
c.syntax = 'trustworthy add-user [options]'
|
39
|
+
c.summary = 'Add a new user sub-key'
|
40
|
+
c.description = 'Load the master key and create a new user sub-key for it.'
|
41
|
+
c.example 'add a new user the master key', 'trustworthy add-key -c foo.yml'
|
42
|
+
c.option '-k', '--keys NUMBER', Integer, 'Number of keys to create'
|
43
|
+
c.option '-c', '--config FILE', String, 'Configuration file'
|
44
|
+
c.action do |args, options|
|
45
|
+
options.default :keys => 1
|
46
|
+
|
47
|
+
raise Commander::Runner::InvalidCommandError.new('Must provide a config file') unless options.config
|
48
|
+
raise Commander::Runner::InvalidCommandError.new('Cannot generate fewer than one key') if options.keys < 1
|
49
|
+
|
50
|
+
settings = Trustworthy::Settings.load(options.config)
|
51
|
+
master_key = load_master_key(settings)
|
52
|
+
|
53
|
+
options.keys.times do
|
54
|
+
add_user_key(settings, master_key)
|
55
|
+
end
|
56
|
+
|
57
|
+
settings.write(options.config)
|
58
|
+
say "Updated #{options.config} with #{options.keys} new key(s)"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
command 'add-secret' do |c|
|
63
|
+
c.syntax = 'trustworthy add-secret [options]'
|
64
|
+
c.summary = 'Add a new secret'
|
65
|
+
c.description = 'Loads the master key and uses it to encrypt a new secret.'
|
66
|
+
c.example 'add a new secret with master key', 'trustworthy add-secret -c foo.yml -e ENCRYPTION_KEY -i encryption.key -o encryption.key.tw'
|
67
|
+
c.option '-e', '--environment ENVIRONMENT', 'Environment variable to store secret in when running exec'
|
68
|
+
c.option '-i', '--input FILE', 'File to be encrypted'
|
69
|
+
c.option '-o', '--output FILE', 'Encrypted output file'
|
70
|
+
c.option '-c', '--config FILE', String, 'Configuration file'
|
71
|
+
c.action do |args, options|
|
72
|
+
raise Commander::Runner::InvalidCommandError.new('Must provide a config file') unless options.config
|
73
|
+
raise Commander::Runner::InvalidCommandError.new('Must provide an environment variable') unless options.environment
|
74
|
+
raise Commander::Runner::InvalidCommandError.new('Must provide an input file') unless options.input
|
75
|
+
raise Commander::Runner::InvalidCommandError.new('Must provide an output file') unless options.output
|
76
|
+
|
77
|
+
settings = Trustworthy::Settings.load(options.config)
|
78
|
+
master_key = load_master_key(settings)
|
79
|
+
|
80
|
+
plaintext = File.read(options.input)
|
81
|
+
ciphertext = master_key.encrypt(plaintext)
|
82
|
+
|
83
|
+
File.open(options.output, 'w') do |file|
|
84
|
+
file.write(ciphertext)
|
85
|
+
end
|
86
|
+
|
87
|
+
settings.add_secret(options.environment, options.output)
|
88
|
+
settings.write(options.config)
|
89
|
+
say "Updated #{options.config} with the #{options.environment} secret"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
command 'exec' do |c|
|
94
|
+
c.syntax = 'trustworthy exec -c [config file] program'
|
95
|
+
c.summary = 'Load secrets and run program'
|
96
|
+
c.description = 'Loads and decrypted all secrets with the master key and stores them in the environment before forking program'
|
97
|
+
c.example 'run rails console', 'trustworthy -c foo.yml rails console'
|
98
|
+
c.option '-c', '--config FILE', String, 'Configuration file'
|
99
|
+
c.action do |args, options|
|
100
|
+
raise Commander::Runner::InvalidCommandError.new('Must provide a config file') unless options.config
|
101
|
+
|
102
|
+
settings = Trustworthy::Settings.load(options.config)
|
103
|
+
master_key = load_master_key(settings)
|
104
|
+
|
105
|
+
environment = {}
|
106
|
+
settings.secrets.each do |key, encrypted_file|
|
107
|
+
ciphertext = File.read(encrypted_file)
|
108
|
+
environment[key] = master_key.decrypt(ciphertext)
|
109
|
+
end
|
110
|
+
|
111
|
+
pid = POSIX::Spawn.spawn(environment, *args)
|
112
|
+
Process.waitpid(pid)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def load_master_key(settings)
|
117
|
+
keys = []
|
118
|
+
2.times do
|
119
|
+
username = ask('Username: ')
|
120
|
+
password = password('Password: ', '')
|
121
|
+
keys << settings.unlock_key(username, password)
|
122
|
+
say "Unlocked key for #{username}."
|
123
|
+
end
|
124
|
+
|
125
|
+
Trustworthy::MasterKey.create_from_keys(*keys)
|
126
|
+
end
|
127
|
+
|
128
|
+
def add_user_key(settings, master_key)
|
129
|
+
key = master_key.create_key
|
130
|
+
username = ask('Username: ').to_s
|
131
|
+
|
132
|
+
password = nil
|
133
|
+
loop do
|
134
|
+
password = password('Password: ', '')
|
135
|
+
password_confirm = password('Password (again): ', '')
|
136
|
+
if password == password_confirm
|
137
|
+
break
|
138
|
+
else
|
139
|
+
say_error 'Password mismatch'
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
settings.add_key(key, username, password)
|
144
|
+
say "User #{username} added."
|
145
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module Trustworthy
|
2
|
+
class Crypto
|
3
|
+
def initialize(encryption_key, authentication_key)
|
4
|
+
@encryption_key = encryption_key
|
5
|
+
@authentication_key = authentication_key
|
6
|
+
@digest = OpenSSL::Digest.new('SHA256')
|
7
|
+
@cipher = OpenSSL::Cipher.new('AES-256-CBC')
|
8
|
+
end
|
9
|
+
|
10
|
+
def decrypt(ciphertext)
|
11
|
+
ciphertext.force_encoding('BINARY') if ciphertext.respond_to?(:force_encoding)
|
12
|
+
iv = ciphertext.slice(0..15)
|
13
|
+
ciphertext = ciphertext.slice(16..-1)
|
14
|
+
@cipher.decrypt
|
15
|
+
@cipher.key = @encryption_key
|
16
|
+
@cipher.iv = iv
|
17
|
+
@cipher.update(ciphertext) + @cipher.final
|
18
|
+
end
|
19
|
+
|
20
|
+
def encrypt(plaintext)
|
21
|
+
iv = Trustworthy::Random.bytes(16)
|
22
|
+
@cipher.encrypt
|
23
|
+
@cipher.key = @encryption_key
|
24
|
+
@cipher.iv = iv
|
25
|
+
ciphertext = @cipher.update(plaintext) + @cipher.final
|
26
|
+
iv + ciphertext
|
27
|
+
end
|
28
|
+
|
29
|
+
def sign(data)
|
30
|
+
OpenSSL::HMAC.digest(@digest, @authentication_key, data)
|
31
|
+
end
|
32
|
+
|
33
|
+
def valid_signature?(given_signature, data)
|
34
|
+
computed_signature = sign(data)
|
35
|
+
_secure_compare(given_signature, computed_signature)
|
36
|
+
end
|
37
|
+
|
38
|
+
def _secure_compare(a, b)
|
39
|
+
return false unless a.bytesize == b.bytesize
|
40
|
+
|
41
|
+
bytes = a.unpack("C#{a.bytesize}")
|
42
|
+
|
43
|
+
result = 0
|
44
|
+
b.each_byte do |byte|
|
45
|
+
result |= byte ^ bytes.shift
|
46
|
+
end
|
47
|
+
result == 0
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Trustworthy
|
2
|
+
class MasterKey
|
3
|
+
def self.create
|
4
|
+
slope = Trustworthy::Random.number
|
5
|
+
intercept = Trustworthy::Random.number
|
6
|
+
new(slope, intercept)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.create_from_keys(key1, key2)
|
10
|
+
slope = (key2.y - key1.y) / (key2.x - key1.x)
|
11
|
+
intercept = key1.y - (slope * key1.x)
|
12
|
+
new(slope, intercept)
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(slope, intercept)
|
16
|
+
@slope = slope
|
17
|
+
@intercept = intercept
|
18
|
+
end
|
19
|
+
|
20
|
+
def create_key
|
21
|
+
Trustworthy::Key.create(@slope, @intercept)
|
22
|
+
end
|
23
|
+
|
24
|
+
def encrypt(plaintext)
|
25
|
+
ciphertext = _crypto.encrypt(plaintext)
|
26
|
+
signature = _crypto.sign(ciphertext)
|
27
|
+
signature + ciphertext
|
28
|
+
end
|
29
|
+
|
30
|
+
def decrypt(ciphertext)
|
31
|
+
ciphertext.force_encoding('BINARY') if ciphertext.respond_to?(:force_encoding)
|
32
|
+
signature = ciphertext.slice(0..31)
|
33
|
+
ciphertext = ciphertext.slice(32..-1)
|
34
|
+
raise 'invalid signature' unless _crypto.valid_signature?(signature, ciphertext)
|
35
|
+
_crypto.decrypt(ciphertext)
|
36
|
+
end
|
37
|
+
|
38
|
+
def _crypto
|
39
|
+
return @crypto if @crypto
|
40
|
+
|
41
|
+
secret = @intercept.to_s('F')
|
42
|
+
hkdf = HKDF.new(secret)
|
43
|
+
encryption_key = hkdf.next_bytes(32)
|
44
|
+
authentication_key = hkdf.next_bytes(32)
|
45
|
+
@crypto = Trustworthy::Crypto.new(encryption_key, authentication_key)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Trustworthy
|
2
|
+
module Random
|
3
|
+
def self.number(size = 32)
|
4
|
+
raw_bytes = bytes(size)
|
5
|
+
number = raw_bytes.unpack('H*').first.hex
|
6
|
+
BigDecimal.new(number.to_s)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.bytes(size = 32)
|
10
|
+
flags = File::RDONLY
|
11
|
+
flags |= File::NOCTTY if defined? File::NOCTTY
|
12
|
+
File.open(_source, flags) do |file|
|
13
|
+
file.read(size)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def self._source
|
18
|
+
'/dev/random'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Trustworthy
|
2
|
+
class Settings
|
3
|
+
attr_reader :keys, :secrets
|
4
|
+
|
5
|
+
def self.load(filename)
|
6
|
+
data = File.read(filename)
|
7
|
+
yaml = YAML.load(data)
|
8
|
+
new(yaml['keys'], yaml['secrets'])
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(keys = {}, secrets = {})
|
12
|
+
@keys = keys
|
13
|
+
@secrets = secrets
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_key(key, username, password)
|
17
|
+
salt = SCrypt::Engine.generate_salt
|
18
|
+
|
19
|
+
crypto = _crypto_from_password(salt, password)
|
20
|
+
plaintext = "#{key.x.to_s('F')},#{key.y.to_s('F')}"
|
21
|
+
ciphertext = crypto.encrypt(plaintext)
|
22
|
+
|
23
|
+
@keys[username] = {
|
24
|
+
'salt' => salt,
|
25
|
+
'ciphertext' => ciphertext,
|
26
|
+
'authentication' => crypto.sign(ciphertext)
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_secret(environment, filename)
|
31
|
+
@secrets[environment] = filename
|
32
|
+
end
|
33
|
+
|
34
|
+
def unlock_key(username, password)
|
35
|
+
key_data = keys[username]
|
36
|
+
ciphertext = key_data['ciphertext']
|
37
|
+
signature = key_data['authentication']
|
38
|
+
salt = key_data['salt']
|
39
|
+
crypto = _crypto_from_password(salt, password)
|
40
|
+
raise 'invalid signature' unless crypto.valid_signature?(signature, ciphertext)
|
41
|
+
plaintext = crypto.decrypt(ciphertext)
|
42
|
+
x, y = plaintext.split(',').map { |n| BigDecimal.new(n) }
|
43
|
+
Trustworthy::Key.new(x, y)
|
44
|
+
end
|
45
|
+
|
46
|
+
def write(filename)
|
47
|
+
File.open(filename, 'w') do |file|
|
48
|
+
data = YAML.dump('keys' => @keys, 'secrets' => @secrets)
|
49
|
+
file.write(data)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def _crypto_from_password(salt, password)
|
54
|
+
cost, salt = salt.rpartition('$')
|
55
|
+
raw_key = SCrypt::Engine.scrypt(password, salt, cost, 64)
|
56
|
+
|
57
|
+
encryption_key = raw_key.slice(0, 32)
|
58
|
+
authentication_key = raw_key.slice(32, 32)
|
59
|
+
|
60
|
+
Trustworthy::Crypto.new(encryption_key, authentication_key)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/trustworthy.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'hkdf'
|
3
|
+
require 'openssl'
|
4
|
+
require 'posix/spawn'
|
5
|
+
require 'scrypt'
|
6
|
+
require 'securerandom'
|
7
|
+
require 'trustworthy/crypto'
|
8
|
+
require 'trustworthy/key'
|
9
|
+
require 'trustworthy/master_key'
|
10
|
+
require 'trustworthy/random'
|
11
|
+
require 'trustworthy/settings'
|
12
|
+
require 'trustworthy/version'
|
13
|
+
require 'yaml'
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'fakefs/safe'
|
2
|
+
|
3
|
+
require 'trustworthy'
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
config.order = 'random'
|
7
|
+
config.before(:each) do
|
8
|
+
Trustworthy::Random.stub(:_source).and_return('/dev/urandom')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module Trustworthy
|
13
|
+
module TestValues
|
14
|
+
EncryptionKey = ['7fc6880ba7a75148e6b14a6ebca73b9861954835fd17237cbedb71a6ad3fefc4'].pack('H*')
|
15
|
+
AuthenticationKey = ['12e7489dc1b2e6da1213cac0df946615a7088eeccdd6b2d4bf330e9560fabd52'].pack('H*')
|
16
|
+
InitializationVector = ['39164ec082fb8b7336d3c5500af99dcb'].pack('H*')
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Trustworthy::Crypto do
|
4
|
+
before(:each) do
|
5
|
+
@crypto = Trustworthy::Crypto.new(Trustworthy::TestValues::EncryptionKey, Trustworthy::TestValues::AuthenticationKey)
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'sign' do
|
9
|
+
it 'should sign the input using the authentication key' do
|
10
|
+
signature = @crypto.sign('foobar')
|
11
|
+
signature.should == ['f9e948d9259d35e729b14e370a7456ec4b22d2a0a1d5daa1accbeb54f414865e'].pack('H*')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe 'valid_signature?' do
|
16
|
+
it 'should return true when the signature matches the data' do
|
17
|
+
given_signature = ['f9e948d9259d35e729b14e370a7456ec4b22d2a0a1d5daa1accbeb54f414865e'].pack('H*')
|
18
|
+
@crypto.should be_valid_signature(given_signature, 'foobar')
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should return false when the signature matches the data' do
|
22
|
+
given_signature = ['4d15a6427d6b56e07b819ff0a147dd8ff77b1b5fafd33f9071b0359755edde42'].pack('H*')
|
23
|
+
@crypto.should_not be_valid_signature(given_signature, 'foobar')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'encrypt' do
|
28
|
+
it 'should encrypt the input using the encryption key' do
|
29
|
+
Trustworthy::Random.stub(:bytes).and_return(Trustworthy::TestValues::InitializationVector)
|
30
|
+
ciphertext = @crypto.encrypt('foobar')
|
31
|
+
ciphertext.should == ['39164ec082fb8b7336d3c5500af99dcbf3af2b0abd6f2ac79f1a76cb4e092a7c'].pack('H*')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe 'decrypt' do
|
36
|
+
it 'should decrypt the input using the encryption key' do
|
37
|
+
ciphertext = ['39164ec082fb8b7336d3c5500af99dcbf3af2b0abd6f2ac79f1a76cb4e092a7c'].pack('H*')
|
38
|
+
plaintext = @crypto.decrypt(ciphertext)
|
39
|
+
plaintext.should == 'foobar'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Trustworthy::Key do
|
4
|
+
describe 'self.create' do
|
5
|
+
it 'should create a key along the slope and intercept' do
|
6
|
+
Trustworthy::Random.stub(:number).and_return(BigDecimal.new('10'))
|
7
|
+
key = Trustworthy::Key.create(6, 24)
|
8
|
+
key.y.should == 84
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Trustworthy::MasterKey do
|
4
|
+
it 'should encrypt and decrypt a master_key' do
|
5
|
+
master_key = Trustworthy::MasterKey.create
|
6
|
+
key1 = master_key.create_key
|
7
|
+
key2 = master_key.create_key
|
8
|
+
|
9
|
+
ciphertext = master_key.encrypt('foobar')
|
10
|
+
|
11
|
+
new_master_key = Trustworthy::MasterKey.create_from_keys(key1, key2)
|
12
|
+
plaintext = new_master_key.decrypt(ciphertext)
|
13
|
+
plaintext.should == 'foobar'
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should function with any 2 of n keys' do
|
17
|
+
master_key1 = Trustworthy::MasterKey.create
|
18
|
+
key1 = master_key1.create_key
|
19
|
+
key2 = master_key1.create_key
|
20
|
+
|
21
|
+
ciphertext = master_key1.encrypt('foobar')
|
22
|
+
|
23
|
+
master_key2 = Trustworthy::MasterKey.create_from_keys(key1, key2)
|
24
|
+
key3 = master_key2.create_key
|
25
|
+
|
26
|
+
master_key3 = Trustworthy::MasterKey.create_from_keys(key1, key3)
|
27
|
+
plaintext = master_key3.decrypt(ciphertext)
|
28
|
+
plaintext.should == 'foobar'
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'self.create' do
|
32
|
+
it 'should generate a random slope and intercept' do
|
33
|
+
Trustworthy::Random.stub(:number).and_return(BigDecimal.new('10'))
|
34
|
+
master_key = Trustworthy::MasterKey.create
|
35
|
+
key = master_key.create_key
|
36
|
+
key.x.should == 10
|
37
|
+
key.y.should == 110
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'self.create_from_keys' do
|
42
|
+
it 'should calculate the slope and intercept given two keys' do
|
43
|
+
Trustworthy::Random.stub(:number).and_return(BigDecimal.new('10'))
|
44
|
+
|
45
|
+
key1 = Trustworthy::Key.new(BigDecimal.new('2'), BigDecimal.new('30'))
|
46
|
+
key2 = Trustworthy::Key.new(BigDecimal.new('5'), BigDecimal.new('60'))
|
47
|
+
|
48
|
+
master_key = Trustworthy::MasterKey.create_from_keys(key1, key2)
|
49
|
+
new_key = master_key.create_key
|
50
|
+
new_key.x.should == 10
|
51
|
+
new_key.y.should == 110
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe 'create_key' do
|
56
|
+
it 'should define a new key' do
|
57
|
+
master_key = Trustworthy::MasterKey.new(BigDecimal.new('6'), BigDecimal.new('24'))
|
58
|
+
key = master_key.create_key
|
59
|
+
key.x.should_not == 0
|
60
|
+
key.y.should_not == 0
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
describe 'encrypt' do
|
65
|
+
it 'should encrypt and sign the data using the intercept' do
|
66
|
+
Trustworthy::Random.stub(:bytes).and_return(Trustworthy::TestValues::InitializationVector)
|
67
|
+
master_key = Trustworthy::MasterKey.new(BigDecimal.new('6'), BigDecimal.new('24'))
|
68
|
+
ciphertext = master_key.encrypt('foobar')
|
69
|
+
ciphertext.should == ['0438a89ef9e5792849ef611bff0e7b405a84ac515461489499679ca77ade6a6a39164ec082fb8b7336d3c5500af99dcbde056af859baa7e72c4c2661651e88d5'].pack('H*')
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe 'decrypt' do
|
74
|
+
it 'should decrypt and verify the data using the intercept' do
|
75
|
+
master_key = Trustworthy::MasterKey.new(BigDecimal.new('6'), BigDecimal.new('24'))
|
76
|
+
ciphertext = ['0438a89ef9e5792849ef611bff0e7b405a84ac515461489499679ca77ade6a6a39164ec082fb8b7336d3c5500af99dcbde056af859baa7e72c4c2661651e88d5'].pack('H*')
|
77
|
+
plaintext = master_key.decrypt(ciphertext)
|
78
|
+
plaintext.should == 'foobar'
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should raise an invalid signature error if signatures do not match' do
|
82
|
+
master_key = Trustworthy::MasterKey.new(BigDecimal.new('6'), BigDecimal.new('24'))
|
83
|
+
ciphertext = ['00000000000000000000000000000000000000005461489499679ca77ade6a6a39164ec082fb8b7336d3c5500af99dcbde056af859baa7e72c4c2661651e88d5'].pack('H*')
|
84
|
+
expect { master_key.decrypt(ciphertext) }.to raise_error('invalid signature')
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Trustworthy::Random do
|
4
|
+
describe 'self.number' do
|
5
|
+
it 'should return a random number' do
|
6
|
+
random = Trustworthy::Random.number
|
7
|
+
random.should_not == Trustworthy::Random.number
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'should return a number up to a given byte size' do
|
11
|
+
random = Trustworthy::Random.number(2)
|
12
|
+
random.should <= 2 ** 16
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'self.bytes' do
|
17
|
+
it 'should return a string of the given length' do
|
18
|
+
bytes = Trustworthy::Random.bytes(16)
|
19
|
+
bytes.size.should == 16
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'should return a random string' do
|
23
|
+
bytes1 = Trustworthy::Random.bytes(16)
|
24
|
+
bytes2 = Trustworthy::Random.bytes(16)
|
25
|
+
bytes1.should_not == bytes2
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Trustworthy::Settings do
|
4
|
+
before(:each) do
|
5
|
+
SCrypt::Engine.stub(:generate_salt).and_return('400$8$1b$3e31f076a3226825')
|
6
|
+
Trustworthy::Random.stub(:bytes).and_return(Trustworthy::TestValues::InitializationVector)
|
7
|
+
|
8
|
+
key = Trustworthy::Key.new(BigDecimal.new('2'), BigDecimal.new('3'))
|
9
|
+
@settings = Trustworthy::Settings.new
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'add_key' do
|
13
|
+
it 'encrypts and signs the key with the password' do
|
14
|
+
key = Trustworthy::Key.new(BigDecimal.new('2'), BigDecimal.new('3'))
|
15
|
+
settings = Trustworthy::Settings.new
|
16
|
+
settings.add_key(key, 'user', 'password1')
|
17
|
+
settings.keys['user']['salt'].should == '400$8$1b$3e31f076a3226825'
|
18
|
+
settings.keys['user']['authentication'].should == ['20c274efc68a460568853cc4be586183012769a312549d1aa57e29a36ec1503d'].pack('H*')
|
19
|
+
settings.keys['user']['ciphertext'].should == ['39164ec082fb8b7336d3c5500af99dcb17d2e60496ed553dfab4b05c568aa926'].pack('H*')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe 'add_secret' do
|
24
|
+
it 'adds a secret file name with an environment' do
|
25
|
+
settings = Trustworthy::Settings.new
|
26
|
+
settings.add_secret('foo', 'foo.enc')
|
27
|
+
settings.secrets['foo'].should == 'foo.enc'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'unlock_key' do
|
32
|
+
it 'verifies and decrypts the key with the password' do
|
33
|
+
key = Trustworthy::Key.new(BigDecimal.new('2'), BigDecimal.new('3'))
|
34
|
+
settings = Trustworthy::Settings.new
|
35
|
+
settings.add_key(key, 'user', 'password1')
|
36
|
+
unlocked_key = settings.unlock_key('user', 'password1')
|
37
|
+
unlocked_key.x.should == BigDecimal.new('2')
|
38
|
+
unlocked_key.y.should == BigDecimal.new('3')
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
describe 'write' do
|
44
|
+
it 'writes key information to the given file' do
|
45
|
+
key = Trustworthy::Key.new(BigDecimal.new('2'), BigDecimal.new('3'))
|
46
|
+
settings = Trustworthy::Settings.new
|
47
|
+
settings.add_key(key, 'user', 'password1')
|
48
|
+
FakeFS do
|
49
|
+
settings.write('settings.yml')
|
50
|
+
|
51
|
+
data = File.read('settings.yml')
|
52
|
+
yaml = YAML.load(data)
|
53
|
+
yaml['keys']['user']['salt'].should == '400$8$1b$3e31f076a3226825'
|
54
|
+
yaml['keys']['user']['authentication'].should == ['20c274efc68a460568853cc4be586183012769a312549d1aa57e29a36ec1503d'].pack('H*')
|
55
|
+
yaml['keys']['user']['ciphertext'].should == ['39164ec082fb8b7336d3c5500af99dcb17d2e60496ed553dfab4b05c568aa926'].pack('H*')
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe 'self.load' do
|
61
|
+
it 'loads the key information from the given file' do
|
62
|
+
FakeFS do
|
63
|
+
File.open('settings.yml', 'w') do |file|
|
64
|
+
file.write(<<-EOF)
|
65
|
+
keys:
|
66
|
+
user:
|
67
|
+
salt: 400$8$1b$3e31f076a3226825
|
68
|
+
ciphertext: !binary |-
|
69
|
+
ORZOwIL7i3M208VQCvmdyxfS5gSW7VU9+rSwXFaKqSY=
|
70
|
+
authentication: !binary |-
|
71
|
+
IMJ078aKRgVohTzEvlhhgwEnaaMSVJ0apX4po27BUD0=
|
72
|
+
EOF
|
73
|
+
end
|
74
|
+
|
75
|
+
settings = Trustworthy::Settings.load('settings.yml')
|
76
|
+
settings.keys['user']['salt'].should == '400$8$1b$3e31f076a3226825'
|
77
|
+
settings.keys['user']['authentication'].should == ['20c274efc68a460568853cc4be586183012769a312549d1aa57e29a36ec1503d'].pack('H*')
|
78
|
+
settings.keys['user']['ciphertext'].should == ['39164ec082fb8b7336d3c5500af99dcb17d2e60496ed553dfab4b05c568aa926'].pack('H*')
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
metadata
ADDED
@@ -0,0 +1,181 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: trustworthy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- John Downey
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-17 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: commander
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '4.1'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '4.1'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: hkdf
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 0.1.0
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 0.1.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: posix-spawn
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.3.6
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.3.6
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: scrypt
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '1.1'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '1.1'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: fakefs
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ~>
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: 0.4.0
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ~>
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: 0.4.0
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: rspec
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ~>
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '2.11'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ~>
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '2.11'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: rake
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
description: Implements a special case (k = 2) of Adi Shamir's secret sharing algorithm.
|
127
|
+
This allows secret files to be encrypted on disk but loaded into a process on start
|
128
|
+
if two keys are available.
|
129
|
+
email:
|
130
|
+
- jdowney@gmail.com
|
131
|
+
executables:
|
132
|
+
- trustworthy
|
133
|
+
extensions: []
|
134
|
+
extra_rdoc_files: []
|
135
|
+
files:
|
136
|
+
- lib/trustworthy/crypto.rb
|
137
|
+
- lib/trustworthy/key.rb
|
138
|
+
- lib/trustworthy/master_key.rb
|
139
|
+
- lib/trustworthy/random.rb
|
140
|
+
- lib/trustworthy/settings.rb
|
141
|
+
- lib/trustworthy/version.rb
|
142
|
+
- lib/trustworthy.rb
|
143
|
+
- spec/spec_helper.rb
|
144
|
+
- spec/trustworthy/crypto_spec.rb
|
145
|
+
- spec/trustworthy/key_spec.rb
|
146
|
+
- spec/trustworthy/master_key_spec.rb
|
147
|
+
- spec/trustworthy/random_spec.rb
|
148
|
+
- spec/trustworthy/settings_spec.rb
|
149
|
+
- bin/trustworthy
|
150
|
+
- README.md
|
151
|
+
homepage: http://github.com/jtdowney/trustworthy
|
152
|
+
licenses: []
|
153
|
+
post_install_message:
|
154
|
+
rdoc_options: []
|
155
|
+
require_paths:
|
156
|
+
- lib
|
157
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
158
|
+
none: false
|
159
|
+
requirements:
|
160
|
+
- - ! '>='
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: '0'
|
163
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
164
|
+
none: false
|
165
|
+
requirements:
|
166
|
+
- - ! '>='
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
version: '0'
|
169
|
+
requirements: []
|
170
|
+
rubyforge_project:
|
171
|
+
rubygems_version: 1.8.24
|
172
|
+
signing_key:
|
173
|
+
specification_version: 3
|
174
|
+
summary: Launch processes while keeping secrets encrypted on disk
|
175
|
+
test_files:
|
176
|
+
- spec/spec_helper.rb
|
177
|
+
- spec/trustworthy/crypto_spec.rb
|
178
|
+
- spec/trustworthy/key_spec.rb
|
179
|
+
- spec/trustworthy/master_key_spec.rb
|
180
|
+
- spec/trustworthy/random_spec.rb
|
181
|
+
- spec/trustworthy/settings_spec.rb
|