aead 1.3.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/.gitignore +8 -0
- data/.travis.yml +3 -0
- data/.yardopts +2 -0
- data/Gemfile +7 -0
- data/Guardfile +10 -0
- data/LICENSE.md +22 -0
- data/README.md +57 -0
- data/Rakefile +46 -0
- data/VERSION +1 -0
- data/aead.gemspec +34 -0
- data/ext/openssl/cipher/aead/.gitignore +4 -0
- data/ext/openssl/cipher/aead/aead.c +156 -0
- data/ext/openssl/cipher/aead/extconf.rb +6 -0
- data/lib/aead.rb +4 -0
- data/lib/aead/cipher.rb +221 -0
- data/lib/aead/cipher/aes_256_cbc_hmac_sha_256.rb +30 -0
- data/lib/aead/cipher/aes_256_ctr_hmac_sha_256.rb +30 -0
- data/lib/aead/cipher/aes_256_gcm.rb +45 -0
- data/lib/aead/cipher/aes_hmac.rb +69 -0
- data/lib/aead/nonce.rb +270 -0
- data/lib/openssl/cipher/.gitignore +0 -0
- data/spec/aead/cipher/aes_256_cbc_hmac_sha_256_spec.rb +142 -0
- data/spec/aead/cipher/aes_256_ctr_hmac_sha_256_spec.rb +142 -0
- data/spec/aead/cipher/aes_256_gcm_spec.rb +133 -0
- data/spec/aead/cipher_spec.rb +61 -0
- data/spec/aead/nonce_spec.rb +124 -0
- data/spec/spec_helper.rb +26 -0
- metadata +287 -0
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'aead/cipher'
|
2
|
+
require 'aead/cipher/aes_hmac'
|
3
|
+
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
#
|
7
|
+
# Encrypt plaintext using the CBC mode of AES and authenticate the
|
8
|
+
# result with HMAC-SHA-256.
|
9
|
+
#
|
10
|
+
class AEAD::Cipher::AES_256_CBC_HMAC_SHA_256 < AEAD::Cipher
|
11
|
+
include AEAD::Cipher::AES_HMAC
|
12
|
+
|
13
|
+
def self.key_len; 64; end
|
14
|
+
def self.iv_len; 16; end
|
15
|
+
def self.nonce_len; 16; end
|
16
|
+
def self.tag_len; 32; end
|
17
|
+
|
18
|
+
def self.encryption_key_len; 32; end
|
19
|
+
def self.signing_key_len; 32; end
|
20
|
+
|
21
|
+
def self.cipher_mode; 'aes-256-cbc'; end
|
22
|
+
def self.digest_mode; 'SHA256'; end
|
23
|
+
|
24
|
+
#
|
25
|
+
# CBC requires a randomly-generated nonce.
|
26
|
+
#
|
27
|
+
def self.generate_nonce
|
28
|
+
SecureRandom.random_bytes(self.nonce_len)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'aead/cipher'
|
2
|
+
require 'aead/cipher/aes_hmac'
|
3
|
+
|
4
|
+
require 'securerandom'
|
5
|
+
|
6
|
+
#
|
7
|
+
# Encrypt plaintext using the CTR mode of AES and authenticate the
|
8
|
+
# result with HMAC-SHA-256.
|
9
|
+
#
|
10
|
+
class AEAD::Cipher::AES_256_CTR_HMAC_SHA_256 < AEAD::Cipher
|
11
|
+
include AEAD::Cipher::AES_HMAC
|
12
|
+
|
13
|
+
def self.key_len; 64; end
|
14
|
+
def self.iv_len; 16; end
|
15
|
+
def self.nonce_len; 16; end
|
16
|
+
def self.tag_len; 32; end
|
17
|
+
|
18
|
+
def self.encryption_key_len; 32; end
|
19
|
+
def self.signing_key_len; 32; end
|
20
|
+
|
21
|
+
def self.cipher_mode; 'aes-256-ctr'; end
|
22
|
+
def self.digest_mode; 'SHA256'; end
|
23
|
+
|
24
|
+
#
|
25
|
+
# CBC requires non-range-overlapped IVs, and random numbers suffice.
|
26
|
+
#
|
27
|
+
def self.generate_nonce
|
28
|
+
SecureRandom.random_bytes(self.nonce_len)
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'aead/cipher'
|
2
|
+
|
3
|
+
#
|
4
|
+
# Encrypt plaintext using the Galois Counter Mode of AES.
|
5
|
+
#
|
6
|
+
class AEAD::Cipher::AES_256_GCM < AEAD::Cipher
|
7
|
+
def self.key_len; 32; end
|
8
|
+
def self.iv_len; 12; end
|
9
|
+
def self.nonce_len; 12; end
|
10
|
+
def self.tag_len; 16; end
|
11
|
+
|
12
|
+
#
|
13
|
+
# Instantiates the cipher with a secret key.
|
14
|
+
#
|
15
|
+
# @param [String] key a secret encryption key
|
16
|
+
#
|
17
|
+
def initialize(key)
|
18
|
+
super('aes-256-gcm', key)
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def _encrypt(nonce, aad, plaintext)
|
24
|
+
self.cipher(:encrypt) do |cipher|
|
25
|
+
cipher.key = self.key
|
26
|
+
cipher.iv = nonce
|
27
|
+
cipher.aad = aad.to_s if aad
|
28
|
+
|
29
|
+
cipher.update(plaintext) + cipher.final + cipher.gcm_tag
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def _decrypt(nonce, aad, ciphertext, tag)
|
34
|
+
self.cipher(:decrypt) do |cipher|
|
35
|
+
cipher.key = self.key
|
36
|
+
cipher.iv = nonce
|
37
|
+
cipher.gcm_tag = tag
|
38
|
+
cipher.aad = aad.to_s if aad
|
39
|
+
|
40
|
+
cipher.update(ciphertext).tap { cipher.verify }
|
41
|
+
end
|
42
|
+
rescue OpenSSL::Cipher::CipherError
|
43
|
+
raise ArgumentError, 'ciphertext failed authentication step'
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'aead/cipher'
|
2
|
+
|
3
|
+
#
|
4
|
+
# Provides the implementation details of AES + HMAC, assuming the
|
5
|
+
# class including this module has defined proper class methods.
|
6
|
+
#
|
7
|
+
module AEAD::Cipher::AES_HMAC
|
8
|
+
#
|
9
|
+
# Initializes the cipher with a given secret encryption key.
|
10
|
+
#
|
11
|
+
# @param [String] key a secret encryption key
|
12
|
+
#
|
13
|
+
def initialize(key)
|
14
|
+
super(self.class.cipher_mode, key)
|
15
|
+
end
|
16
|
+
|
17
|
+
protected
|
18
|
+
|
19
|
+
def encryption_key
|
20
|
+
self.key[0, self.class.encryption_key_len]
|
21
|
+
end
|
22
|
+
|
23
|
+
def signing_key
|
24
|
+
self.key[self.class.encryption_key_len, self.class.signing_key_len]
|
25
|
+
end
|
26
|
+
|
27
|
+
def _encrypt(nonce, aad, plaintext)
|
28
|
+
self.cipher(:encrypt) do |cipher|
|
29
|
+
cipher.key = self.encryption_key
|
30
|
+
cipher.iv = nonce
|
31
|
+
|
32
|
+
ciphertext = cipher.update(plaintext) + cipher.final
|
33
|
+
mac = hmac_generate(nonce, aad.to_s, ciphertext)
|
34
|
+
|
35
|
+
ciphertext << mac
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def _decrypt(nonce, aad, ciphertext, tag)
|
40
|
+
raise ArgumentError, 'ciphertext failed authentication step' unless
|
41
|
+
hmac_verify(nonce, aad.to_s, ciphertext, tag)
|
42
|
+
|
43
|
+
self.cipher(:decrypt) do |cipher|
|
44
|
+
cipher.key = self.encryption_key
|
45
|
+
cipher.iv = nonce
|
46
|
+
|
47
|
+
cipher.update(ciphertext) << cipher.final
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def hmac_generate(nonce, aad, ciphertext)
|
52
|
+
OpenSSL::HMAC.digest self.class.digest_mode, self.signing_key,
|
53
|
+
hmac_encode(self.class.cipher_mode) <<
|
54
|
+
hmac_encode(ciphertext) <<
|
55
|
+
hmac_encode(nonce) <<
|
56
|
+
hmac_encode(aad)
|
57
|
+
end
|
58
|
+
|
59
|
+
def hmac_verify(nonce, aad, ciphertext, hmac)
|
60
|
+
self.class.signature_compare(
|
61
|
+
hmac_generate(nonce, aad, ciphertext),
|
62
|
+
hmac
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
def hmac_encode(string)
|
67
|
+
[ string.length ].pack('Q>') << string
|
68
|
+
end
|
69
|
+
end
|
data/lib/aead/nonce.rb
ADDED
@@ -0,0 +1,270 @@
|
|
1
|
+
require 'aead'
|
2
|
+
|
3
|
+
require 'macaddr'
|
4
|
+
require 'monitor'
|
5
|
+
require 'pathname'
|
6
|
+
require 'securerandom'
|
7
|
+
require 'tempfile'
|
8
|
+
|
9
|
+
#
|
10
|
+
# Generates RFC 5114-compliant nonces.
|
11
|
+
#
|
12
|
+
class AEAD::Nonce
|
13
|
+
include MonitorMixin
|
14
|
+
|
15
|
+
# Number of octets in the counter field.
|
16
|
+
COUNTER_OCTET_SIZE = 4
|
17
|
+
|
18
|
+
# Initial value of the counter field (4 octets zeroed out)
|
19
|
+
COUNTER_INITIAL_VALUE = '%08x' % 0
|
20
|
+
|
21
|
+
# Maximum possible value of the counter before rolling over (4
|
22
|
+
# octets all set to one).
|
23
|
+
COUNTER_MAXIMUM_VALUE = '%08x' % (2 ** (COUNTER_OCTET_SIZE * 8) - 1)
|
24
|
+
|
25
|
+
# Number of nonces to reserve between state file updates. 256 is
|
26
|
+
# convenient in that it leads to pleasant state files and represents
|
27
|
+
# a reasonable medium between frequent file locks and wasted
|
28
|
+
# nonce values when the process terminates.
|
29
|
+
COUNTER_BATCH_SIZE = 0xff
|
30
|
+
|
31
|
+
# The LSB of the most-significant octet of the MAC is the multicast
|
32
|
+
# bit, and should be set on generated MAC addresses to distinguish
|
33
|
+
# them from real ones
|
34
|
+
MAC_MULTICAST_MASK = 0x010000000000
|
35
|
+
|
36
|
+
# The statefile is not configurable. All processes on a single
|
37
|
+
# machine must share the same state file.
|
38
|
+
STATE_FILE = Pathname.new('/var/tmp/ruby-aead').expand_path
|
39
|
+
|
40
|
+
# Packed format of the nonce state. As recommended by RFC 5116. From
|
41
|
+
# MSB to LSB:
|
42
|
+
# octets 1 - 8 : fixed (hardware id + random id)
|
43
|
+
# octets 9 - 12: counter
|
44
|
+
PACK_FORMAT = "H12 H4 H8"
|
45
|
+
|
46
|
+
#
|
47
|
+
# Globally replaces the state file with a tempfile, so testing
|
48
|
+
# doesn't waste valuable nonces in the global state file.
|
49
|
+
#
|
50
|
+
# @param [String] file the tempfile to use
|
51
|
+
#
|
52
|
+
def self.stub_for_testing!(file = Tempfile.new('ruby-aead'))
|
53
|
+
define_method :state_file_with_stub_for_testing do
|
54
|
+
@state_file_stubbed_for_testing ||= Pathname.new(file)
|
55
|
+
end
|
56
|
+
|
57
|
+
alias_method :state_file_without_stub_for_testing, :state_file unless
|
58
|
+
self.instance_methods.include?(:state_file_without_stub_for_testing)
|
59
|
+
|
60
|
+
alias_method :state_file, :state_file_with_stub_for_testing
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# Generates an RFC 5114-compliant nonce suitable for use in AEAD
|
65
|
+
# encryption modes.
|
66
|
+
#
|
67
|
+
# @return [String] a 12-byte nonce
|
68
|
+
#
|
69
|
+
def self.generate
|
70
|
+
@instance ||= self.new
|
71
|
+
@instance.shift
|
72
|
+
end
|
73
|
+
|
74
|
+
#
|
75
|
+
# Initializes the nonce generator. Resumes the counter from disk if
|
76
|
+
# it has generated nonces before.
|
77
|
+
#
|
78
|
+
# @return [Nonce] the generator
|
79
|
+
#
|
80
|
+
def initialize
|
81
|
+
self.state_file = STATE_FILE
|
82
|
+
|
83
|
+
super # so the Monitor is initialized
|
84
|
+
end
|
85
|
+
|
86
|
+
#
|
87
|
+
# Returns a nonce from the generator. If a count is passed, returns
|
88
|
+
# an array of nonces.
|
89
|
+
#
|
90
|
+
# @param [nil, Integer] count the number of nonces to return
|
91
|
+
# @return [String, Array<String>] a single nonce or array of nonces
|
92
|
+
#
|
93
|
+
def shift(count = nil)
|
94
|
+
# short-circuit with a single nonce if no argument
|
95
|
+
return self.state.pack(PACK_FORMAT) if count.nil?
|
96
|
+
|
97
|
+
count.times.map do
|
98
|
+
self.state.pack(PACK_FORMAT)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
protected
|
103
|
+
|
104
|
+
# State file is kept as an accessor to make it easier for tests to
|
105
|
+
# manipulate state externally.
|
106
|
+
attr_accessor :state_file
|
107
|
+
|
108
|
+
#
|
109
|
+
# Requests the current state of the nonce generator. Merely
|
110
|
+
# querying the current state bumps its counter to the next value,
|
111
|
+
# helping ensure we never return the same nonce twice.
|
112
|
+
#
|
113
|
+
def state
|
114
|
+
@_state ||= load_state
|
115
|
+
@_state[0..2]
|
116
|
+
ensure
|
117
|
+
# don't bump the state if we raised an exception and didin't
|
118
|
+
# actually return the nonce
|
119
|
+
raise if $!
|
120
|
+
|
121
|
+
# after returning the state, bump it to the next one and reload
|
122
|
+
# from the state file if we've exceeded the maximum counter for
|
123
|
+
# the reserved batch
|
124
|
+
@_state = bump_state(@_state.dup)
|
125
|
+
@_state = load_state if (@_state[2].hex > @_state[3].hex)
|
126
|
+
end
|
127
|
+
|
128
|
+
def state_with_thread_safety
|
129
|
+
self.synchronize { self.state_without_thread_safety }
|
130
|
+
end
|
131
|
+
|
132
|
+
alias state_without_thread_safety state
|
133
|
+
alias state state_with_thread_safety
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
#
|
138
|
+
# Called from Object#dup and Object#clone. We must ensure states are
|
139
|
+
# never repeated, so ensure that we wipe internal state.
|
140
|
+
#
|
141
|
+
def initialize_copy(other)
|
142
|
+
@_state = nil
|
143
|
+
end
|
144
|
+
|
145
|
+
#
|
146
|
+
# Returns the initial state value:
|
147
|
+
# * Octets 1 - 6: MAC address
|
148
|
+
# * Octets 7 - 8: Random identifier
|
149
|
+
# * Octets 9 - 12: Zeroed out counter
|
150
|
+
#
|
151
|
+
def init_state
|
152
|
+
[ mac_address, SecureRandom.hex(2), COUNTER_INITIAL_VALUE ]
|
153
|
+
end
|
154
|
+
|
155
|
+
#
|
156
|
+
# Loads the state from the state file, reserving
|
157
|
+
# `COUNTER_BATCH_SIZE` nonces in the state file between
|
158
|
+
# invocations.
|
159
|
+
#
|
160
|
+
def load_state
|
161
|
+
open_state_file do |io|
|
162
|
+
bytes = io.read
|
163
|
+
state =
|
164
|
+
bytes.bytesize == 12 ? bump_state(bytes.unpack(PACK_FORMAT)) :
|
165
|
+
bytes.bytesize == 0 ? init_state :
|
166
|
+
nil
|
167
|
+
|
168
|
+
_verify_nonce_state(state)
|
169
|
+
_verify_nonce_mac(state)
|
170
|
+
|
171
|
+
# set the (dummmy) fourth field to the maximum counter in the batch
|
172
|
+
state[3] = bump_counter(state[2], COUNTER_BATCH_SIZE)
|
173
|
+
|
174
|
+
# write out the current state, using the maximum batch counter
|
175
|
+
# instead of the counter's current value
|
176
|
+
output = (state[0..1] << state[3]).pack(PACK_FORMAT)
|
177
|
+
|
178
|
+
io.rewind
|
179
|
+
io.write output
|
180
|
+
|
181
|
+
state
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
#
|
186
|
+
# Bumps the state provided to the next increment. Ensures that we
|
187
|
+
# haven't exceeded the maximum state value allowed by the nonce
|
188
|
+
# format.
|
189
|
+
#
|
190
|
+
def bump_state(state)
|
191
|
+
_verify_nonce_below_maximum_value(state)
|
192
|
+
|
193
|
+
state[2] = bump_counter state[2], 1
|
194
|
+
state
|
195
|
+
end
|
196
|
+
|
197
|
+
#
|
198
|
+
# Increments the provided byte-string counter.
|
199
|
+
#
|
200
|
+
def bump_counter(counter, increment)
|
201
|
+
"%08x" % (counter.hex + increment)
|
202
|
+
end
|
203
|
+
|
204
|
+
private
|
205
|
+
|
206
|
+
def open_state_file
|
207
|
+
self.state_file.open(File::RDWR) do |io|
|
208
|
+
begin
|
209
|
+
io.flock File::LOCK_EX
|
210
|
+
yield io
|
211
|
+
ensure
|
212
|
+
io.flush
|
213
|
+
io.flock File::LOCK_UN
|
214
|
+
end
|
215
|
+
end
|
216
|
+
rescue Errno::ENOENT
|
217
|
+
Kernel.warn <<-ALERT
|
218
|
+
======================================================================
|
219
|
+
WARNING:
|
220
|
+
Nonce state file does not exist and will be automatically generated
|
221
|
+
for you. If this is _not_ your first time running this program,
|
222
|
+
please ensure that the state file, located at
|
223
|
+
|
224
|
+
#{self.state_file.to_s}
|
225
|
+
|
226
|
+
is never deleted or otherwise removed. Its presence is crucial to
|
227
|
+
this application's cryptographic security.
|
228
|
+
======================================================================
|
229
|
+
ALERT
|
230
|
+
self.state_file.open(File::CREAT, 0600) { }
|
231
|
+
retry
|
232
|
+
end
|
233
|
+
|
234
|
+
def mac_address
|
235
|
+
mac_address_real or mac_address_pseudo
|
236
|
+
end
|
237
|
+
|
238
|
+
def mac_address_real
|
239
|
+
mac_addresses_real.first
|
240
|
+
end
|
241
|
+
|
242
|
+
def mac_addresses_real
|
243
|
+
Mac.addr.list.map {|addr| addr.tr(':-', '') } rescue []
|
244
|
+
end
|
245
|
+
|
246
|
+
def mac_address_pseudo
|
247
|
+
(SecureRandom.hex(48 / 8).hex | MAC_MULTICAST_MASK).to_s(16)
|
248
|
+
end
|
249
|
+
|
250
|
+
def _verify_nonce_state(state)
|
251
|
+
return if state
|
252
|
+
|
253
|
+
raise ArgumentError,
|
254
|
+
"nonce state file corrupt; MANUAL REPAIR REQUIRED, DO NOT RM"
|
255
|
+
end
|
256
|
+
|
257
|
+
def _verify_nonce_mac(state)
|
258
|
+
return if
|
259
|
+
mac_addresses_real.include?(state.first) or
|
260
|
+
state.first.hex & MAC_MULTICAST_MASK != 0
|
261
|
+
|
262
|
+
raise ArgumentError,
|
263
|
+
"nonce state file must not be copied from another machine"
|
264
|
+
end
|
265
|
+
|
266
|
+
def _verify_nonce_below_maximum_value(state)
|
267
|
+
raise ArgumentError, "nonce counter has reached maximum value" if
|
268
|
+
state[2].hex > COUNTER_MAXIMUM_VALUE.hex
|
269
|
+
end
|
270
|
+
end
|
File without changes
|