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

Commit

Permalink
Merge remote-tracking branch 'origin/ruby/prepare-to-merge-ruby-siv' …
Browse files Browse the repository at this point in the history
…into ruby/merge-siv
  • Loading branch information
tarcieri committed Jun 24, 2017
2 parents bddc7b7 + 7a1da1a commit 091ed58
Show file tree
Hide file tree
Showing 2 changed files with 211 additions and 0 deletions.
136 changes: 136 additions & 0 deletions lib/sivchain/aes_siv.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
require "cmac"
require "openssl"
require "aes_siv/version"

class AES_SIV
DOUBLE_CONSTANT = ("\x0" * 15) + "\x87"

def initialize(key)
raise ArgumentError unless [32, 48, 64].include?(key.length)

length = key.length / 2

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

def encrypt(plaintext, options = {})
inputs = _gather_inputs(plaintext, options)
v = _s2v(inputs)
ciphertext = _transform(v, plaintext)
v + ciphertext
end

def decrypt(ciphertext, options = {})
v = ciphertext.slice(0, 16)
ciphertext = ciphertext.slice(16..-1)
plaintext = _transform(v, ciphertext)

inputs = _gather_inputs(plaintext, options)
t = _s2v(inputs)

if t == v
plaintext
else
fail "bad encrypt"
end
end

def _gather_inputs(plaintext, options = {})
associated_data = options.fetch(:associated_data, [])
associated_data = Array(associated_data)
nonce = options[:nonce]

inputs = []
inputs.concat(associated_data)
inputs << nonce if nonce
inputs << plaintext
inputs
end

def _transform(v, data)
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"
cmac.sign(data)
else
d = cmac.sign("\0" * 16)

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

d = _double(d)
block = cmac.sign(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.sign(d)
end
end

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

def _double(value)
if value[0].ord < 0x80
_leftshift(value)
else
_xor(_leftshift(value), DOUBLE_CONSTANT)
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 _xor(a, b)
a = a.b
b = b.b

output = ''
length = [a.length, b.length].min
length.times do |i|
output << (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
75 changes: 75 additions & 0 deletions spec/sivchain/aes_siv_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
require 'test_helper'

class AES_SIVTest < Minitest::Test
def test_valid_key_lengths
aes_siv = AES_SIV.new("\0" * 32)
refute_nil aes_siv

aes_siv = AES_SIV.new("\0" * 48)
refute_nil aes_siv

aes_siv = AES_SIV.new("\0" * 64)
refute_nil aes_siv
end

def test_invalid_key_lengths
assert_raises(ArgumentError) do
AES_SIV.new("\0" * 52)
end
end

def test_deterministic_authenticated_encryption
key = ["fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"].pack("H*")
associated_data = ["101112131415161718191a1b1c1d1e1f2021222324252627"].pack("H*")
plaintext = ["112233445566778899aabbccddee"].pack("H*")
ciphertext = ["85632d07c6e8f37f950acd320a2ecc9340c02b9690c4dc04daef7f6afe5c"].pack("H*")

aes_siv = AES_SIV.new(key)

assert_equal ciphertext, aes_siv.encrypt(plaintext, associated_data: associated_data)
end

def test_nonce_based_authenticated_encryption
key = ["7f7e7d7c7b7a79787776757473727170404142434445464748494a4b4c4d4e4f"].pack("H*")
associated_data = []
associated_data << ["00112233445566778899aabbccddeeffdeaddadadeaddadaffeeddccbbaa99887766554433221100"].pack("H*")
associated_data << ["102030405060708090a0"].pack("H*")
nonce = ["09f911029d74e35bd84156c5635688c0"].pack("H*")
plaintext = ["7468697320697320736f6d6520706c61696e7465787420746f20656e6372797074207573696e67205349562d414553"].pack("H*")
ciphertext = ["7bdb6e3b432667eb06f4d14bff2fbd0fcb900f2fddbe404326601965c889bf17dba77ceb094fa663b7a3f748ba8af829ea64ad544a272e9c485b62a3fd5c0d"].pack("H*")

aes_siv = AES_SIV.new(key)

assert_equal ciphertext, aes_siv.encrypt(plaintext, associated_data: associated_data, nonce: nonce)
end

def test_encrypt_and_decrypt_aes128
key = SecureRandom.random_bytes(32)
nonce = SecureRandom.random_bytes(16)

aes_siv = AES_SIV.new(key)
ciphertext = aes_siv.encrypt("too many secrets", nonce: nonce)

assert_equal "too many secrets", aes_siv.decrypt(ciphertext, nonce: nonce)
end

def test_encrypt_and_decrypt_aes192
key = SecureRandom.random_bytes(48)
nonce = SecureRandom.random_bytes(16)

aes_siv = AES_SIV.new(key)
ciphertext = aes_siv.encrypt("too many secrets", nonce: nonce)

assert_equal "too many secrets", aes_siv.decrypt(ciphertext, nonce: nonce)
end

def test_encrypt_and_decrypt_aes256
key = SecureRandom.random_bytes(64)
nonce = SecureRandom.random_bytes(16)

aes_siv = AES_SIV.new(key)
ciphertext = aes_siv.encrypt("too many secrets", nonce: nonce)

assert_equal "too many secrets", aes_siv.decrypt(ciphertext, nonce: nonce)
end
end

0 comments on commit 091ed58

Please sign in to comment.