trustworthy 0.1.0 → 0.2.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.
@@ -1,63 +1,56 @@
1
1
  module Trustworthy
2
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'])
3
+ def self.open(filename)
4
+ store = YAML::Store.new(filename)
5
+ store.transaction do
6
+ yield Trustworthy::Settings.new(store)
7
+ end
9
8
  end
10
9
 
11
- def initialize(keys = {}, secrets = {})
12
- @keys = keys
13
- @secrets = secrets
10
+ def initialize(store)
11
+ @store = store
14
12
  end
15
13
 
16
14
  def add_key(key, username, password)
17
15
  salt = SCrypt::Engine.generate_salt
18
16
 
19
- crypto = _crypto_from_password(salt, password)
17
+ cipher = _cipher_from_password(salt, password)
18
+ nonce = Trustworthy::Cipher.generate_nonce
20
19
  plaintext = "#{key.x.to_s('F')},#{key.y.to_s('F')}"
21
- ciphertext = crypto.encrypt(plaintext)
20
+ ciphertext = cipher.encrypt(nonce, '', plaintext)
22
21
 
23
- @keys[username] = {
22
+ @store[username] = {
24
23
  'salt' => salt,
25
- 'ciphertext' => ciphertext,
26
- 'authentication' => crypto.sign(ciphertext)
24
+ 'encrypted_point' => nonce + ciphertext,
27
25
  }
28
26
  end
29
27
 
30
- def add_secret(environment, filename)
31
- @secrets[environment] = filename
28
+ def empty?
29
+ @store.roots.empty?
30
+ end
31
+
32
+ def find_key(username)
33
+ @store[username]
32
34
  end
33
35
 
34
36
  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)
37
+ key = find_key(username)
38
+ salt = key['salt']
39
+ ciphertext = key['encrypted_point']
40
+ ciphertext.force_encoding('BINARY') if ciphertext.respond_to?(:force_encoding)
41
+ nonce = ciphertext.slice(0, Trustworthy::Cipher.nonce_len)
42
+ ciphertext = ciphertext.slice(Trustworthy::Cipher.nonce_len..-1)
43
+
44
+ cipher = _cipher_from_password(salt, password)
45
+ plaintext = cipher.decrypt(nonce, '', ciphertext)
42
46
  x, y = plaintext.split(',').map { |n| BigDecimal.new(n) }
43
47
  Trustworthy::Key.new(x, y)
44
48
  end
45
49
 
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)
50
+ def _cipher_from_password(salt, password)
54
51
  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)
52
+ key = SCrypt::Engine.scrypt(password, salt, cost, 64)
53
+ Trustworthy::Cipher.new(key)
61
54
  end
62
55
  end
63
56
  end
@@ -1,3 +1,3 @@
1
1
  module Trustworthy
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
data/lib/trustworthy.rb CHANGED
@@ -1,13 +1,16 @@
1
+ require 'aead'
1
2
  require 'bigdecimal'
2
3
  require 'hkdf'
3
- require 'openssl'
4
- require 'posix/spawn'
5
4
  require 'scrypt'
6
5
  require 'securerandom'
7
- require 'trustworthy/crypto'
8
6
  require 'trustworthy/key'
9
7
  require 'trustworthy/master_key'
10
8
  require 'trustworthy/random'
11
9
  require 'trustworthy/settings'
12
10
  require 'trustworthy/version'
13
- require 'yaml'
11
+ require 'yaml/store'
12
+
13
+ module Trustworthy
14
+ CipherAlgorithm = 'AES-256-CBC-HMAC-SHA-256'
15
+ Cipher = AEAD::Cipher.new(CipherAlgorithm)
16
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,18 +1,30 @@
1
- require 'fakefs/safe'
2
-
3
1
  require 'trustworthy'
2
+ require 'trustworthy/cli'
3
+ require 'construct'
4
+ require 'highline/simulate'
4
5
 
5
6
  RSpec.configure do |config|
6
7
  config.order = 'random'
8
+ config.include Construct::Helpers
7
9
  config.before(:each) do
8
10
  Trustworthy::Random.stub(:_source).and_return('/dev/urandom')
9
11
  end
10
12
  end
11
13
 
