Skip to content
This repository has been archived by the owner on Feb 5, 2021. It is now read-only.

Commit

Permalink
ruby: Merge jtdowney/aes_siv into the Ruby implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
tarcieri committed Jun 24, 2017
1 parent 091ed58 commit 2485bdc
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 322 deletions.
3 changes: 2 additions & 1 deletion lib/sivchain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

require "sivchain/version"

require "sivchain/cmac"
require "sivchain/aes/siv"
require "sivchain/aes/cmac"

# Advanced symmetric encryption using the AES-SIV (RFC 5297) and CHAIN constructions
module SIVChain
Expand Down
109 changes: 109 additions & 0 deletions lib/sivchain/aes/cmac.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
# frozen_string_literal: true

module SIVChain
# The Advanced Encryption Standard Block Cipher
module AES
# The AES-CMAC message authentication code
class CMAC
Exception = Class.new(StandardError)
ZeroBlock = ("\0" * 16).b.freeze
ConstantBlock = (("\0" * 15) + "\x87").b.freeze

def initialize(key)
case key.length
when 16
@cipher = OpenSSL::Cipher.new("AES-128-ECB")
when 32
@cipher = OpenSSL::Cipher.new("AES-256-ECB")
else raise ArgumentError, "key must be 16 or 32 bytes"
end

@cipher.encrypt
@cipher.padding = 0
@cipher.key = key
@key1, @key2 = _generate_subkeys
end

alias inspect to_s

def digest(message)
message = message.b

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

last_ciphertext = ZeroBlock
count = message.length / 16
range = Range.new(0, count - 1)
blocks = range.map { |i| message.slice(16 * i, 16) }
blocks.each_with_index do |block, i|
block = _xor(final_block, block) if i == range.last
block = _xor(block, last_ciphertext)
last_ciphertext = _encrypt_block(block)
end

last_ciphertext
end

private

def _encrypt_block(block)
@cipher.update(block) + @cipher.final
end

def _generate_subkeys
key0 = _encrypt_block(ZeroBlock)
key1 = _next_key(key0)
key2 = _next_key(key1)
[key1, key2]
end

def _needs_padding?(message)
message.empty? || message.length % 16 != 0
end

def _next_key(key)
if key[0].ord < 0x80
_leftshift(key)
else
_xor(_leftshift(key), ConstantBlock)
end
end

def _leftshift(input)
overflow = 0
words = input.unpack("N4").reverse
words = words.map do |word|
new_word = (word << 1) & 0xFFFFFFFF
new_word |= overflow
overflow = (word & 0x80000000) >= 0x80000000 ? 1 : 0
new_word
end
words.reverse.pack("N4")
end

def _pad_message(message)
padded_length = message.length + 16 - (message.length % 16)
message += "\x80".b
message.ljust(padded_length, "\0")
end

def _xor(a, b)
a = a.b
b = b.b

output = "".b
length = [a.length, b.length].min
length.times do |i|
output << (a[i].ord ^ b[i].ord).chr
end

output
end
end
end
end
138 changes: 138 additions & 0 deletions lib/sivchain/aes/siv.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# encoding: binary
# frozen_string_literal: true

module SIVChain
# The Advanced Encryption Standard Block Cipher
module AES
# The AES-SIV misuse resistant authenticated encryption cipher
class SIV
DOUBLE_CONSTANT = ("\x0" * 15) + "\x87"

def initialize(key)
raise TypeError, "expected String, got #{key.class}" unless key.is_a?(String)
raise ArgumentError, "key must be Encoding::BINARY" unless key.encoding == Encoding::BINARY
raise ArgumentError, "key must be 32 or 64 bytes" unless [32, 64].include?(key.length)

length = key.length / 2

@key1 = key.slice(0, length)
@key2 = key.slice(length..-1)
end

alias inspect to_s

def encrypt(plaintext, associated_data = [])
inputs = []
inputs.concat(Array(associated_data))
inputs << plaintext

v = _s2v(inputs)
ciphertext = _transform(v, plaintext)
v + ciphertext
end

def decrypt(ciphertext, associated_data = [])
v = ciphertext.slice(0, 16)
ciphertext = ciphertext.slice(16..-1)
plaintext = _transform(v, ciphertext)

inputs = []
inputs.concat(Array(associated_data))
inputs << plaintext
t = _s2v(inputs)

# TODO: not constant time
raise "bad encrypt" unless t == v

plaintext
end

private

def _transform(v, data)
return "".b if data.empty?

counter = v.dup
counter[8] = (counter[8].ord & 0x7f).chr
counter[12] = (counter[12].ord & 0x7f).chr

cipher = OpenSSL::Cipher::AES.new(@key1.length * 8, :CTR)
cipher.encrypt
cipher.iv = counter
cipher.key = @key2
cipher.update(data) + cipher.final
end

def _s2v(inputs)
inputs = Array(inputs)
cmac = CMAC.new(@key1)

if inputs.empty?
data = ("\0" * 15) + "\x01"
return cmac.digest(data)
end

d = cmac.digest("\0" * 16)

inputs.each_with_index do |input, index|
break if index == inputs.size - 1

d = _double(d)
block = cmac.digest(input)
d = _xor(d, block)
end

input = inputs.last

if input.bytesize >= 16
d = _xorend(input, d)
else
d = _double(d)
d = _xor(d, _pad(input))
end

cmac.digest(d)
end

def _pad(value)
difference = 15 - value.length
pad = "\x80" + ("\0" * difference)
value + pad
end

def _double(value)
# TODO: not constant time
return _leftshift(value) if value[0].ord < 0x80
_xor(_leftshift(value), DOUBLE_CONSTANT)
end

def _leftshift(input)
overflow = 0
words = input.unpack("N4").reverse
words = words.map do |word|
new_word = (word << 1) & 0xFFFFFFFF
new_word |= overflow
overflow = (word & 0x80000000) >= 0x80000000 ? 1 : 0
new_word
end
words.reverse.pack("N4")
end

def _xor(a, b)
length = [a.length, b.length].min
output = "\0" * length
length.times do |i|
output[i] = (a[i].ord ^ b[i].ord).chr
end
output
end

def _xorend(a, b)
difference = a.length - b.length
left = a.slice(0, difference)
right = a.slice(difference..-1)
left + _xor(right, b)
end
end
end
end
136 changes: 0 additions & 136 deletions lib/sivchain/aes_siv.rb

This file was deleted.

Loading

0 comments on commit 2485bdc

Please sign in to comment.