///|
/// GCM (NIST SP 800-38D) authenticated encryption over a 128-bit block
/// cipher, restricted to the 12-octet IV that RFC 9580 §5.13.5 requires
/// for OpenPGP (the general 1..2^64-bit IV derivation via GHASH is not
/// implemented; a 96-bit IV uses J0 = IV || 0^31 || 1).
///
/// Verified against the published GCM test cases in src/cipher/gcm_test.mbt
/// (AES-128 and AES-256, with and without associated data).
///|
/// GHASH multiplication X · H in GF(2^128) with the GCM bit order
/// (SP 800-38D §6.3): V starts as H, the bits of X select, and the V
/// shift propagates carries from the least- to the most-significant byte,
/// reducing with R = 0xE1 on the bit that falls off.
fn gcm_mul(x : BytesView, h : BytesView) -> Bytes {
let z = Array::make(16, b'\x00')
let v = Array::make(16, b'\x00')
for i in 0..<16 {
v[i] = h[i]
}
for i in 0..<128 {
if ((x[i / 8].to_int() >> (7 - i % 8)) & 1) == 1 {
for j in 0..<16 {
z[j] = (z[j].to_int() ^ v[j].to_int()).to_byte()
}
}
let lsb = v[15].to_int() & 1
let mut carry = 0
for j in 0..<16 {
let hi = v[j].to_int()
v[j] = (((hi >> 1) | (carry << 7)) & 0xFF).to_byte()
carry = hi & 1
}
if lsb == 1 {
v[0] = (v[0].to_int() ^ 0xE1).to_byte()
}
}
Bytes::from_array(z)
}
///|
/// GHASH_H(A', C'): both inputs zero-padded to blocks, followed by the
/// 64-bit big-endian bit lengths of A and C (SP 800-38D §6.4).
fn gcm_ghash(h : BytesView, ad : BytesView, ct : BytesView) -> Bytes raise {
let mut y = Bytes::make(16, b'\x00')
let mut off = 0
while off < ad.length() {
let end = if off + 16 < ad.length() { off + 16 } else { ad.length() }
let blk : Array[Byte] = Array(capacity=16)
for i in off..> shift) & 0xFFL).to_int().to_byte())
}
for i in 0..<8 {
let shift = (7 - i) * 8
lens.push(((ct_bits >> shift) & 0xFFL).to_int().to_byte())
}
gcm_mul(
@primitives.xor_bytes(y.exact_view(), Bytes::from_array(lens).exact_view()),
h,
)
}
///|
/// J0 = IV || 0^31 || 1 for a 12-octet IV.
fn gcm_j0(iv : BytesView) -> Bytes raise {
guard iv.length() == 12 else {
@error.fail("gcm: only a 12-octet IV is supported, got \{iv.length()}")
}
let out : Array[Byte] = Array(capacity=16)
for b in iv {
out.push(b)
}
out.push(b'\x00')
out.push(b'\x00')
out.push(b'\x00')
out.push(b'\x01')
Bytes::from_array(out)
}
///|
/// Increment the low 32 bits of the counter block (big-endian, mod 2^32).
fn gcm_inc32(ctr : BytesView) -> Bytes {
let out : Array[Byte] = Array(capacity=16)
for b in ctr {
out.push(b)
}
// 32-bit counter arithmetic needs Int64: Int is 32 bits on this toolchain.
let mut v : Int64 = 0L
for i in 12..<16 {
v = (v << 8) | ctr[i].to_int().to_int64()
}
v = (v + 1L) & 0xFFFFFFFFL
for i in 12..<16 {
let shift = (15 - i) * 8
out[i] = ((v >> shift) & 0xFFL).to_int().to_byte()
}
Bytes::from_array(out)
}
///|
/// GCM-ENCRYPT (SP 800-38D §7.1). Returns ciphertext || 16-octet tag.
pub fn gcm_encrypt(
bc : &@primitives.BlockCipher,
iv : BytesView,
ad : BytesView,
plain : BytesView,
) -> Bytes raise {
guard bc.block_size() == 16 else {
@error.fail("gcm: needs a 128-bit block cipher")
}
let h = bc.encrypt_block(Bytes::make(16, b'\x00').exact_view())
let j0 = gcm_j0(iv)
let out = Buffer(size_hint=plain.length() + 16)
let mut ctr = gcm_inc32(j0.exact_view())
let mut off = 0
while off < plain.length() {
let end = if off + 16 < plain.length() { off + 16 } else { plain.length() }
let ks = bc.encrypt_block(ctr.exact_view())
out.write_bytes(
@primitives.xor_bytes(plain[off:end], ks.exact_view()[0:end - off]).exact_view(),
)
ctr = gcm_inc32(ctr.exact_view())
off = off + 16
}
let ct = out.to_bytes()
let tag = @primitives.xor_bytes(
bc.encrypt_block(j0.exact_view()).exact_view(),
gcm_ghash(h.exact_view(), ad, ct.exact_view()).exact_view(),
)
ct.add(tag)
}
///|
/// GCM-DECRYPT (SP 800-38D §7.2). `cipher` is ciphertext || 16-octet tag;
/// raises when the tag does not verify, and only then is no plaintext
/// returned.
pub fn gcm_decrypt(
bc : &@primitives.BlockCipher,
iv : BytesView,
ad : BytesView,
cipher : BytesView,
) -> Bytes raise {
guard bc.block_size() == 16 else {
@error.fail("gcm: needs a 128-bit block cipher")
}
guard cipher.length() >= 16 else {
@error.fail("gcm: ciphertext shorter than the tag")
}
let ct = cipher[0:cipher.length() - 16]
let stored_tag = cipher[cipher.length() - 16:cipher.length()]
let h = bc.encrypt_block(Bytes::make(16, b'\x00').exact_view())
let j0 = gcm_j0(iv)
let tag = @primitives.xor_bytes(
bc.encrypt_block(j0.exact_view()).exact_view(),
gcm_ghash(h.exact_view(), ad, ct).exact_view(),
)
guard @primitives.ct_equal(tag.exact_view(), stored_tag) else {
@error.fail("gcm: authentication tag mismatch")
}
let out = Buffer(size_hint=ct.length())
let mut ctr = gcm_inc32(j0.exact_view())
let mut off = 0
while off < ct.length() {
let end = if off + 16 < ct.length() { off + 16 } else { ct.length() }
let ks = bc.encrypt_block(ctr.exact_view())
out.write_bytes(
@primitives.xor_bytes(ct[off:end], ks.exact_view()[0:end - off]).exact_view(),
)
ctr = gcm_inc32(ctr.exact_view())
off = off + 16
}
out.to_bytes()
}