12
- module Trustworthy
13
- module TestValues
14
- EncryptionKey = ['7fc6880ba7a75148e6b14a6ebca73b9861954835fd17237cbedb71a6ad3fefc4'].pack('H*')
15
- AuthenticationKey = ['12e7489dc1b2e6da1213cac0df946615a7088eeccdd6b2d4bf330e9560fabd52'].pack('H*')
16
- InitializationVector = ['39164ec082fb8b7336d3c5500af99dcb'].pack('H*')
14
+ module TestValues
15
+ SettingsFile = 'trustworthy.yml'
16
+ InitializationVector = ['39164ec082fb8b7336d3c5500af99dcb'].pack('H*')
17
+ Plaintext = 'the chair is against the wall'
18
+ Ciphertext = ['39164ec082fb8b7336d3c5500af99dcb6f5eac5d817a65b8ac7c5ed80691db36904e1ce613a1057d807b37127d927a4b0a9a1b951ef576fc9a9edca0ef5b83e2bae850a8b3b79bfeddff892d1941d439'].pack('H*')
19
+ Salt = '400$8$1b$3e31f076a3226825'
20
+ MasterKey = Trustworthy::MasterKey.new(BigDecimal.new('1'), BigDecimal.new('5'))
21
+ EncryptedPoint = ['39164ec082fb8b7336d3c5500af99dcb17d2e60496ed553dfab4b05c568aa9260cb8e53930911c8e718bc97eb88def400e5cac1e4ee3d15060920c25d1346285'].pack('H*')
22
+ EncryptedFile = ['39164ec082fb8b7336d3c5500af99dcba37f59607382f87a2da14881a9e1eabd23965d46d7c1a651b0c930cd0ee756d9358d67edaaba02a22e902136a2a90953672c2937b0cbaac0167922918578a98c'].pack('H*')
23
+ end
24
+
25
+ def create_config(filename)
26
+ Trustworthy::Settings.open(filename) do |settings|
27
+ settings.add_key(TestValues::MasterKey.create_key, 'user1', 'password1')
28
+ settings.add_key(TestValues::MasterKey.create_key, 'user2', 'password2')
17
29
  end
18
30
  end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+
