-
Notifications
You must be signed in to change notification settings - Fork 167
Expand file tree
/
Copy pathnist_encryption_spec.rb
More file actions
151 lines (115 loc) · 5.43 KB
/
nist_encryption_spec.rb
File metadata and controls
151 lines (115 loc) · 5.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
require 'rails_helper'
# NOTE this set of specs intentionally includes lots of
# duplicated code in order to explicitly show the algorithm at work.
describe 'NIST Encryption Model' do
# Generate and store a 128-bit salt S.
# Z1, Z2 = scrypt(S, password) # split 256-bit output into two halves
# Generate random R.
# D = KMS_GCM_Encrypt(key=server_secret, plaintext=R) ^ Z1
# E = hash( Z2 + R )
# F = hash(E)
# C = GCM_Encrypt(key = E, plaintext=PII) #occurs outside AWS-KMS
# Store F in password file, and store C and D.
#
# To decrypt PII and to verify passwords:
# Compute Z1’, Z2’ = scrypt(S, password’)
# R’ = KMS_GCM_Decrypt(key=server_secret, ciphertext=(D ^ Z1*)).
# E’ = hash( Z2’ + R’)
# F’ = hash(E’)
# Check to see if F’ matches the entry in the password file; if so, allow the login.
# plaintext_PII = GCM_Decrypt(key=E’, ciphertext = C)
before do
allow(FeatureManagement).to receive(:use_kms?).and_return(true)
end
let(:kms_prefix) { '{}cH' } # XOR of 'KMSx' and '0000'
describe 'password hashing' do
it 'creates two substrings Z1 and Z2 via scrypt of password and salt' do
# Generate and store a 128-bit salt S.
## (ours is actually 256 bits)
password = 'a long sekrit'
salt = Pii::Cipher.random_key
expect(salt.bytes.length).to eq 32
# Z1, Z2 = scrypt(S, password) # split 256-bit output into two halves
user_access_key = UserAccessKey.new(password: password, salt: salt)
expect(hex_to_bin(user_access_key.z1).length).to eq 16
expect(hex_to_bin(user_access_key.z2).length).to eq 16
expect { SCrypt::Password.new(user_access_key.as_scrypt_hash) }.
to_not raise_error
end
end
describe 'KMS encryption of random R' do
it 'creates encrypted key D and hash E' do
random_R, ciphered_R = stub_aws_kms_client
allow(Pii::Cipher).to receive(:random_key).and_return(random_R)
password = 'a long sekrit'
salt = SecureRandom.random_bytes(32)
user_access_key = UserAccessKey.new(password: password, salt: salt)
# D = KMS_GCM_Encrypt(key=server_secret, plaintext=R) ^ Z1
# E = hash( Z2 + R )
encrypted_D = user_access_key.xor(ciphered_R)
hash_E = OpenSSL::Digest::SHA256.hexdigest(user_access_key.z2 + random_R)
encrypted_key_maker = EncryptedKeyMaker.new
encrypted_key_maker.make(user_access_key)
expect(user_access_key.encrypted_d).to eq(kms_prefix + encrypted_D)
expect(user_access_key.hash_e).to eq hash_E
end
end
describe 'password storage and login' do
it 'creates hash F of Z2 using auto-generated salt' do
random_R, ciphered_R = stub_aws_kms_client
allow(Pii::Cipher).to receive(:random_key).and_return(random_R)
password = 'a long sekrit'
user = create(:user, password: password)
expect(user.valid_password?(password)).to eq true
expect(user.user_access_key).to be_a UserAccessKey
expect(user.user_access_key.random_r).to eq random_R
expect(user.encryption_key).to_not be_nil
expect(user.password_salt).to_not be_nil
hash_E = OpenSSL::Digest::SHA256.hexdigest(user.user_access_key.z2 + random_R)
hash_F = OpenSSL::Digest::SHA256.hexdigest(hash_E)
expect(user.encrypted_password).to eq hash_F
expect(user.user_access_key.hash_e).to eq hash_E
expect(user.user_access_key.hash_f).to eq hash_F
encrypted_key_maker = EncryptedKeyMaker.new
expect(encrypted_key_maker.unlock(user.user_access_key, user.encryption_key)).to eq hash_E
encrypted_D = Base64.strict_decode64(user.encryption_key)
expect(kms_prefix + user.user_access_key.xor(ciphered_R)).to eq(encrypted_D)
end
end
describe 'PII encryption' do
it 'creates encrypted payload C with KMS-encrypted key D using local AES cipher' do
random_R, ciphered_R = stub_aws_kms_client
allow(Pii::Cipher).to receive(:random_key).and_return(random_R)
password = 'a long sekrit'
salt = SecureRandom.random_bytes(32)
user_access_key = UserAccessKey.new(password: password, salt: salt)
pii = 'some sensitive stuff'
# D = KMS_GCM_Encrypt(key=server_secret, plaintext=R) ^ Z1
# E = hash( Z2 + R )
# C = GCM_Encrypt(key = E, plaintext=PII) # occurs outside AWS-KMS
# Store C and D.
encrypted_D = user_access_key.xor(ciphered_R)
hash_E = OpenSSL::Digest::SHA256.hexdigest(user_access_key.z2 + random_R)
password_encryptor = Pii::PasswordEncryptor.new
encrypted_payload = password_encryptor.encrypt(pii, user_access_key)
expect(encrypted_payload).to_not match(pii)
# encrypted_payload is an envelope that contains C and D.
encrypted_key, encrypted_C = open_envelope(encrypted_payload)
expect(Base64.strict_decode64(encrypted_key)).to eq(kms_prefix + encrypted_D)
# unroll encrypted_C to verify it was encrypted with hash_E
cipher = Pii::Cipher.new
expect { cipher.decrypt(encrypted_C, hash_E) }.not_to raise_error
deciphered = cipher.decrypt(encrypted_C, hash_E)
deciphered_pii, deciphered_pii_fingerprint = open_envelope(deciphered)
expect(deciphered_pii).to eq pii
fingerprint = Pii::Fingerprinter.fingerprint(deciphered_pii)
expect(fingerprint).to eq deciphered_pii_fingerprint
end
end
def open_envelope(envelope)
envelope.split(Pii::Encryptor::DELIMITER).map { |segment| Base64.strict_decode64(segment) }
end
def hex_to_bin(str)
str.scan(/../).map(&:hex).pack('c*')
end
end