krypt-cmac 1.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 625c065a66091f29a4ba6f4c362021440776de5c970f0bab96acf746c4b4cee5
4
+ data.tar.gz: 955abe821538bf06e71ebe8bef363693acaac7ecec8654f2673ec6472e0b69dc
5
+ SHA512:
6
+ metadata.gz: 8354e0d2338081a7600f14d977c534ea25c4c67ba392ff4eaa28aae1b4229a9a291c6f3de071ca71472679b58fff54f1476054be35ada47b304c268e1ccc54fa
7
+ data.tar.gz: 5ca01c08aa99204e1b509db1298024ddc5dd0b6d1fea419a72a58b17f785d389454bdd94e3ad08da02db69977f06a7ef43e4443cce52e73e252f45d85f87e474
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Martin Boßlet
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # Krypt::Cmac
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/krypt-cmac.svg)](https://badge.fury.io/rb/krypt-cmac)
4
+
5
+ First off, don't use CMAC unless you really need to. HMAC is usually faster, more robust, and easier to use.
6
+ Only go for CMAC if it's already been decided and you need to work with it.
7
+
8
+ Krypt::Cmac provides implementations for all versions of the AES-CMAC algorithm as specified in:
9
+
10
+ - [NIST SP 800-38B](https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38b.pdf)
11
+ - [RFC 4493](https://tools.ietf.org/html/rfc4493)
12
+ - [RFC 4494](https://tools.ietf.org/html/rfc4494)
13
+ - [RFC 4615](https://tools.ietf.org/html/rfc4615)
14
+
15
+ It supports 128, 192, and 256-bit keys, variable length keys, and can handle streaming processing.
16
+ Only AES is supported as the underlying block cipher algorithm. The implementations offer the same
17
+ public API as `OpenSSL::HMAC` except for the `reset` method.
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ gem 'krypt-cmac'
25
+ ```
26
+
27
+ And then execute:
28
+
29
+ ```bash
30
+ bundle install
31
+ ```
32
+
33
+ If you aren't using `bundler` for managing dependencies, you may install the gem directly by executing:
34
+
35
+ ```bash
36
+ gem install krypt-cmac
37
+ ```
38
+
39
+ ## Usage
40
+
41
+ ### AES-CMAC with 128, 192 or 256 bits (NIST SP 800-38B, RFC 4493)
42
+
43
+ When using the default implementation, the size of the key determines the version of AES being used. This
44
+ implies that keys must be either 128, 192 or 256 bits long, resulting in AES-128-CMAC, AES-192-CMAC or
45
+ AES-256-CMAC tags. See below for variable key lengths.
46
+
47
+ ```ruby
48
+ require 'krypt/cmac'
49
+
50
+ key = ["2b7e151628aed2a6abf7158809cf4f3c"].pack("H*")
51
+ message = ["6bc1bee22e409f96e93d7e117393172a"].pack("H*")
52
+
53
+ # One-shot computation
54
+ cmac = Krypt::Cmac.new(key)
55
+ tag = cmac.digest(message)
56
+
57
+ # Streaming computation
58
+ cmac = Krypt::Cmac.new(key)
59
+ cmac.update(message)
60
+ tag = cmac.digest
61
+ ```
62
+
63
+ ### AES-CMAC-96 (RFC 4494)
64
+
65
+ To generate CMAC tags that are 96 bits long instead of the default 128 bits, use `Krypt::Cmac::Cmac96`.
66
+ Note that CMAC-96 tags are simply regular tags truncated to 96 bits. If you need any other tag size
67
+ below 128 bits, you can truncate the regular tag manually.
68
+
69
+ ```ruby
70
+ require 'krypt/cmac'
71
+
72
+ key = ["2b7e151628aed2a6abf7158809cf4f3c"].pack("H*")
73
+ message = ["6bc1bee22e409f96e93d7e117393172a"].pack("H*")
74
+
75
+ # AES-CMAC-96 one-shot computation
76
+ cmac = Krypt::Cmac::Cmac96.new(key)
77
+ tag = cmac.digest(message)
78
+
79
+ # AES-CMAC-96 streaming computation
80
+ cmac = Krypt::Cmac::Cmac96.new(key)
81
+ cmac.update(message)
82
+ tag = cmac.digest
83
+ ```
84
+
85
+ ### AES-CMAC-PRF-128 (RFC 4615)
86
+
87
+ If you need to generate CMAC tags from keys of varying lengths and not the usual 128, 192, or 256 bit
88
+ range, use AES-CMAC-PRF-128 as provided by `Krypt::Cmac::CmacPrf128`. It computes a regular AES-CMAC on the
89
+ key first and uses the 128 bit result as the actual key for CMAC computation. You might also use it if
90
+ you need an AES-128-CMAC tag for keys that are 192 or 256 bits long. Using regular AES-CMAC with such
91
+ keys would compute AES-192-CMAC and AES-256-CMAC tags respectively.
92
+
93
+ ```ruby
94
+ require 'krypt/cmac'
95
+
96
+ key = ["2b7e151628aed2a6abf7158809cf4f3c"].pack("H*")
97
+ message = ["6bc1bee22e409f96e93d7e117393172a"].pack("H*")
98
+
99
+ # AES-CMAC-PRF-128 one-shot computation
100
+ cmac = Krypt::Cmac::CmacPrf128.new(key)
101
+ tag = cmac.digest(message)
102
+
103
+ # AES-CMAC-PRF-128 streaming computation
104
+ cmac = Krypt::Cmac::CmacPrf128.new(key)
105
+ cmac.update(message)
106
+ tag = cmac.digest
107
+ ```
108
+
109
+ ### Tag verification
110
+
111
+ Verifying a given tag means recomputing it and then comparing the two. However, it is crucial for security reasons
112
+ to avoid comparing them with `==` or similar comparisons subject to
113
+ [short-circuiting](https://en.wikipedia.org/wiki/Short-circuit_evaluation). To securely verify a tag, use:
114
+
115
+ ```ruby
116
+ require 'krypt/cmac'
117
+
118
+ key = ["2b7e151628aed2a6abf7158809cf4f3c"].pack("H*")
119
+ message = ["6bc1bee22e409f96e93d7e117393172a"].pack("H*")
120
+ tag = ["070a16b46b4d4144f79bdd9dd04a287c"].pack("H*")
121
+
122
+ # Verifying a tag with data supplied to the method
123
+ cmac = Krypt::Cmac.new(key)
124
+ valid = cmac.verify(tag, message)
125
+
126
+ # Verifying a tag without data supplied to the method
127
+ cmac = Krypt::Cmac.new(key)
128
+ cmac.update(message)
129
+ begin
130
+ valid = cmac.verify(tag)
131
+ puts "Tag successfully verified"
132
+ rescue Krypt::Cmac::TagMismatchError => e
133
+ # tag invalid
134
+ end
135
+ ```
136
+
137
+ Even though the `verify` method returns `true` on successful verification, it still raises a
138
+ `Krypt::Cmac::TagMismatchError` on invalid tags. This ensures that invalid tags cannot go undetected if the
139
+ verifying code forgets to check for `true` explicitly.
140
+
141
+ ## Development
142
+
143
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can
144
+ also run `bin/console` for an interactive prompt that will allow you to experiment.
145
+
146
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the
147
+ version number in `lib/krypt/cmac/version.rb`, and then run `bundle exec rake release`, which will create a git tag for
148
+ the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
149
+
150
+ ## Contributing
151
+
152
+ Bug reports and pull requests are welcome on GitHub at https://github.com/krypt/krypt-cmac.
153
+
154
+ ## License
155
+
156
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,31 @@
1
+ module Krypt
2
+ class Cmac
3
+ # Implements AES-CMAC-96 as defined in RFC 4494. Computes a regular
4
+ # 128 bit tag and truncates it at 96 bit. All methods of Krypt::Cmac are available.
5
+ #
6
+ # @example
7
+ # key = ["2b7e151628aed2a6abf7158809cf4f3c"].pack("H*")
8
+ # message = ["6bc1bee22e409f96e93d7e117393172a"].pack("H*")
9
+ # prf = Krypt::Cmac::Cmac96.new(key)
10
+ # tag = prf.digest("data")
11
+ #
12
+ # @see Krypt::Cmac
13
+ #
14
+ # References:
15
+ # - AES-CMAC-96 RFC: https://tools.ietf.org/html/rfc4494
16
+ class Cmac96 < Cmac
17
+ def initialize(key)
18
+ super
19
+ end
20
+
21
+ # Returns the computed MAC tag as a 96-bit string as described in RFC 4494.
22
+ #
23
+ # @param data [String] The data to update the CMAC computation with before finalizing.
24
+ # If nil, the CMAC computation is finalized without updating with any data.
25
+ # @return [String] The computed MAC tag as a 96-bit string.
26
+ def digest(data = nil)
27
+ super.byteslice(0, 12)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,48 @@
1
+ module Krypt
2
+ class Cmac
3
+ # Implements AES-CMAC-PRF-128 as defined in RFC 4615. Variable length keys are supported,
4
+ # but if the key is not 128 bits long, it is derived using the AES-CMAC algorithm to enforce
5
+ # a 128 bit key and a CMAC computed with AES-128. All methods of Krypt::Cmac are available.
6
+ #
7
+ # @example
8
+ # key = ["0102030405"].pack("H*")
9
+ # prf = Krypt::Cmac::CmacPrf128.new(key)
10
+ # tag = prf.digest("data")
11
+ #
12
+ # @see Krypt::Cmac
13
+ #
14
+ # References:
15
+ # - AES-CMAC-PRF-128 RFC: https://tools.ietf.org/html/rfc4615
16
+ class CmacPrf128
17
+ def initialize(key)
18
+ @key = derive_key(key)
19
+ @cmac = Krypt::Cmac.new(@key)
20
+ end
21
+
22
+ # Delegates to the underlying Krypt::Cmac instance.
23
+ def method_missing(method, *args, &block)
24
+ if @cmac.respond_to?(method)
25
+ @cmac.send(method, *args, &block)
26
+ else
27
+ super
28
+ end
29
+ end
30
+
31
+ # Delegates to the underlying Krypt::Cmac instance.
32
+ def respond_to_missing?(method, include_private = false)
33
+ @cmac.respond_to?(method) || super
34
+ end
35
+
36
+ private
37
+
38
+ def derive_key(key)
39
+ # If the key is already 128 bits, return it as is
40
+ return key if key.bytesize == 16
41
+
42
+ # Otherwise, derive a 128-bit key using the AES-CMAC algorithm
43
+ cmac = Krypt::Cmac.new(Krypt::Cmac::BLOCK_OF_ZEROS)
44
+ cmac.digest(key)
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,10 @@
1
+ module Krypt
2
+ class Cmac
3
+ # Raised when the CMAC is in an invalid state for the operation, e.g. when calling `update` after `digest`
4
+ # or by supplying additional data to `digest` after finalization.
5
+ class InvalidStateError < StandardError; end
6
+
7
+ # Raised when MAC tag verification fails.
8
+ class TagMismatchError < StandardError; end
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ module Krypt
2
+ class Cmac
3
+ VERSION = "1.1.0"
4
+ end
5
+ end
data/lib/krypt/cmac.rb ADDED
@@ -0,0 +1,245 @@
1
+ require "openssl"
2
+ require_relative "cmac/version"
3
+ require_relative "cmac/errors"
4
+ require_relative "cmac/cmac_96"
5
+ require_relative "cmac/cmac_prf_128"
6
+
7
+ module Krypt
8
+ # Based on the CMAC algorithm described in RFC 4493 and NIST SP 800-38B.
9
+ # Calculates a message authentication code (MAC) for a message using AES as the block cipher.
10
+ # The underlying AES key size can be 128, 192, or 256 bits, this governs the
11
+ # version of AES being used to compute the MAC. The MAC size is always 128 bits.
12
+ #
13
+ # If a 96-bit MAC is required (as in RFC 4494), {#cmac_96} can be used. If other
14
+ # reduced versions are required, the MAC tag can be truncated manually after calling
15
+ # {#digest}.
16
+ #
17
+ # If variable length keys such as in AES-CMAC-PRF-128 (RFC 4615) must be supported,
18
+ # or if the MAC shall be computed with AES-128 - regardless of the key length,
19
+ # {Krypt::Cmac::CmacPrf128} can be used. This class derives a 128-bit key from the
20
+ # given key using the AES-CMAC algorithm.
21
+ #
22
+ # The CMAC computation can be updated with data in multiple calls to {#update}
23
+ # or by using the << operator. The MAC tag is finalized by calling {#digest}.
24
+ # The computation can be updated with data before finalizing by passing the
25
+ # data as an argument to {#digest}, allowing for one-shot tag computation.
26
+ #
27
+ # @example
28
+ # key = ["2b7e151628aed2a6abf7158809cf4f3c"].pack("H*")
29
+ # message = ["6bc1bee22e409f96e93d7e117393172a"].pack("H*")
30
+ # message2 = ["ae2d8a57"].pack("H*")
31
+ #
32
+ # # One-shot computation
33
+ # cmac = Krypt::Cmac.new(key)
34
+ # tag = cmac.digest(message)
35
+ #
36
+ # # Streaming computation
37
+ # cmac = Krypt::Cmac.new(key)
38
+ # cmac.update(message) # Or: cmac << message
39
+ # cmac.update(message2)
40
+ # tag = cmac.digest
41
+ #
42
+ # # Streaming computation with chaining
43
+ # tag = Krypt::Cmac.new(key).update(message).update(message2).digest
44
+ #
45
+ # @see Krypt::Cmac::Cmac96
46
+ # @see Krypt::Cmac::CmacPrf128
47
+ #
48
+ # References:
49
+ # - NIST SP 800-38B: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38b.pdf
50
+ # - CMAC RFC: https://tools.ietf.org/html/rfc4493
51
+ # - AES-CMAC-96 RFC: https://tools.ietf.org/html/rfc4494
52
+ # - AES-CMAC-PRF-128 RFC: https://tools.ietf.org/html/rfc4615
53
+ class Cmac
54
+ # The constant string for subkey generation for a cipher with block size 128.
55
+ # Note that this is the same for different key lengths, as AES block size is always 128 bits.
56
+ RB = 0x87
57
+ # AES block size in bytes, which is always 128 (i.e. 16 bytes) regardless of key length.
58
+ AES_BLOCK_SIZE = 16
59
+ # A block of zeros, used for padding and initialization vectors.
60
+ BLOCK_OF_ZEROS = "\0".b * AES_BLOCK_SIZE
61
+
62
+ attr_reader :key, :k1, :k2, :l, :mac_tag
63
+
64
+ # Creates a new CMAC instance with the given key. The key length determines
65
+ # the version of AES to use for the CMAC computation.
66
+ #
67
+ # @param key [String] The key to use for the CMAC computation.
68
+ # The key must be 128, 192, or 256 bits long, selecting the AES version to use.
69
+ # @raise [ArgumentError] If the key length is not 128, 192, or 256 bits.
70
+ def initialize(key)
71
+ unless [16, 24, 32].include?(key.bytesize)
72
+ raise ArgumentError, "Key must be 128, 192, or 256 bits long"
73
+ end
74
+ @key = key.b
75
+ @k1, @k2, @l = generate_subkeys(@key)
76
+ @buffer = "".b
77
+ @aes_cbc = cipher_for_key(@key, :CBC)
78
+ @aes_cbc.iv = BLOCK_OF_ZEROS
79
+ end
80
+
81
+ # Updates the CMAC computation with the given data. The data can be any
82
+ # string, and the CMAC computation is updated with the data. The data can
83
+ # be given in multiple calls to update, and the CMAC computation is
84
+ # updated with each call.
85
+ #
86
+ # @param data [String] The data to update the CMAC computation with.
87
+ # @return [self] The CMAC instance itself, to allow chaining.
88
+ # @raise [InvalidStateError] If the CMAC has already been finalized.
89
+ def update(data)
90
+ return self if data.nil?
91
+ raise InvalidStateError.new("CMAC has already been finalized") if @mac_tag
92
+
93
+ @buffer << data
94
+ buffer_len = @buffer.bytesize
95
+
96
+ # Return early if the buffer does not contain enough data to form a block
97
+ return self if buffer_len <= AES_BLOCK_SIZE
98
+
99
+ # Ensure that we do not process the final block yet. The final block is
100
+ # processed in the digest method. This is to ensure that the final block
101
+ # is padded correctly. For now, just process the remaining full blocks.
102
+ remainder = buffer_len % AES_BLOCK_SIZE
103
+ remainder = AES_BLOCK_SIZE if remainder == 0
104
+ @aes_cbc.update(@buffer.slice!(0...-remainder))
105
+
106
+ self # Return self to allow chaining
107
+ end
108
+ # Allow the << operator to be used as an alias for update.
109
+ # This allows the CMAC computation to be updated with the << operator.
110
+ # (@see #update)
111
+ alias_method :<<, :update
112
+
113
+ # Finalizes the CMAC computation and returns the computed MAC tag. If data
114
+ # is given, the CMAC computation is updated with the data before finalizing.
115
+ # The CMAC computation is finalized after calling this method, and no further
116
+ # updates are allowed.
117
+ #
118
+ # @param data [String] The data to update the CMAC computation with before finalizing.
119
+ # If nil, the CMAC computation is finalized without updating with any data.
120
+ # @return [String] The computed MAC tag.
121
+ # @raise [ArgumentError] If data is given after the CMAC has already been finalized.
122
+ def digest(data = nil)
123
+ raise ArgumentError.new("CMAC has already been finalized") if data && @mac_tag
124
+ return @mac_tag if @mac_tag
125
+
126
+ update(data) if data
127
+ @mac_tag = @aes_cbc.update(pad_last_block(@buffer)) # No need to call final because no padding is used
128
+ end
129
+
130
+ # Returns the computed MAC tag as a hex-encoded string.
131
+ #
132
+ # @param data [String] The data to update the CMAC computation with before finalizing.
133
+ # If nil, the CMAC computation is finalized without updating with any data.
134
+ # @return [String] The computed MAC tag as a hex-encoded string.
135
+ def hexdigest(data = nil)
136
+ digest(data).unpack1("H*")
137
+ end
138
+
139
+ # Returns the computed MAC tag as a Base64-encoded string.
140
+ #
141
+ # @param data [String] The data to update the CMAC computation with before finalizing.
142
+ # If nil, the CMAC computation is finalized without updating with any data.
143
+ # @return [String] The computed MAC tag as a Base64-encoded string.
144
+ def base64digest(data = nil)
145
+ [digest(data)].pack("m0")
146
+ end
147
+
148
+ # Verifies the given MAC tag against the computed MAC tag for the given data.
149
+ #
150
+ # @param tag [String] The MAC tag to verify.
151
+ # @param data [String] The data to verify the MAC tag against.
152
+ # If nil, the MAC tag is verified against the current CMAC computation.
153
+ # @return [Boolean] True if the MAC tag is verified. Raises otherwise.
154
+ # @raise [TagMismatchError] If the MAC tag verification fails.
155
+ def verify(tag, data = nil)
156
+ if !secure_compare(tag, digest(data))
157
+ raise TagMismatchError.new("MAC tag verification failed, the tags do not match")
158
+ end
159
+ true # Needs not be checked because an error is raised if the tags do not match
160
+ end
161
+
162
+ # Compares the CMAC instance with another CMAC instance. The comparison is
163
+ # done by comparing the computed MAC tags. The comparison is done in constant
164
+ # time to prevent timing attacks.
165
+ #
166
+ # @param other [Krypt::Cmac] The other CMAC instance to compare with.
167
+ # @return [Boolean] True if the MAC tags are equal, false otherwise.
168
+ def ==(other)
169
+ return false unless Cmac === other
170
+ return false unless digest.bytesize == other.digest.bytesize
171
+ OpenSSL.fixed_length_secure_compare(digest, other.digest)
172
+ end
173
+
174
+ private
175
+
176
+ def cipher_for_key(key, mode)
177
+ algorithm = "AES-#{key.size * 8}-#{mode}"
178
+ OpenSSL::Cipher.new(algorithm).tap do |cipher|
179
+ cipher.encrypt
180
+ cipher.key = key
181
+ cipher.padding = 0 # Do not use padding
182
+ end
183
+ end
184
+
185
+ def generate_subkeys(key)
186
+ aes_ecb = cipher_for_key(key, :ECB)
187
+ l = aes_ecb.update(BLOCK_OF_ZEROS) # No need to call final because no padding is used
188
+ k1 = generate_subkey(l)
189
+ k2 = generate_subkey(k1)
190
+ [k1, k2, l]
191
+ end
192
+
193
+ def generate_subkey(bytes)
194
+ msb = most_significant_bit_set?(bytes)
195
+ k = shift_left(bytes)
196
+ k.setbyte(-1, k.getbyte(-1) ^ RB) if msb
197
+ k
198
+ end
199
+
200
+ def pad_last_block(buffer)
201
+ len = buffer.bytesize # 0 < len <= AES_BLOCK_SIZE
202
+
203
+ if len == AES_BLOCK_SIZE
204
+ xor_block(buffer, k1)
205
+ else
206
+ buffer << "\x80".b # Append 1 bit (0x80 in hex) with ASCII-8BIT encoding
207
+ buffer << "\x00".b * (AES_BLOCK_SIZE - (len + 1)) # Pad the rest with 0 bits
208
+ xor_block(buffer, k2)
209
+ end
210
+ end
211
+
212
+ def shift_left(byte_string)
213
+ bytes = byte_string.bytes
214
+ carry = 0
215
+
216
+ bytes.reverse_each.with_index do |byte, i|
217
+ new_carry = (byte & 0x80) >> 7
218
+ # Update bytes beginning from the end, so use negative index plus 1
219
+ # i=0 => bytes[-1], i=1 => bytes[-2], etc.
220
+ # Shift left, clear the LSB to ensure it is 0 with 0xFE (whose LSB is 0), and finally add carry
221
+ bytes[-(i + 1)] = ((byte << 1) & 0xFE) | carry
222
+ carry = new_carry
223
+ end
224
+
225
+ bytes.pack("C*") # Convert bytes back to a string
226
+ end
227
+
228
+ def most_significant_bit_set?(bytes)
229
+ most_significant_bit(bytes) != 0
230
+ end
231
+
232
+ def most_significant_bit(bytes)
233
+ bytes.unpack1("C") & 0x80
234
+ end
235
+
236
+ def xor_block(block, key)
237
+ block.bytes.each_with_index.map { |b, i| b ^ key.getbyte(i) }.pack("C*")
238
+ end
239
+
240
+ def secure_compare(a, b)
241
+ return false unless a.bytesize == b.bytesize
242
+ OpenSSL.fixed_length_secure_compare(a, b)
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,16 @@
1
+ RSpec.describe Krypt::Cmac::CmacPrf128 do
2
+ include_context "String helpers"
3
+ include_context "reference samples"
4
+
5
+ let(:data) { hex_to_bin(ReferenceSamples::AES_PRF_128[:data]) }
6
+
7
+ (ReferenceSamples::AES_PRF_128.keys - %i[data]).each do |sample_key|
8
+ sample = ReferenceSamples::AES_PRF_128[sample_key]
9
+ context sample[:description] do
10
+ let(:key) { hex_to_bin(sample[:key]) }
11
+ let(:expected_mac) { hex_to_bin(sample[:tag]) }
12
+
13
+ it_behaves_like "correct AES-CMAC-PRF-128"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ RSpec.describe "Krypt::Cmac with AES-128 key" do
2
+ include_context "String helpers"
3
+ include_context "reference samples"
4
+
5
+ let(:key) { hex_to_bin(ReferenceSamples::AES_128[:key]) }
6
+
7
+ ReferenceSamples.sample_keys.each do |sample_key|
8
+ sample = ReferenceSamples::AES_128[sample_key]
9
+ context sample[:description] do
10
+ let(:data) { hex_to_bin(sample[:data]) }
11
+ let(:expected_mac) { hex_to_bin(sample[:tag]) }
12
+
13
+ it_behaves_like "correct CMAC"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ RSpec.describe "Krypt::Cmac with AES-192 key" do
2
+ include_context "String helpers"
3
+ include_context "reference samples"
4
+
5
+ let(:key) { hex_to_bin(ReferenceSamples::AES_192[:key]) }
6
+
7
+ ReferenceSamples.sample_keys.each do |sample_key|
8
+ sample = ReferenceSamples::AES_192[sample_key]
9
+ context sample[:description] do
10
+ let(:data) { hex_to_bin(sample[:data]) }
11
+ let(:expected_mac) { hex_to_bin(sample[:tag]) }
12
+
13
+ it_behaves_like "correct CMAC"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ RSpec.describe "Krypt::Cmac with AES-256 key" do
2
+ include_context "String helpers"
3
+ include_context "reference samples"
4
+
5
+ let(:key) { hex_to_bin(ReferenceSamples::AES_256[:key]) }
6
+
7
+ ReferenceSamples.sample_keys.each do |sample_key|
8
+ sample = ReferenceSamples::AES_256[sample_key]
9
+ context sample[:description] do
10
+ let(:data) { hex_to_bin(sample[:data]) }
11
+ let(:expected_mac) { hex_to_bin(sample[:tag]) }
12
+
13
+ it_behaves_like "correct CMAC"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ RSpec.describe Krypt::Cmac::Cmac96 do
2
+ include_context "String helpers"
3
+ include_context "reference samples"
4
+
5
+ let(:key) { hex_to_bin(ReferenceSamples::AES_CMAC_96[:key]) }
6
+
7
+ ReferenceSamples.sample_keys.each do |sample_key|
8
+ sample = ReferenceSamples::AES_CMAC_96[sample_key]
9
+ context sample[:description] do
10
+ let(:data) { hex_to_bin(sample[:data]) }
11
+ let(:expected_mac) { hex_to_bin(sample[:tag]) }
12
+
13
+ it_behaves_like "correct AES-CMAC-96"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,82 @@
1
+ RSpec.describe Krypt::Cmac do
2
+ include_context "String helpers"
3
+
4
+ it "has a version number" do
5
+ expect(Krypt::Cmac::VERSION).not_to be nil
6
+ end
7
+
8
+ context "general usage" do
9
+ let(:sample) { ReferenceSamples::AES_128[:single_block] }
10
+ let(:key) { hex_to_bin(ReferenceSamples::AES_128[:key]) }
11
+ let(:data) { hex_to_bin(sample[:data]) }
12
+ let(:tag) { hex_to_bin(sample[:tag]) }
13
+
14
+ subject { Krypt::Cmac.new(key) }
15
+
16
+ it "initializes with a key" do
17
+ expect(subject.key).to eq(key)
18
+ end
19
+
20
+ it "updates with data and allows chaining" do
21
+ expect { subject.update(data).update(data) }.not_to raise_error
22
+ end
23
+
24
+ it "raises an error if update is called after digest" do
25
+ subject.digest(data)
26
+ expect { subject.update(data) }.to raise_error(Krypt::Cmac::InvalidStateError, "CMAC has already been finalized")
27
+ end
28
+
29
+ it "raises an error if digest is called with data after finalization" do
30
+ subject.digest(data)
31
+ expect { subject.digest(data) }.to raise_error(ArgumentError, "CMAC has already been finalized")
32
+ end
33
+
34
+ it "returns the computed MAC tag if digest is called multiple times" do
35
+ subject.update(data)
36
+ tag1 = subject.digest
37
+ tag2 = subject.digest
38
+ expect(tag1).to eq(tag2)
39
+ expect(tag1).to eq(tag)
40
+ end
41
+
42
+ it "returns the correct MAC tag when updating and calling digest without arguments" do
43
+ computed_tag = subject.update(data).digest
44
+ expect(computed_tag).to eq(tag)
45
+ end
46
+
47
+ it "returns the correct MAC tag when calling digest with the data as an argument" do
48
+ computed_tag = subject.digest(data)
49
+ expect(computed_tag).to eq(tag)
50
+ end
51
+ end
52
+
53
+ context "initialization with different key sizes" do
54
+ subject { Krypt::Cmac.new(key) }
55
+
56
+ shared_examples "correct key" do
57
+ it { expect(subject.key).to eq(key) }
58
+ end
59
+
60
+ context "128-bit key" do
61
+ let(:key) { hex_to_bin(ReferenceSamples::AES_128[:key]) }
62
+ it_behaves_like "correct key"
63
+ end
64
+
65
+ context "192-bit key" do
66
+ let(:key) { hex_to_bin(ReferenceSamples::AES_192[:key]) }
67
+ it_behaves_like "correct key"
68
+ end
69
+
70
+ context "256-bit key" do
71
+ let(:key) { hex_to_bin(ReferenceSamples::AES_256[:key]) }
72
+ it_behaves_like "correct key"
73
+ end
74
+
75
+ context "invalid key size" do
76
+ let(:key) { b64_to_bin("deadbeef") }
77
+ it "raises ArgumentError" do
78
+ expect { subject }.to raise_error(ArgumentError, "Key must be 128, 192, or 256 bits long")
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,73 @@
1
+ require "krypt/cmac"
2
+
3
+ RSpec.shared_context "reference samples" do
4
+ include_context "String helpers"
5
+
6
+ shared_examples "correct computation" do
7
+ subject {
8
+ described_class.new(key)
9
+ }
10
+
11
+ it { expect(subject.digest(data)).to eq(expected_mac) }
12
+
13
+ it "computes the correct digest when using update" do
14
+ expect(subject.update(data).digest).to eq(expected_mac)
15
+ end
16
+
17
+ it "computes the correct digest when using <<" do
18
+ subject << data
19
+ expect(subject.digest).to eq(expected_mac)
20
+ end
21
+
22
+ it "computes the correct hexdigest" do
23
+ expect(subject.hexdigest(data)).to eq(bin_to_hex(expected_mac))
24
+ end
25
+
26
+ it "computes the correct base64digest" do
27
+ expect(subject.base64digest(data)).to eq(bin_to_b64(expected_mac))
28
+ end
29
+
30
+ it "returns true when verifying the correct MAC" do
31
+ expect(subject.verify(expected_mac, data)).to be true
32
+ end
33
+
34
+ it "returns true when verifying the correct MAC without supplying data to `verify`" do
35
+ expect(subject.update(data).verify(expected_mac)).to be true
36
+ end
37
+
38
+ it "returns true when comparing two equal CMAC instances" do
39
+ expect(subject.update(data)).to eq(described_class.new(key).update(data))
40
+ end
41
+
42
+ it "returns false when comparing two different CMAC instances" do
43
+ expect(subject.update("nope")).not_to eq(described_class.new(key).update(data))
44
+ end
45
+
46
+ it "raises an error when verifying an incorrect MAC" do
47
+ expect { subject.verify(Krypt::Cmac::BLOCK_OF_ZEROS, data) }.to raise_error(Krypt::Cmac::TagMismatchError, "MAC tag verification failed, the tags do not match")
48
+ end
49
+
50
+ it "computes the correct MAC when updating the data in chunks" do
51
+ (1..(data.size)).each do |i|
52
+ mac = described_class.new(key)
53
+ data.chars.each_slice(i) { |slice| mac.update(slice.join) }
54
+ expect(mac.digest).to eq(expected_mac)
55
+ end
56
+ end
57
+ end
58
+
59
+ shared_examples "correct CMAC" do
60
+ let(:described_class) { Krypt::Cmac }
61
+ include_examples "correct computation"
62
+ end
63
+
64
+ shared_examples "correct AES-CMAC-96" do
65
+ let(:described_class) { Krypt::Cmac::Cmac96 }
66
+ include_examples "correct computation"
67
+ end
68
+
69
+ shared_examples "correct AES-CMAC-PRF-128" do
70
+ let(:described_class) { Krypt::Cmac::CmacPrf128 }
71
+ include_examples "correct computation"
72
+ end
73
+ end
@@ -0,0 +1,17 @@
1
+ RSpec.shared_context "String helpers" do
2
+ def hex_to_bin(hex)
3
+ [hex].pack("H*")
4
+ end
5
+
6
+ def bin_to_hex(bin)
7
+ bin.unpack1("H*")
8
+ end
9
+
10
+ def b64_to_bin(b64)
11
+ b64.unpack1("m")
12
+ end
13
+
14
+ def bin_to_b64(bin)
15
+ [bin].pack("m0")
16
+ end
17
+ end
@@ -0,0 +1,21 @@
1
+ require "simplecov"
2
+ SimpleCov.start
3
+
4
+ require "krypt/cmac"
5
+
6
+ RSpec.configure do |config|
7
+ # Include shared contexts
8
+ Dir[File.join(__dir__, "shared_contexts", "**", "*.rb")].each { |f| require f }
9
+ # Include all support files
10
+ Dir[File.join(__dir__, "support", "**", "*.rb")].each { |f| require f }
11
+
12
+ # Enable flags like --only-failures and --next-failure
13
+ config.example_status_persistence_file_path = ".rspec_status"
14
+
15
+ # Disable RSpec exposing methods globally on `Module` and `main`
16
+ config.disable_monkey_patching!
17
+
18
+ config.expect_with :rspec do |c|
19
+ c.syntax = :expect
20
+ end
21
+ end
@@ -0,0 +1,129 @@
1
+ require "krypt/cmac"
2
+
3
+ module ReferenceSamples
4
+ module_function
5
+
6
+ def sample_keys
7
+ @_keys ||= (AES_128.keys - %i[key])
8
+ end
9
+
10
+ # Taken from /res/AES_CMAC.pdf
11
+ AES_128 = {
12
+ key: "2b7e151628aed2a6abf7158809cf4f3c",
13
+ empty: {
14
+ description: "empty message (len 0)",
15
+ data: "",
16
+ tag: "bb1d6929e95937287fa37d129b756746"
17
+ },
18
+ single_block: {
19
+ description: "single block message (i.e. len 16)",
20
+ data: "6bc1bee22e409f96e93d7e117393172a",
21
+ tag: "070a16b46b4d4144f79bdd9dd04a287c"
22
+ },
23
+ non_multiple_block: {
24
+ description: "with a message that is not a multiple of the block size, e.g. len 20",
25
+ data: "6bc1bee22e409f96e93d7e117393172aae2d8a57",
26
+ tag: "7d85449ea6ea19c823a7bf78837dfade"
27
+ },
28
+ multiple_block: {
29
+ description: "with a message that is a multiple of the block size, e.g. len 64",
30
+ data: "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710",
31
+ tag: "51f0bebf7e3b9d92fc49741779363cfe"
32
+ }
33
+ }
34
+
35
+ # Taken from /res/AES_CMAC.pdf
36
+ AES_192 = {
37
+ key: "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b",
38
+ empty: {
39
+ description: "empty message (len 0)",
40
+ data: "",
41
+ tag: "d17ddf46adaacde531cac483de7a9367"
42
+ },
43
+ single_block: {
44
+ description: "single block message (i.e. len 16)",
45
+ data: "6bc1bee22e409f96e93d7e117393172a",
46
+ tag: "9e99a7bf31e710900662f65e617c5184"
47
+ },
48
+ non_multiple_block: {
49
+ description: "with a message that is not a multiple of the block size, e.g. len 20",
50
+ data: "6bc1bee22e409f96e93d7e117393172aae2d8a57",
51
+ tag: "3d75c194ed96070444a9fa7ec740ecf8"
52
+ },
53
+ multiple_block: {
54
+ description: "with a message that is a multiple of the block size, e.g. len 64",
55
+ data: "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710",
56
+ tag: "a1d5df0eed790f794d77589659f39a11"
57
+ }
58
+ }
59
+
60
+ # Taken from /res/AES_CMAC.pdf
61
+ AES_256 = {
62
+ key: "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4",
63
+ empty: {
64
+ description: "empty message (len 0)",
65
+ data: "",
66
+ tag: "028962f61b7bf89efc6b551f4667d983"
67
+ },
68
+ single_block: {
69
+ description: "single block message (i.e. len 16)",
70
+ data: "6bc1bee22e409f96e93d7e117393172a",
71
+ tag: "28a7023f452e8f82bd4bf28d8c37c35c"
72
+ },
73
+ non_multiple_block: {
74
+ description: "with a message that is not a multiple of the block size, e.g. len 20",
75
+ data: "6bc1bee22e409f96e93d7e117393172aae2d8a57",
76
+ tag: "156727dc0878944a023c1fe03bad6d93"
77
+ },
78
+ multiple_block: {
79
+ description: "with a message that is a multiple of the block size, e.g. len 64",
80
+ data: "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710",
81
+ tag: "e1992190549f6ed5696a2c056c315410"
82
+ }
83
+ }
84
+
85
+ # Taken from RFC 4494
86
+ AES_CMAC_96 = {
87
+ key: "2b7e151628aed2a6abf7158809cf4f3c",
88
+ empty: {
89
+ description: "empty message (len 0)",
90
+ data: "",
91
+ tag: "bb1d6929e95937287fa37d12"
92
+ },
93
+ single_block: {
94
+ description: "single block message (i.e. len 16)",
95
+ data: "6bc1bee22e409f96e93d7e117393172a",
96
+ tag: "070a16b46b4d4144f79bdd9d"
97
+ },
98
+ non_multiple_block: {
99
+ description: "with a message that is not a multiple of the block size, e.g. len 40",
100
+ data: "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411",
101
+ tag: "dfa66747de9ae63030ca3261"
102
+ },
103
+ multiple_block: {
104
+ description: "with a message that is a multiple of the block size, e.g. len 64",
105
+ data: "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710",
106
+ tag: "51f0bebf7e3b9d92fc497417"
107
+ }
108
+ }
109
+
110
+ # Taken from RFC 4615
111
+ AES_PRF_128 = {
112
+ data: "000102030405060708090a0b0c0d0e0f10111213",
113
+ key_length_18: {
114
+ key: "000102030405060708090a0b0c0d0e0fedcb",
115
+ description: "key length 18",
116
+ tag: "84a348a4a45d235babfffc0d2b4da09a"
117
+ },
118
+ key_length_16: {
119
+ key: "000102030405060708090a0b0c0d0e0f",
120
+ description: "key length 16",
121
+ tag: "980ae87b5f4c9c5214f5b6a8455e4c2d"
122
+ },
123
+ key_length_10: {
124
+ key: "00010203040506070809",
125
+ description: "key length 10",
126
+ tag: "290d9e112edb09ee141fcf64c0b72f3d"
127
+ }
128
+ }
129
+ end
metadata ADDED
@@ -0,0 +1,61 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: krypt-cmac
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Martin Boßlet
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-03-06 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |2
13
+ An implementation of AES-CMAC for 128, 192, and 256 bit keys as specified in NIST SP 800-38B and RFC 4493, capable of streaming processing.
14
+ Also included is an implementation of AES-CMAC-PRF-128 as specified in RFC 4615 and of CMAC-96 as specified in RFC 4494.
15
+ email:
16
+ - martin.bosslet@gmail.com
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - LICENSE
22
+ - README.md
23
+ - lib/krypt/cmac.rb
24
+ - lib/krypt/cmac/cmac_96.rb
25
+ - lib/krypt/cmac/cmac_prf_128.rb
26
+ - lib/krypt/cmac/errors.rb
27
+ - lib/krypt/cmac/version.rb
28
+ - spec/krypt/cmac/aes-cmac-prf-128_spec.rb
29
+ - spec/krypt/cmac/aes_128_spec.rb
30
+ - spec/krypt/cmac/aes_192_spec.rb
31
+ - spec/krypt/cmac/aes_256_spec.rb
32
+ - spec/krypt/cmac/aes_cmac_96_spec.rb
33
+ - spec/krypt/cmac/cmac_spec.rb
34
+ - spec/shared_contexts/reference_samples.rb
35
+ - spec/shared_contexts/string_helpers.rb
36
+ - spec/spec_helper.rb
37
+ - spec/support/reference_samples.rb
38
+ homepage: https://github.com/krypt/krypt-cmac
39
+ licenses:
40
+ - MIT
41
+ metadata:
42
+ homepage_uri: https://github.com/krypt/krypt-cmac
43
+ source_code_uri: https://github.com/krypt/krypt-cmac
44
+ rdoc_options: []
45
+ require_paths:
46
+ - lib
47
+ required_ruby_version: !ruby/object:Gem::Requirement
48
+ requirements:
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 3.0.0
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ requirements: []
58
+ rubygems_version: 3.6.2
59
+ specification_version: 4
60
+ summary: AES-CMAC as specified in NIST SP 800-38B, RFC 4493, RFC 4494 and RFC 4615.
61
+ test_files: []