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.
@@ -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
@@ -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