module Clacky::AesGcm

def self.aes_ecb(key)

Return a lambda: block(16 bytes) → encrypted block(16 bytes)
def self.aes_ecb(key)
er.new("aes-256-ecb")
 c.final

def self.build_j0(iv, h)

For other lengths: J0 = GHASH(H, {}, IV)
For 12-byte IVs (standard): J0 = IV || 0x00000001
Build J0 counter block.
def self.build_j0(iv, h)
2
x00\x01"
)

def self.bytes_to_int(str)

Convert a binary string to an unsigned big-endian integer.
def self.bytes_to_int(str)
 { |acc, b| (acc << 8) | b }

def self.compute_tag(aes, h, j0, ciphertext, aad)

tag = E(K, J0) XOR GHASH(H, aad, ciphertext)
Compute GCM auth tag.
def self.compute_tag(aes, h, j0, ciphertext, aad)
 ciphertext)

def self.ctr_crypt(aes, ctr0, data)

Starting counter block is `ctr0` (already incremented to J0+1 by caller).
CTR-mode encryption/decryption (symmetric — same operation).
def self.ctr_crypt(aes, ctr0, data)
.empty?
tesize
all(ctr)
byteslice(pos, BLOCK_SIZE)
(keystream, chunk)

def self.decrypt(key, iv, ciphertext, tag, aad = "")

Raises:
  • (OpenSSL::Cipher::CipherError) - on authentication failure

Returns:
  • (String) - plaintext (UTF-8)

Parameters:
  • aad (String) -- additional authenticated data (may be empty)
  • tag (String) -- 16-byte binary auth tag
  • ciphertext (String) -- binary ciphertext
  • iv (String) -- 12-byte binary IV
  • key (String) -- 32-byte binary key
def self.decrypt(key, iv, ciphertext, tag, aad = "")
  aes       = aes_ecb(key)
  h         = aes.call("\x00" * BLOCK_SIZE)
  j0        = build_j0(iv, h)
  exp_tag   = compute_tag(aes, h, j0, ciphertext, aad.b)
  unless secure_compare(exp_tag, tag)
    raise OpenSSL::Cipher::CipherError, "bad decrypt (authentication tag mismatch)"
  end
  ctr_crypt(aes, inc32(j0), ciphertext).force_encoding("UTF-8")
end

def self.each_block(data, &block)

Iterate over 16-byte zero-padded blocks of data, yielding each block.
def self.each_block(data, &block)
y?
size
slice(i, BLOCK_SIZE)
st(BLOCK_SIZE, "\x00") if chunk.bytesize < BLOCK_SIZE

def self.encrypt(key, iv, plaintext, aad = "")

Returns:
  • (Array) - [ciphertext, auth_tag] both binary strings

Parameters:
  • aad (String) -- additional authenticated data (may be empty)
  • plaintext (String) -- binary or UTF-8 plaintext
  • iv (String) -- 12-byte binary IV (recommended for GCM)
  • key (String) -- 32-byte binary key
def self.encrypt(key, iv, plaintext, aad = "")
  aes  = aes_ecb(key)
  h    = aes.call("\x00" * BLOCK_SIZE)              # H = E(K, 0^128)
  j0   = build_j0(iv, h)
  ct   = ctr_crypt(aes, inc32(j0), plaintext.b)
  tag  = compute_tag(aes, h, j0, ct, aad.b)
  [ct, tag]
end

def self.gf128_mul(x, y)

def self.gf128_mul(x, y)
<< 127) != 0
1
 1

def self.ghash(h, aad, ciphertext)

ghash = Σ (Xi * H^i) where Xi are 128-bit blocks of padded aad + ciphertext + lengths
GHASH: polynomial hashing over GF(2^128)
def self.ghash(h, aad, ciphertext)
t(h)
s
blk| x = gf128_mul(bytes_to_int(blk) ^ x, h_int) }
t blocks
xt) { |blk| x = gf128_mul(bytes_to_int(blk) ^ x, h_int) }
aad) || len(ciphertext) in bits, each as 64-bit big-endian
tesize * 8].pack("Q>") + [ciphertext.bytesize * 8].pack("Q>")
_to_int(len_block) ^ x, h_int)

def self.inc32(block)

Increment the rightmost 32 bits of a 16-byte counter block (big-endian).
def self.inc32(block)
eslice(0, 12)
eslice(12, 4).unpack1("N")
+ 1) & 0xFFFFFFFF].pack("N")

def self.int_to_bytes(n)

Convert an unsigned integer to a 16-byte big-endian binary string.
def self.int_to_bytes(n)
shift(n & 0xFF); n >>= 8 }

def self.secure_compare(a, b)

Constant-time string comparison to prevent timing attacks.
def self.secure_compare(a, b)
ytesize != b.bytesize
) { |x, y| result |= x ^ y }

def self.xor_blocks(a, b)

XOR two binary strings, truncated to the shorter length.
def self.xor_blocks(a, b)
b.bytesize].min
 (a.getbyte(i) ^ b.getbyte(i)).chr }.join.b