3
+ describe Trustworthy::CLI::AddKey do
4
+ before(:each) do
5
+ $terminal.stub(:say)
6
+ end
7
+
8
+ around(:each) do |example|
9
+ within_construct do |construct|
10
+ construct.file(TestValues::SettingsFile)
11
+ create_config(TestValues::SettingsFile)
12
+ example.run
13
+ end
14
+ end
15
+
16
+ describe 'run' do
17
+ it 'should add a new user' do
18
+ HighLine::Simulate.with(
19
+ 'user1',
20
+ 'password1',
21
+ 'user2',
22
+ 'password2',
23
+ 'user3',
24
+ 'password3',
25
+ 'password3'
26
+ ) do
27
+ Trustworthy::CLI::AddKey.new.run([])
28
+ end
29
+
30
+ contents = File.read(TestValues::SettingsFile)
31
+ subkeys = YAML.load(contents)
32
+ subkeys.should have_key('user1')
33
+ subkeys.should have_key('user2')
34
+ subkeys.should have_key('user3')
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,114 @@
1
+ require 'spec_helper'
2
+
3
+ describe Trustworthy::CLI::Command do
4
+ def test_command
5
+ return @klass if @klass
6
+ @klass = Class.new
7
+ @klass.send(:include, Trustworthy::CLI::Command)
8
+ @klass
9
+ end
10
+
11
+ before(:each) do
12
+ $terminal.stub(:say)
13
+ end
14
+
15
+ around(:each) do |example|
16
+ within_construct do |construct|
17
+ construct.file(TestValues::SettingsFile)
18
+ create_config(TestValues::SettingsFile)
19
+ example.run
20
+ end
21
+ end
22
+
23
+ describe 'unlock_master_key' do
24
+ it 'should require two distinct keys to unlock' do
25
+ command = test_command.new
26
+ command.should_receive(:error).with('Key user1 is already in use')
27
+
28
+ HighLine::Simulate.with(
29
+ 'user1',
30
+ 'password1',
31
+ 'user1',
32
+ 'user2',
33
+ 'password2'
34
+ ) do
35
+ Trustworthy::Settings.open(TestValues::SettingsFile) do |settings|
36
+ master_key = command.unlock_master_key(settings)
37
+ master_key.should == TestValues::MasterKey
38
+ end
39
+ end
40
+ end
41
+
42
+ it 'should required an existing user for the first key' do
43
+ command = test_command.new
44
+ command.should_receive(:error).with('Key missing does not exist')
45
+
46
+ HighLine::Simulate.with(
47
+ 'missing',
48
+ 'user1',
49
+ 'password1',
50
+ 'user2',
51
+ 'password2'
52
+ ) do
53
+ Trustworthy::Settings.open(TestValues::SettingsFile) do |settings|
54
+ master_key = command.unlock_master_key(settings)
55
+ master_key.should == TestValues::MasterKey
56
+ end
57
+ end
58
+ end
59
+
60
+ it 'should required an existing user for the second key' do
61
+ command = test_command.new
62
+ command.should_receive(:error).with('Key missing does not exist')
63
+
64
+ HighLine::Simulate.with(
65
+ 'user1',
66
+ 'password1',
67
+ 'missing',
68
+ 'user2',
69
+ 'password2'
70
+ ) do
71
+ Trustworthy::Settings.open(TestValues::SettingsFile) do |settings|
72
+ master_key = command.unlock_master_key(settings)
73
+ master_key.should == TestValues::MasterKey
74
+ end
75
+ end
76
+ end
77
+
78
+ it 'should prompt for the correct password for the first key' do
79
+ command = test_command.new
80
+ command.should_receive(:error).with('Password incorrect for user1')
81
+
82
+ HighLine::Simulate.with(
83
+ 'user1',
84
+ 'bad_password',
85
+ 'password1',
86
+ 'user2',
87
+ 'password2'
88
+ ) do
89
+ Trustworthy::Settings.open(TestValues::SettingsFile) do |settings|
90
+ master_key = command.unlock_master_key(settings)
91
+ master_key.should == TestValues::MasterKey
92
+ end
93
+ end
94
+ end
95
+
96
+ it 'should prompt for the correct password for the second key' do
97
+ command = test_command.new
98
+ command.should_receive(:error).with('Password incorrect for user2')
99
+
100
+ HighLine::Simulate.with(
101
+ 'user1',
102
+ 'password1',
103
+ 'user2',
104
+ 'bad_password',
105
+ 'password2'
106
+ ) do
107
+ Trustworthy::Settings.open(TestValues::SettingsFile) do |settings|
108
+ master_key = command.unlock_master_key(settings)
109
+ master_key.should == TestValues::MasterKey
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+
3
+ describe Trustworthy::CLI::Decrypt do
4
+ before(:each) do
5
+ $terminal.stub(:say)
6
+ end
7
+
8
+ around(:each) do |example|
9
+ within_construct do |construct|
10
+ construct.file(TestValues::SettingsFile)
11
+ construct.file('input.txt', TestValues::EncryptedFile)
12
+ construct.file('output.txt')
13
+ create_config(TestValues::SettingsFile)
14
+ example.run
15
+ end
16
+ end
17
+
18
+ describe 'run' do
19
+ it 'should unlock the master key and decrypt the file' do
20
+ HighLine::Simulate.with(
21
+ 'user1',
22
+ 'password1',
23
+ 'user2',
24
+ 'password2'
25
+ ) do
26
+ Trustworthy::CLI::Decrypt.new.run(['-i', 'input.txt', '-o', 'output.txt'])
27
+ end
28
+
29
+ File.open('output.txt', 'rb') do |file|
30
+ ciphertext = file.read
31
+ ciphertext.should == TestValues::Plaintext
32
+ end
33
+ end
34
+
35
+ it 'should require an input file' do
36
+ HighLine::Simulate.with(
37
+ 'user1',
38
+ 'password1',
39
+ 'user2',
40
+ 'password2'
41
+ ) do
42
+ decrypt = Trustworthy::CLI::Encrypt.new
43
+ decrypt.should_receive(:error).with('Must provide an input file')
44
+ decrypt.should_receive(:print_help)
45
+ decrypt.run([])
46
+ end
47
+ end
48
+
49
+ it 'should require an output file' do
50
+ HighLine::Simulate.with(
51
+ 'user1',
52
+ 'password1',
53
+ 'user2',
54
+ 'password2'
55
+ ) do
56
+ decrypt = Trustworthy::CLI::Encrypt.new
57
+ decrypt.should_receive(:error).with('Must provide an output file')
58
+ decrypt.should_receive(:print_help)
59
+ decrypt.run(['-i', 'input.txt'])
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,64 @@
1
+ require 'spec_helper'
2
+
3
+ describe Trustworthy::CLI::Encrypt do
4
+ before(:each) do
5
+ $terminal.stub(:say)
6
+ AEAD::Cipher::AES_256_CBC_HMAC_SHA_256.stub(:generate_nonce).and_return(TestValues::InitializationVector)
7
+ end
8
+
9
+ around(:each) do |example|
10
+ within_construct do |construct|
11
+ construct.file(TestValues::SettingsFile)
12
+ construct.file('input.txt', TestValues::Plaintext)
13
+ construct.file('output.txt')
14
+ create_config(TestValues::SettingsFile)
15
+ example.run
16
+ end
17
+ end
18
+
19
+ describe 'run' do
20
+ it 'should unlock the master key and encrypt the file' do
21
+ HighLine::Simulate.with(
22
+ 'user1',
23
+ 'password1',
24
+ 'user2',
25
+ 'password2'
26
+ ) do
27
+ Trustworthy::CLI::Encrypt.new.run(['-i', 'input.txt', '-o', 'output.txt'])
28
+ end
29
+
30
+ File.open('output.txt', 'rb') do |file|
31
+ ciphertext = file.read
32
+ ciphertext.should == TestValues::EncryptedFile
33
+ end
34
+ end
35
+
36
+ it 'should require an input file' do
37
+ HighLine::Simulate.with(
38
+ 'user1',
39
+ 'password1',
40
+ 'user2',
41
+ 'password2'
42
+ ) do
43
+ encrypt = Trustworthy::CLI::Encrypt.new
44
+ encrypt.should_receive(:error).with('Must provide an input file')
45
+ encrypt.should_receive(:print_help)
46
+ encrypt.run([])
47
+ end
48
+ end
49
+
50
+ it 'should require an output file' do
51
+ HighLine::Simulate.with(
52
+ 'user1',
53
+ 'password1',
54
+ 'user2',
55
+ 'password2'
56
+ ) do
57
+ encrypt = Trustworthy::CLI::Encrypt.new
58
+ encrypt.should_receive(:error).with('Must provide an output file')
59
+ encrypt.should_receive(:print_help)
60
+ encrypt.run(['-i', 'input.txt'])
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,111 @@
1
+ require 'spec_helper'
2
+
3
+ describe Trustworthy::CLI::Init do
4
+ before(:each) do
5
+ $terminal.stub(:say)
6
+ end
7
+
8
+ around(:each) do |example|
9
+ within_construct do |construct|
10
+ construct.file(TestValues::SettingsFile)
11
+ example.run
12
+ end
13
+ end
14
+
15
+ describe 'run' do
16
+ it 'should not allow any previous keys to exist' do
17
+ create_config(TestValues::SettingsFile)
18
+ $terminal.should_receive(:say).with(/Config trustworthy\.yml already exists/)
19
+ Trustworthy::CLI::Init.new.run([])
20
+ end
21
+
22
+ it 'should write a settings file' do
23
+ HighLine::Simulate.with(
24
+ 'user1',
25
+ 'password1',
26
+ 'password1',
27
+ 'user2',
28
+ 'password2',
29
+ 'password2'
30
+ ) do
31
+ Trustworthy::CLI::Init.new.run([])
32
+ end
33
+
34
+ contents = File.read(TestValues::SettingsFile)
35
+ subkeys = YAML.load(contents)
36
+ subkeys.should have_key('user1')
37
+ subkeys.should have_key('user2')
38
+ end
39
+
40
+ it 'should confirm passwords' do
41
+ HighLine::Simulate.with(
42
+ 'user1',
43
+ 'password1',
44
+ 'password2',
45
+ 'password1',
46
+ 'password1',
47
+ 'user2',
48
+ 'password2',
49
+ 'password2'
50
+ ) do
51
+ Trustworthy::CLI::Init.new.run([])
52
+ end
53
+
54
+ contents = File.read(TestValues::SettingsFile)
55
+ subkeys = YAML.load(contents)
56
+ subkeys.should have_key('user1')
57
+ subkeys.should have_key('user2')
58
+ end
59
+
60
+ it 'should write to a specified file' do
61
+ filename = 'test.yml'
62
+ within_construct do |construct|
63
+ construct.file(filename)
64
+ HighLine::Simulate.with(
65
+ 'user1',
66
+ 'password1',
67
+ 'password1',
68
+ 'user2',
69
+ 'password2',
70
+ 'password2'
71
+ ) do
72
+ Trustworthy::CLI::Init.new.run(['-c', filename])
73
+ end
74
+
75
+ contents = File.read(filename)
76
+ subkeys = YAML.load(contents)
77
+ subkeys.should have_key('user1')
78
+ subkeys.should have_key('user2')
79
+ end
80
+ end
81
+
82
+ it 'should generate the specified number of keys' do
83
+ HighLine::Simulate.with(
84
+ 'user1',
85
+ 'password1',
86
+ 'password1',
87
+ 'user2',
88
+ 'password2',
89
+ 'password2',
90
+ 'user3',
91
+ 'password3',
92
+ 'password3'
93
+ ) do
94
+ Trustworthy::CLI::Init.new.run(['-k', '3'])
95
+ end
96
+
97
+ contents = File.read(TestValues::SettingsFile)
98
+ subkeys = YAML.load(contents)
99
+ subkeys.should have_key('user1')
100
+ subkeys.should have_key('user2')
101
+ subkeys.should have_key('user3')
102
+ end
103
+
104
+ it 'should require two subkeys minimum' do
105
+ init = Trustworthy::CLI::Init.new
106
+ init.should_receive(:error).with('Must generate at least two keys')
107
+ init.should_receive(:print_help)
108
+ init.run(['-k', '1'])
109
+ end
110
+ end
111
+ end
@@ -6,11 +6,11 @@ describe Trustworthy::MasterKey do
6
6
  key1 = master_key.create_key
