This repository has been archived by the owner on Feb 5, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
ruby: Merge jtdowney/aes_siv into the Ruby implementation
- Loading branch information
Showing
9 changed files
with
312 additions
and
322 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.