cmac 0.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.
- data/README.md +35 -0
- data/lib/cmac.rb +127 -0
- data/lib/cmac/exception.rb +4 -0
- data/lib/cmac/version.rb +3 -0
- data/spec/cmac_spec.rb +46 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/test_vectors.txt +65 -0
- metadata +94 -0
data/README.md
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# CMAC [](https://travis-ci.org/jtdowney/cmac)
|
2
|
+
|
3
|
+
This gem is ruby implementation of the Cipher-based Message Authentication Code (CMAC) as defined in [RFC4493](http://tools.ietf.org/html/rfc4493), [RFC4494](http://tools.ietf.org/html/rfc4494), and [RFC4615](http://tools.ietf.org/html/rfc4615). Message authentication codes provide integrity protection of data given that two parties share a secret key.
|
4
|
+
|
5
|
+
```ruby
|
6
|
+
key = OpenSSL::Random.random_bytes(16)
|
7
|
+
message = 'attack at dawn'
|
8
|
+
cmac = CMAC.new(key)
|
9
|
+
cmac.sign(message)
|
10
|
+
=> "\xF6\xB8\xC1L]s\xBF\x1A\x87<\xA4\xA1Z\xE0f\xAA"
|
11
|
+
```
|
12
|
+
|
13
|
+
Once you've obtained the signature (also called a tag) of a message you can use CMAC to verify it as well.
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
tag = "\xF6\xB8\xC1L]s\xBF\x1A\x87<\xA4\xA1Z\xE0f\xAA"
|
17
|
+
cmac.valid_message?(tag, message)
|
18
|
+
=> true
|
19
|
+
cmac.valid_message?(tag, 'attack at dusk')
|
20
|
+
=> false
|
21
|
+
```
|
22
|
+
|
23
|
+
CMAC can also be used with a variable length input key as described in RFC4615.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
key = 'setec astronomy'
|
27
|
+
message = 'attack at dawn'
|
28
|
+
cmac = CMAC.new(key)
|
29
|
+
cmac.sign(message)
|
30
|
+
=> "\\\x11\x90\xE6\x91\xB2\xC4\x82`\x90\xA6\xEC:\x0E\x1C\xF3"
|
31
|
+
```
|
32
|
+
|
33
|
+
## License
|
34
|
+
|
35
|
+
The CMAC gem is released under the [MIT license](http://www.opensource.org/licenses/MIT).
|
data/lib/cmac.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
require 'cmac/exception'
|
4
|
+
require 'cmac/version'
|
5
|
+
|
6
|
+
class CMAC
|
7
|
+
ZeroBlock = "\0" * 16
|
8
|
+
ConstantBlock = ("\0" * 15) + "\x87"
|
9
|
+
|
10
|
+
def initialize(key)
|
11
|
+
key.force_encoding('BINARY') if key.respond_to?(:force_encoding)
|
12
|
+
@key = _derive_key(key)
|
13
|
+
@key1, @key2 = _generate_subkeys(@key)
|
14
|
+
end
|
15
|
+
|
16
|
+
def sign(message, truncate = 16)
|
17
|
+
raise CMAC::Exception.new('Tag cannot be greater than maximum (16 bytes)') if truncate > 16
|
18
|
+
raise CMAC::Exception.new('Tag cannot be less than minimum (8 bytes)') if truncate < 8
|
19
|
+
message.force_encoding('BINARY') if message.respond_to?(:force_encoding)
|
20
|
+
|
21
|
+
if _needs_padding?(message)
|
22
|
+
message = _pad_message(message)
|
23
|
+
final_block = @key2
|
24
|
+
else
|
25
|
+
final_block = @key1
|
26
|
+
end
|
27
|
+
|
28
|
+
last_ciphertext = ZeroBlock
|
29
|
+
count = message.length / 16
|
30
|
+
range = Range.new(0, count - 1)
|
31
|
+
blocks = range.map { |i| message.slice(16 * i, 16) }
|
32
|
+
blocks.each_with_index do |block, i|
|
33
|
+
if i == range.last
|
34
|
+
block = _xor(final_block, block)
|
35
|
+
end
|
36
|
+
|
37
|
+
block = _xor(block, last_ciphertext)
|
38
|
+
last_ciphertext = _encrypt_block(@key, block)
|
39
|
+
end
|
40
|
+
|
41
|
+
last_ciphertext.slice(0, truncate)
|
42
|
+
end
|
43
|
+
alias :encrypt :sign
|
44
|
+
|
45
|
+
def valid_message?(tag, message)
|
46
|
+
other_tag = sign(message)
|
47
|
+
_secure_compare?(tag, other_tag)
|
48
|
+
end
|
49
|
+
|
50
|
+
def _derive_key(key)
|
51
|
+
if key.length == 16
|
52
|
+
key
|
53
|
+
else
|
54
|
+
cmac = CMAC.new(ZeroBlock)
|
55
|
+
cmac.encrypt(key)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def _encrypt_block(key, block)
|
60
|
+
cipher = OpenSSL::Cipher.new('AES-128-ECB')
|
61
|
+
cipher.encrypt
|
62
|
+
cipher.padding = 0
|
63
|
+
cipher.key = key
|
64
|
+
cipher.update(block) + cipher.final
|
65
|
+
end
|
66
|
+
|
67
|
+
def _generate_subkeys(key)
|
68
|
+
key0 = _encrypt_block(key, ZeroBlock)
|
69
|
+
key1 = _next_key(key0)
|
70
|
+
key2 = _next_key(key1)
|
71
|
+
[key1, key2]
|
72
|
+
end
|
73
|
+
|
74
|
+
def _needs_padding?(message)
|
75
|
+
message.length == 0 || message.length % 16 != 0
|
76
|
+
end
|
77
|
+
|
78
|
+
def _next_key(key)
|
79
|
+
if key[0].ord < 0x80
|
80
|
+
_leftshift(key)
|
81
|
+
else
|
82
|
+
_xor(_leftshift(key), ConstantBlock)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def _leftshift(input)
|
87
|
+
overflow = 0
|
88
|
+
words = input.unpack('N4').reverse
|
89
|
+
words = words.map do |word|
|
90
|
+
new_word = (word << 1) & 0xFFFFFFFF
|
91
|
+
new_word |= overflow
|
92
|
+
overflow = (word & 0x80000000) >= 0x80000000 ? 1 : 0
|
93
|
+
new_word
|
94
|
+
end
|
95
|
+
words.reverse.pack('N4')
|
96
|
+
end
|
97
|
+
|
98
|
+
def _pad_message(message)
|
99
|
+
padded_length = message.length + 16 - (message.length % 16)
|
100
|
+
message = message + "\x80"
|
101
|
+
message.ljust(padded_length, "\0")
|
102
|
+
end
|
103
|
+
|
104
|
+
def _secure_compare?(a, b)
|
105
|
+
return false unless a.bytesize == b.bytesize
|
106
|
+
|
107
|
+
bytes = a.unpack("C#{a.bytesize}")
|
108
|
+
|
109
|
+
result = 0
|
110
|
+
b.each_byte do |byte|
|
111
|
+
result |= byte ^ bytes.shift
|
112
|
+
end
|
113
|
+
result == 0
|
114
|
+
end
|
115
|
+
|
116
|
+
def _xor(a, b)
|
117
|
+
a.force_encoding('BINARY') if a.respond_to?(:force_encoding)
|
118
|
+
b.force_encoding('BINARY') if b.respond_to?(:force_encoding)
|
119
|
+
|
120
|
+
output = ''
|
121
|
+
length = [a.length, b.length].min
|
122
|
+
length.times do |i|
|
123
|
+
output << (a[i].ord ^ b[i].ord).chr
|
124
|
+
end
|
125
|
+
output
|
126
|
+
end
|
127
|
+
end
|
data/lib/cmac/version.rb
ADDED
data/spec/cmac_spec.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe CMAC do
|
4
|
+
describe 'sign' do
|
5
|
+
test_vectors.each do |name, options|
|
6
|
+
it "should match the \"#{name}\" test vector" do
|
7
|
+
cmac = CMAC.new(options[:Key])
|
8
|
+
cmac.sign(options[:Message], options[:Truncate].to_i).should == options[:Tag]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should give a truncated output if requested' do
|
13
|
+
cmac = CMAC.new(TestKey)
|
14
|
+
cmac.sign('attack at dawn', 12).length.should == 12
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should raise error if truncation request is greater than 16 bytes' do
|
18
|
+
cmac = CMAC.new(TestKey)
|
19
|
+
expect do
|
20
|
+
cmac.sign('attack at dawn', 17)
|
21
|
+
end.to raise_error(CMAC::Exception, 'Tag cannot be greater than maximum (16 bytes)')
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should raise error if truncation request is less than 8 bytes' do
|
25
|
+
cmac = CMAC.new(TestKey)
|
26
|
+
expect do
|
27
|
+
cmac.sign('attack at dawn', 7)
|
28
|
+
end.to raise_error(CMAC::Exception, 'Tag cannot be less than minimum (8 bytes)')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe 'valid_message?' do
|
33
|
+
it 'should be true for matching messages' do
|
34
|
+
message = 'attack at dawn'
|
35
|
+
cmac = CMAC.new(TestKey)
|
36
|
+
tag = cmac.sign(message)
|
37
|
+
cmac.should be_valid_message(tag, message)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should be false for modified messages' do
|
41
|
+
cmac = CMAC.new(TestKey)
|
42
|
+
tag = cmac.sign('attack at dawn')
|
43
|
+
cmac.should_not be_valid_message(tag, 'attack at dusk')
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'cmac'
|
2
|
+
|
3
|
+
TestKey = "\x01" * 16
|
4
|
+
|
5
|
+
RSpec.configure do |config|
|
6
|
+
config.order = 'random'
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_vectors
|
10
|
+
test_file = File.expand_path('../test_vectors.txt', __FILE__)
|
11
|
+
test_lines = File.readlines(test_file).map(&:strip).reject(&:empty?)
|
12
|
+
|
13
|
+
vectors = {}
|
14
|
+
test_lines.each_slice(5) do |lines|
|
15
|
+
name = lines.shift
|
16
|
+
values = lines.inject({}) do |hash, line|
|
17
|
+
key, value = line.split('=').map(&:strip)
|
18
|
+
value = '' unless value
|
19
|
+
value = [value.slice(2..-1)].pack('H*') if value.start_with?('0x')
|
20
|
+
hash[key.to_sym] = value
|
21
|
+
hash
|
22
|
+
end
|
23
|
+
vectors[name] = values
|
24
|
+
end
|
25
|
+
vectors
|
26
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
Empty Message, no truncation
|
2
|
+
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
|
3
|
+
Message =
|
4
|
+
Truncate = 16
|
5
|
+
Tag = 0xbb1d6929e95937287fa37d129b756746
|
6
|
+
|
7
|
+
Message of 16 bytes, no truncation
|
8
|
+
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
|
9
|
+
Message = 0x6bc1bee22e409f96e93d7e117393172a
|
10
|
+
Truncate = 16
|
11
|
+
Tag = 0x070a16b46b4d4144f79bdd9dd04a287c
|
12
|
+
|
13
|
+
Message of 40 bytes, no truncation
|
14
|
+
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
|
15
|
+
Message = 0x6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411
|
16
|
+
Truncate = 16
|
17
|
+
Tag = 0xdfa66747de9ae63030ca32611497c827
|
18
|
+
|
19
|
+
Message of 64 bytes, no truncation
|
20
|
+
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
|
21
|
+
Message = 0x6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710
|
22
|
+
Truncate = 16
|
23
|
+
Tag = 0x51f0bebf7e3b9d92fc49741779363cfe
|
24
|
+
|
25
|
+
Empty message, 12 byte truncation
|
26
|
+
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
|
27
|
+
Message =
|
28
|
+
Truncate = 12
|
29
|
+
Tag = 0xbb1d6929e95937287fa37d12
|
30
|
+
|
31
|
+
Message of 16 bytes, 12 byte truncation
|
32
|
+
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
|
33
|
+
Message = 0x6bc1bee22e409f96e93d7e117393172a
|
34
|
+
Truncate = 12
|
35
|
+
Tag = 0x070a16b46b4d4144f79bdd9d
|
36
|
+
|
37
|
+
Message of 40 bytes, 12 byte truncation
|
38
|
+
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
|
39
|
+
Message = 0x6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411
|
40
|
+
Truncate = 12
|
41
|
+
Tag = 0xdfa66747de9ae63030ca3261
|
42
|
+
|
43
|
+
Message of 64 bytes, 12 byte truncation
|
44
|
+
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
|
45
|
+
Message = 0x6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710
|
46
|
+
Truncate = 12
|
47
|
+
Tag = 0x51f0bebf7e3b9d92fc497417
|
48
|
+
|
49
|
+
Test Case AES-CMAC-PRF-128 with 20-octet input, 18 byte key
|
50
|
+
Key = 0x000102030405060708090a0b0c0d0e0fedcb
|
51
|
+
Message = 0x000102030405060708090a0b0c0d0e0f10111213
|
52
|
+
Truncate = 16
|
53
|
+
Tag = 0x84a348a4a45d235babfffc0d2b4da09a
|
54
|
+
|
55
|
+
Test Case AES-CMAC-PRF-128 with 20-octet input, 16 byte key
|
56
|
+
Key = 0x000102030405060708090a0b0c0d0e0f
|
57
|
+
Message = 0x000102030405060708090a0b0c0d0e0f10111213
|
58
|
+
Truncate = 16
|
59
|
+
Tag = 0x980ae87b5f4c9c5214f5b6a8455e4c2d
|
60
|
+
|
61
|
+
Test Case AES-CMAC-PRF-128 with 20-octet input, 10 byte key
|
62
|
+
Key = 0x00010203040506070809
|
63
|
+
Message = 0x000102030405060708090a0b0c0d0e0f10111213
|
64
|
+
Truncate = 16
|
65
|
+
Tag = 0x290d9e112edb09ee141fcf64c0b72f3d
|
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cmac
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- John Downey
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-23 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rake
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 2.12.0
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 2.12.0
|
46
|
+
description: A ruby implementation of RFC4493, RFC4494, and RFC4615. CMAC is a message
|
47
|
+
authentication code (MAC) built using AES-128.
|
48
|
+
email:
|
49
|
+
- jdowney@gmail.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- lib/cmac/exception.rb
|
55
|
+
- lib/cmac/version.rb
|
56
|
+
- lib/cmac.rb
|
57
|
+
- spec/cmac_spec.rb
|
58
|
+
- spec/spec_helper.rb
|
59
|
+
- spec/test_vectors.txt
|
60
|
+
- README.md
|
61
|
+
homepage: https://github.com/jtdowney/cmac
|
62
|
+
licenses: []
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options: []
|
65
|
+
require_paths:
|
66
|
+
- lib
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
68
|
+
none: false
|
69
|
+
requirements:
|
70
|
+
- - ! '>='
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: '0'
|
73
|
+
segments:
|
74
|
+
- 0
|
75
|
+
hash: -708155467333762105
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ! '>='
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
segments:
|
83
|
+
- 0
|
84
|
+
hash: -708155467333762105
|
85
|
+
requirements: []
|
86
|
+
rubyforge_project:
|
87
|
+
rubygems_version: 1.8.23
|
88
|
+
signing_key:
|
89
|
+
specification_version: 3
|
90
|
+
summary: Cipher-based Message Authentication Code
|
91
|
+
test_files:
|
92
|
+
- spec/cmac_spec.rb
|
93
|
+
- spec/spec_helper.rb
|
94
|
+
- spec/test_vectors.txt
|