class HexaPDF::Encryption::RubyAES
See: PDF2.0 s7.6.3
This implementation is using AES in Cipher Block Chaining (CBC) mode.
times slower when encrypting than the FastAES version.
For reference: This implementation is about 5000 times slower when decrypting and about 1800
class based on OpenSSL should be used when possible.
Since this algorithm is implemented in pure Ruby, it is not very fast. Therefore the FastAES
Implementation of the general encryption algorithm AES.
def add_round_key(state, round_key)
AddRoundKey Step
def add_round_key(state, round_key) i = 0 (state[i] ^= round_key[i]; i += 1) while i < state.size end
def decrypt(state)
def decrypt(state) i = NUMBER_OF_ROUNDS[@key.size] add_round_key(state, @expanded_key_blocks[i]) i -= 1 while i > 0 inverse_sub_bytes_and_shift_rows(state) add_round_key(state, @expanded_key_blocks[i]) inverse_mix_columns(state) i -= 1 end inverse_sub_bytes_and_shift_rows(state) add_round_key(state, @expanded_key_blocks[i]) end
def encrypt(state)
def encrypt(state) add_round_key(state, @expanded_key_blocks[0]) i = 1 while i < NUMBER_OF_ROUNDS[@key.size] sub_bytes_and_shift_rows(state) mix_columns(state) add_round_key(state, @expanded_key_blocks[i]) i += 1 end sub_bytes_and_shift_rows(state) add_round_key(state, @expanded_key_blocks[i]) end
def expand_key(key)
Generates NUMBER_OF_ROUNDS + 1 round keys of 128 bit using Rijndael's key scheduling
KeyExpansion step
def expand_key(key) key_size = key.size nr_bytes = 16 * (NUMBER_OF_ROUNDS[key_size] + 1) result = key.bytes temp = result[-4, 4] while result.size < nr_bytes if result.size % key_size == 0 temp[0] = SBOX[temp[1]] ^ RCON[result.size / key_size] temp[1] = SBOX[temp[2]] temp[2] = SBOX[temp[3]] temp[3] = SBOX[result[-4]] # result[-4] is equal to temp[0] elsif key_size == 32 && result.size % key_size == 16 temp[0] = SBOX[temp[0]] temp[1] = SBOX[temp[1]] temp[2] = SBOX[temp[2]] temp[3] = SBOX[temp[3]] end result << (temp[0] ^= result[-key_size]) result << (temp[1] ^= result[-key_size]) result << (temp[2] ^= result[-key_size]) result << (temp[3] ^= result[-key_size]) end result.each_slice(16).to_a end
def initialize(key, iv, mode)
Creates a new AES object using the given encryption key and initialization vector.
def initialize(key, iv, mode) @key = key @expanded_key_blocks = expand_key(@key) @prev_block = iv.bytes @mode = mode end
def inverse_mix_columns(state)
s[2,c] = [13 09 14 11] s[2,c]
s[1,c] [09 14 11 13] s[1,c]
s[0,c] [14 11 13 09] s[0,c]
I.e. written as matrix multiplication:
fixed polynomial 3x^3 + x^2 + x + 2 modulo x^4 + 1, which is 11x^3 + 13x^2 + 9x + 14.
Each column is interpreted as polynomial in GF(2^8) and multiplied by the inverse of the
InvMixColumns step
def inverse_mix_columns(state) 4.times do |c| # each column s0, s1, s2, s3 = state[4 * c], state[4 * c + 1], state[4 * c + 2], state[4 * c + 3] state[4 * c] = G14MULT[s0] ^ G11MULT[s1] ^ G13MULT[s2] ^ G9MULT[s3] state[4 * c + 1] = G9MULT[s0] ^ G14MULT[s1] ^ G11MULT[s2] ^ G13MULT[s3] state[4 * c + 2] = G13MULT[s0] ^ G9MULT[s1] ^ G14MULT[s2] ^ G11MULT[s3] state[4 * c + 3] = G11MULT[s0] ^ G13MULT[s1] ^ G9MULT[s2] ^ G14MULT[s3] end end
def inverse_sub_bytes_and_shift_rows(state)
2 6 10 14 10 14 2 6
1 5 9 13 ---> 13 1 5 9
0 4 8 12 0 4 8 12
places to the left., i.e.
InvShiftRows: Cyclically shifts row two by three, row three by two and row four by one
inverse S-box, using the byte value as index into it
InvSubBytes: Substitutes each byte in the state with the corresponding value from the
InvSubBytes and InvShiftRows steps combined
def inverse_sub_bytes_and_shift_rows(state) state[0] = INV_SBOX[state[0]] state[4] = INV_SBOX[state[4]] state[8] = INV_SBOX[state[8]] state[12] = INV_SBOX[state[12]] t = INV_SBOX[state[13]] state[13] = INV_SBOX[state[9]] state[9] = INV_SBOX[state[5]] state[5] = INV_SBOX[state[1]] state[1] = t t = INV_SBOX[state[2]] u = INV_SBOX[state[6]] state[2] = INV_SBOX[state[10]] state[6] = INV_SBOX[state[14]] state[10] = t state[14] = u t = INV_SBOX[state[15]] u = INV_SBOX[state[11]] v = INV_SBOX[state[7]] state[15] = INV_SBOX[state[3]] state[11] = t state[7] = u state[3] = v end
def mix_columns(state)
s[2,c] = [01 01 02 03] s[2,c]
s[1,c] [01 02 03 01] s[1,c]
s[0,c] [02 03 01 01] s[0,c]
I.e. written as matrix multiplication:
3x^3 + x^2 + x + 2 modulo x^4 + 1.
Each column is interpreted as polynomial in GF(2^8) and multiplied by the fixed polynomial
MixColumns step
def mix_columns(state) 4.times do |c| # each column s0, s1, s2, s3 = state[4 * c], state[4 * c + 1], state[4 * c + 2], state[4 * c + 3] state[4 * c] = G2MULT[s0] ^ G3MULT[s1] ^ s2 ^ s3 state[4 * c + 1] = s0 ^ G2MULT[s1] ^ G3MULT[s2] ^ s3 state[4 * c + 2] = s0 ^ s1 ^ G2MULT[s2] ^ G3MULT[s3] state[4 * c + 3] = G3MULT[s0] ^ s1 ^ s2 ^ G2MULT[s3] end end
def process(data)
def process(data) data = data.bytes (data.size / BLOCK_SIZE).times do |i| block = data[i * BLOCK_SIZE, BLOCK_SIZE] if @mode == :encrypt xor_blocks(block, @prev_block) # CBC: XOR plain text block with previous cipher block send(@mode, block) @prev_block = block else prev = block.dup send(@mode, block) xor_blocks(block, @prev_block) # CBC: XOR plain text block with previous cipher block @prev_block = prev end data[i * BLOCK_SIZE, BLOCK_SIZE] = block end data.pack('C*') end
def sub_bytes_and_shift_rows(state)
2 6 10 14 10 14 2 6
1 5 9 13 ---> 5 9 13 1
0 4 8 12 0 4 8 12
to the left, i.e.
ShiftRows: Cyclically shifts row two by one, row three by two and row four by three places
using the byte value as index into the S-box.
SubBytes: Substitutes each byte in the state with the corresponding value from the S-box,
SubBytes and ShiftRows steps combined
def sub_bytes_and_shift_rows(state) state[0] = SBOX[state[0]] state[4] = SBOX[state[4]] state[8] = SBOX[state[8]] state[12] = SBOX[state[12]] t = SBOX[state[1]] state[1] = SBOX[state[5]] state[5] = SBOX[state[9]] state[9] = SBOX[state[13]] state[13] = t t = SBOX[state[2]] u = SBOX[state[6]] state[2] = SBOX[state[10]] state[6] = SBOX[state[14]] state[10] = t state[14] = u t = SBOX[state[3]] u = SBOX[state[7]] v = SBOX[state[11]] state[3] = SBOX[state[15]] state[7] = t state[11] = u state[15] = v end