trustworthy 0.1.0 → 0.2.0

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