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.
@@ -0,0 +1,35 @@
1
+ # CMAC [![Build Status](https://secure.travis-ci.org/jtdowney/cmac.png?branch=master)](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).
@@ -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
@@ -0,0 +1,4 @@
1
+ class CMAC
2
+ class Exception < StandardError
3
+ end
4
+ end
@@ -0,0 +1,3 @@
1
+ class CMAC
2
+ VERSION = '0.1.0'
3
+ end
@@ -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
@@ -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