7
7
  key2 = master_key.create_key
8
8
 
9
- ciphertext = master_key.encrypt('foobar')
9
+ ciphertext = master_key.encrypt(TestValues::Plaintext)
10
10
 
11
11
  new_master_key = Trustworthy::MasterKey.create_from_keys(key1, key2)
12
12
  plaintext = new_master_key.decrypt(ciphertext)
13
- plaintext.should == 'foobar'
13
+ plaintext.should == TestValues::Plaintext
14
14
  end
15
15
 
16
16
  it 'should function with any 2 of n keys' do
@@ -18,14 +18,14 @@ describe Trustworthy::MasterKey do
18
18
  key1 = master_key1.create_key
19
19
  key2 = master_key1.create_key
20
20
 
21
- ciphertext = master_key1.encrypt('foobar')
21
+ ciphertext = master_key1.encrypt(TestValues::Plaintext)
22
22
 
23
23
  master_key2 = Trustworthy::MasterKey.create_from_keys(key1, key2)
24
24
  key3 = master_key2.create_key
25
25
 
26
26
  master_key3 = Trustworthy::MasterKey.create_from_keys(key1, key3)
27
27
  plaintext = master_key3.decrypt(ciphertext)
28
- plaintext.should == 'foobar'
28
+ plaintext.should == TestValues::Plaintext
29
29
  end
