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

Commit

Permalink
ruby: Various code cleanups
Browse files Browse the repository at this point in the history
- Extract shared CMAC/SIV code into a Util module
- Add a ct_equal to do constant time-ish comparisons
- Add a CodeClimate config
  • Loading branch information
tarcieri committed Jun 25, 2017
1 parent 2485bdc commit 2bc770f
Show file tree
Hide file tree
Showing 8 changed files with 83 additions and 100 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/.bundle/
/.yardoc
/.rakeTasks
/Gemfile.lock
/_yardoc/
/coverage/
Expand Down
7 changes: 0 additions & 7 deletions .rakeTasks

This file was deleted.

2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ group :development, :test do
gem "guard-rspec"
gem "rake"
gem "rspec", "~> 3.5"
gem "rubocop", "0.48.1"
gem "rubocop", "0.49.1"
gem "tjson", "~> 0.5"
end
1 change: 1 addition & 0 deletions lib/sivchain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

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

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

module SIVChain
Expand All @@ -10,6 +11,9 @@ class CMAC
ConstantBlock = (("\0" * 15) + "\x87").b.freeze

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

case key.length
when 16
@cipher = OpenSSL::Cipher.new("AES-128-ECB")
Expand All @@ -29,8 +33,8 @@ def initialize(key)
def digest(message)
message = message.b

if _needs_padding?(message)
message = _pad_message(message)
if message.empty? || message.length % 16 != 0
message = _pad(message)
final_block = @key2
else
final_block = @key1
Expand All @@ -41,8 +45,8 @@ def digest(message)
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)
block = Util.xor(final_block, block) if i == range.last
block = Util.xor(block, last_ciphertext)
last_ciphertext = _encrypt_block(block)
end

Expand All @@ -51,59 +55,22 @@ def digest(message)

private

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

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 = Util.double(key0)
key2 = Util.double(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
54 changes: 12 additions & 42 deletions lib/sivchain/aes/siv.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ module SIVChain
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
Expand Down Expand Up @@ -41,14 +39,19 @@ def decrypt(ciphertext, associated_data = [])
inputs << plaintext
t = _s2v(inputs)

# TODO: not constant time
raise "bad encrypt" unless t == v
raise "bad encrypt" unless Util.ct_equal(t, v)

plaintext
end

private

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

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

Expand Down Expand Up @@ -77,61 +80,28 @@ def _s2v(inputs)
inputs.each_with_index do |input, index|
break if index == inputs.size - 1

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

input = inputs.last

if input.bytesize >= 16
d = _xorend(input, d)
else
d = _double(d)
d = _xor(d, _pad(input))
d = Util.double(d)
d = Util.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)
left + Util.xor(right, b)
end
end
end
Expand Down
51 changes: 51 additions & 0 deletions lib/sivchain/util.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# encoding: binary
# frozen_string_literal: true

module SIVChain
# Utility functions
module Util
DOUBLE_CONSTANT = (("\x0" * 15) + "\x87").freeze

module_function

# Perform a doubling operation as described in the CMAC and SIV papers
def double(value)
overflow = 0
words = value.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
result = words.reverse.pack("N4")

# TODO: not constant time!
return result if value[0].ord < 0x80
xor(result, DOUBLE_CONSTANT)
end

# Perform an xor on arbitrary bytestrings
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

# Perform a constant time-ish comparison of two bytestrings
def ct_equal(a, b)
return false unless a.bytesize == b.bytesize

l = a.unpack("C*")
r = 0
i = -1

b.each_byte { |v| r |= v ^ l[i += 1] }

r.zero?
end
end
end
2 changes: 1 addition & 1 deletion spec/sivchain/aes/cmac_spec.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

RSpec.describe SIVChain::AES::CMAC do
let(:example_key) { "\x01" * 16 }
let(:example_key) { ("\x01" * 16).b }

describe "inspect" do
it "does not contain instance variable values" do
Expand Down

0 comments on commit 2bc770f

Please sign in to comment.