aead 1.3.0

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