30
30
 
31
31
  describe 'self.create' do
@@ -63,25 +63,25 @@ describe Trustworthy::MasterKey do
63
63
 
64
64
  describe 'encrypt' do
65
65
  it 'should encrypt and sign the data using the intercept' do
66
- Trustworthy::Random.stub(:bytes).and_return(Trustworthy::TestValues::InitializationVector)
66
+ AEAD::Cipher::AES_256_CBC_HMAC_SHA_256.stub(:generate_nonce).and_return(TestValues::InitializationVector)
67
67
  master_key = Trustworthy::MasterKey.new(BigDecimal.new('6'), BigDecimal.new('24'))
68
- ciphertext = master_key.encrypt('foobar')
69
- ciphertext.should == ['0438a89ef9e5792849ef611bff0e7b405a84ac515461489499679ca77ade6a6a39164ec082fb8b7336d3c5500af99dcbde056af859baa7e72c4c2661651e88d5'].pack('H*')
68
+ ciphertext = master_key.encrypt(TestValues::Plaintext)
69
+ ciphertext.should == TestValues::Ciphertext
70
70
  end
71
71
  end
72
72
 
73
73
  describe 'decrypt' do
74
74
  it 'should decrypt and verify the data using the intercept' do
75
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'
76
+ plaintext = master_key.decrypt(TestValues::Ciphertext)
77
+ plaintext.should == TestValues::Plaintext
79
78
  end
80
79
 
81
80
  it 'should raise an invalid signature error if signatures do not match' do
82
81
  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')
82
+ ciphertext = TestValues::Ciphertext.next
83
+ expect { master_key.decrypt(ciphertext) }.to raise_error(ArgumentError, 'ciphertext failed authentication step')
85
84
  end
86
85
  end
87
86
  end
87
+