Skip to content

Commit

Permalink
init commit
Browse files Browse the repository at this point in the history
  • Loading branch information
stakach committed Dec 14, 2019
0 parents commit 00240ce
Show file tree
Hide file tree
Showing 9 changed files with 362 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
doc
lib
.crystal
.shards
app
*.dwarf
.DS_Store
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
language: crystal
install:
- shards install
script:
- crystal spec
- bin/ameba
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2019 ACA Projects

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Crystal Lang CMAC

[![Build Status](https://travis-ci.org/spider-gazelle/cmac.svg?branch=master)](https://travis-ci.org/spider-gazelle/cmac)

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

```crystal
key = Random.new.random_bytes(16)
message = 'attack at dawn'
cmac = CMAC.new(key)
cmac.sign(message)
=> Bytes[246, 184, 193, 76, 93, 115, 191, 26, 135, 60, 164, 161, 90, 224, 102, 170]
```

Once you've obtained the signature (also called a tag) of a message you can use CMAC to verify it as well.

```crystal
tag = Bytes[246, 184, 193, 76, 93, 115, 191, 26, 135, 60, 164, 161, 90, 224, 102, 170]
cmac.valid_message?(tag, message)
=> true
cmac.valid_message?(tag, 'attack at dusk')
=> false
```

CMAC can also be used with a variable length input key as described in RFC4615.

```crystal
key = 'setec astronomy'
message = 'attack at dawn'
cmac = CMAC.new(key)
cmac.sign(message)
=> Bytes[92, 17, 144, 230, 145, 178, 196, 130, 96, 144, 166, 236, 58, 14, 28, 243]
```
6 changes: 6 additions & 0 deletions shard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
name: cmac
version: 1.0.0

development_dependencies:
ameba:
github: veelenga/ameba
54 changes: 54 additions & 0 deletions spec/cmac_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require "./spec_helper"

describe CMAC do
describe "sign" do
test_vectors.each do |options|
it "matches the \"#{options[:name]}\" test vector" do
cmac = CMAC.new(options[:key])
input = options[:message].clone
output = cmac.sign(options[:message], options[:truncate])
output.should eq(options[:tag])

# Ensure memory not modified
input.should eq(options[:message])
end
end

it "gives a truncated output if requested" do
cmac = CMAC.new(TEST_KEY)
output = cmac.sign("attack at dawn", 12)
output.size.should eq(12)
end

it "raises error if truncation request is greater than 16 bytes" do
cmac = CMAC.new(TEST_KEY)
expect_raises(CMAC::Error, "Tag cannot be greater than maximum (16 bytes)") do
cmac.sign("attack at dawn", 17)
end
end

it "raises error if truncation request is less than 8 bytes" do
cmac = CMAC.new(TEST_KEY)
expect_raises(CMAC::Error, "Tag cannot be less than minimum (8 bytes)") do
cmac.sign("attack at dawn", 7)
end
end
end

describe "valid_message?" do
it "is true for matching messages" do
message = "attack at dawn"
cmac = CMAC.new(TEST_KEY)
tag = cmac.sign(message)
result = cmac.valid_message?(tag, message)
result.should be_truthy
end

it "is false for modified messages" do
cmac = CMAC.new(TEST_KEY)
tag = cmac.sign("attack at dawn")
result = cmac.valid_message?(tag, "attack at dusk")
result.should be_falsey
end
end
end
25 changes: 25 additions & 0 deletions spec/spec_helper.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
require "spec"
require "../src/cmac"

TEST_KEY = Bytes[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

def test_vectors
test_lines = File.read_lines("./spec/test_vectors.txt").map(&.strip).reject(&.empty?)
test_lines.each_slice(5).map do |lines|
name = lines.shift
data = {} of String => Bytes
[lines[0], lines[1], lines[3]].each do |line|
key, value = line.split('=').map(&.strip)
data[key.downcase] = value.size > 2 ? value[2..-1].hexbytes : Bytes.new(0)
end
truncate = lines[2].split('=').map(&.strip)[1].to_i

{
name: name,
key: data["key"],
message: data["message"],
tag: data["tag"],
truncate: truncate,
}
end.to_a
end
65 changes: 65 additions & 0 deletions spec/test_vectors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
Empty Message, no truncation
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
Message =
Truncate = 16
Tag = 0xbb1d6929e95937287fa37d129b756746

Message of 16 bytes, no truncation - f
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
Message = 0x6bc1bee22e409f96e93d7e117393172a
Truncate = 16
Tag = 0x070a16b46b4d4144f79bdd9dd04a287c

Message of 40 bytes, no truncation
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
Message = 0x6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411
Truncate = 16
Tag = 0xdfa66747de9ae63030ca32611497c827

Message of 64 bytes, no truncation - f
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
Message = 0x6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710
Truncate = 16
Tag = 0x51f0bebf7e3b9d92fc49741779363cfe

Empty message, 12 byte truncation
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
Message =
Truncate = 12
Tag = 0xbb1d6929e95937287fa37d12

Message of 16 bytes, 12 byte truncation - f
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
Message = 0x6bc1bee22e409f96e93d7e117393172a
Truncate = 12
Tag = 0x070a16b46b4d4144f79bdd9d

Message of 40 bytes, 12 byte truncation
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
Message = 0x6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411
Truncate = 12
Tag = 0xdfa66747de9ae63030ca3261

Message of 64 bytes, 12 byte truncation - f
Key = 0x2b7e151628aed2a6abf7158809cf4f3c
Message = 0x6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710
Truncate = 12
Tag = 0x51f0bebf7e3b9d92fc497417

Test Case AES-CMAC-PRF-128 with 20-octet input, 18 byte key
Key = 0x000102030405060708090a0b0c0d0e0fedcb
Message = 0x000102030405060708090a0b0c0d0e0f10111213
Truncate = 16
Tag = 0x84a348a4a45d235babfffc0d2b4da09a

Test Case AES-CMAC-PRF-128 with 20-octet input, 16 byte key
Key = 0x000102030405060708090a0b0c0d0e0f
Message = 0x000102030405060708090a0b0c0d0e0f10111213
Truncate = 16
Tag = 0x980ae87b5f4c9c5214f5b6a8455e4c2d

Test Case AES-CMAC-PRF-128 with 20-octet input, 10 byte key
Key = 0x00010203040506070809
Message = 0x000102030405060708090a0b0c0d0e0f10111213
Truncate = 16
Tag = 0x290d9e112edb09ee141fcf64c0b72f3d
145 changes: 145 additions & 0 deletions src/cmac.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
require "openssl"

class CMAC
class Error < Exception; end

ZERO_BLOCK = Bytes.new(16)
CONSTANT_BLOCK = Bytes[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x87]

@key : Bytes
@key1 : Bytes
@key2 : Bytes

def initialize(key)
@key = _derive_key(key.to_slice)
@key1, @key2 = _generate_subkeys(@key)
end

def sign(message, truncate = 16) : Bytes
raise Error.new("Tag cannot be greater than maximum (16 bytes)") if truncate > 16
raise Error.new("Tag cannot be less than minimum (8 bytes)") if truncate < 8

message = message.to_slice

if _needs_padding?(message)
message = _pad_message(message)
final_block = @key2
else
final_block = @key1
end

last_ciphertext = ZERO_BLOCK
count = message.size // 16
range = Range.new(0, count - 1)

blocks = range.map do |i|
starting = 16 * i
ending = starting + 16
message[starting...ending]
end

blocks.each_with_index do |block, i|
block = _xor(final_block, block) if i == range.end
block = _xor(block, last_ciphertext)
last_ciphertext = _encrypt_block(@key, block)
end

last_ciphertext[0...truncate]
end

def valid_message?(tag, message) : Bool
other_tag = sign(message)
_secure_compare?(tag, other_tag)
end

def _derive_key(key : Bytes) : Bytes
if key.size == 16
key
else
cmac = CMAC.new(ZERO_BLOCK)
cmac.sign(key)
end
end

def _encrypt_block(key : Bytes, block : Bytes) : Bytes
cipher = OpenSSL::Cipher.new("AES-128-ECB")
cipher.encrypt
cipher.padding = false
cipher.key = key

encrypted_data = IO::Memory.new
encrypted_data.write(cipher.update(block))
encrypted_data.write(cipher.final)
encrypted_data.to_slice
end

def _generate_subkeys(key : Bytes)
key0 = _encrypt_block(key, ZERO_BLOCK)
key1 = _next_key(key0)
key2 = _next_key(key1.clone)
{key1, key2}
end

def _needs_padding?(message : Bytes) : Bool
(message.size == 0) || (message.size % 16 != 0)
end

def _next_key(key : Bytes) : Bytes
if key[0] < 0x80
_leftshift(key)
else
_xor(_leftshift(key), CONSTANT_BLOCK)
end
end

def _leftshift(input : Bytes) : Bytes
io = IO::Memory.new(input)

words = Slice(UInt32).new(4)
4.times { |i| words[i] = io.read_bytes(UInt32, IO::ByteFormat::BigEndian) }
words.reverse!

overflow = 0_u32
words.map! do |word|
new_word = word << 1
new_word |= overflow
overflow = (word & 0x80000000_u32) >= 0x80000000_u32 ? 1_u32 : 0_u32
new_word
end

io.rewind
words.reverse!
words.each { |word| io.write_bytes(word, IO::ByteFormat::BigEndian) }
io.to_slice
end

def _pad_message(message : Bytes) : Bytes
padded_length = message.size + 16 - (message.size % 16)
ljust = padded_length - (message.size + 1)

io = IO::Memory.new
io.write message
io.write_byte 0x80_u8
io.write(Bytes.new(ljust)) if ljust > 0
io.to_slice
end

def _secure_compare?(a : Bytes, b : Bytes) : Bool
return false unless a.size == b.size

result = 0
b.each_with_index do |byte, i|
result |= byte ^ a[i]
end
result == 0
end

def _xor(a : Bytes, b : Bytes) : Bytes
io = IO::Memory.new
length = {a.size, b.size}.min
length.times do |i|
io.write_byte(a[i] ^ b[i])
end
io.to_slice
end
end

0 comments on commit 00240ce

Please sign in to comment.