#![allow(clippy::needless_range_loop)]
use crate::aes::{Aes128, Aes192, Aes256, BLOCK_LEN};
use crate::modes::increment_be32;
use ic_core::traits::{Aead, Algorithm, BlockCipher, SelfTest};
use ic_core::{ensure, Result, Zeroize};
const R: u8 = 0xe1;
pub struct GcmLimits;
impl GcmLimits {
pub const MAX_PLAINTEXT_BYTES: u64 = (1 << 36) - 32;
pub const MAX_RANDOM_NONCE_INVOCATIONS: u64 = 1 << 32;
pub const RECOMMENDED_NONCE_LEN: usize = 12;
}
#[inline]
#[must_use]
pub fn ghash_accelerated() -> bool {
#[cfg(all(target_arch = "x86_64", feature = "std"))]
{
ic_core::cpu::has_pclmulqdq() && std::arch::is_x86_feature_detected!("ssse3")
}
#[cfg(not(all(target_arch = "x86_64", feature = "std")))]
{
false
}
}
pub(crate) fn portable_ghash_mul(x: &mut [u8; BLOCK_LEN], h: &[u8; BLOCK_LEN]) {
let mut z = [0u8; BLOCK_LEN];
let mut v = *h;
for i in 0..128 {
let bit = (x[i / 8] >> (7 - (i % 8))) & 1;
let m = bit.wrapping_neg();
for j in 0..BLOCK_LEN {
z[j] ^= v[j] & m;
}
let lsb = v[BLOCK_LEN - 1] & 1;
let mut carry = 0u8;
for byte in v.iter_mut() {
let next = *byte & 1;
*byte = (*byte >> 1) | (carry << 7);
carry = next;
}
v[0] ^= R & lsb.wrapping_neg();
}
*x = z;
z.zeroize();
v.zeroize();
}
struct Ghash {
h: [u8; BLOCK_LEN],
powers: [[u8; BLOCK_LEN]; 3],
acc: [u8; BLOCK_LEN],
#[cfg(all(target_arch = "x86_64", feature = "std"))]
accelerated: bool,
}
impl Ghash {
fn new(h: [u8; BLOCK_LEN]) -> Self {
let mut me = Self {
h,
powers: [[0u8; BLOCK_LEN]; 3],
acc: [0u8; BLOCK_LEN],
#[cfg(all(target_arch = "x86_64", feature = "std"))]
accelerated: ghash_accelerated(),
};
let mut p = h;
for slot in 0..3 {
me.mul_by(&mut p, &h);
me.powers[slot] = p;
}
me
}
#[inline]
fn mul_by(&self, x: &mut [u8; BLOCK_LEN], y: &[u8; BLOCK_LEN]) {
#[cfg(all(target_arch = "x86_64", feature = "std"))]
if self.accelerated {
unsafe { crate::clmul::mul(x, y) };
return;
}
portable_ghash_mul(x, y);
}
#[inline]
fn absorb4(&mut self, blocks: &[u8]) {
debug_assert_eq!(blocks.len(), BLOCK_LEN * 4);
let mut terms = [[0u8; BLOCK_LEN]; 4];
for (i, t) in terms.iter_mut().enumerate() {
t.copy_from_slice(&blocks[i * BLOCK_LEN..(i + 1) * BLOCK_LEN]);
}
for j in 0..BLOCK_LEN {
terms[0][j] ^= self.acc[j];
}
let multipliers = [&self.powers[2], &self.powers[1], &self.powers[0], &self.h];
for (t, m) in terms.iter_mut().zip(multipliers) {
self.mul_by(t, m);
}
self.acc = terms[0];
for t in &terms[1..] {
for j in 0..BLOCK_LEN {
self.acc[j] ^= t[j];
}
}
}
#[inline]
fn mul_acc(&mut self) {
#[cfg(all(target_arch = "x86_64", feature = "std"))]
if self.accelerated {
unsafe { crate::clmul::mul(&mut self.acc, &self.h) };
return;
}
portable_ghash_mul(&mut self.acc, &self.h);
}
fn update_padded(&mut self, mut data: &[u8]) {
while data.len() >= BLOCK_LEN * 4 {
self.absorb4(&data[..BLOCK_LEN * 4]);
data = &data[BLOCK_LEN * 4..];
}
for chunk in data.chunks(BLOCK_LEN) {
let mut block = [0u8; BLOCK_LEN];
block[..chunk.len()].copy_from_slice(chunk);
for j in 0..BLOCK_LEN {
self.acc[j] ^= block[j];
}
self.mul_acc();
}
}
fn finalize(self) -> [u8; BLOCK_LEN] {
self.acc
}
}
impl Drop for Ghash {
fn drop(&mut self) {
self.h.zeroize();
self.acc.zeroize();
}
}
fn derive_j0(nonce: &[u8], h: &[u8; BLOCK_LEN]) -> [u8; BLOCK_LEN] {
if nonce.len() == 12 {
let mut j0 = [0u8; BLOCK_LEN];
j0[..12].copy_from_slice(nonce);
j0[15] = 1;
j0
} else {
let mut g = Ghash::new(*h);
g.update_padded(nonce);
let mut len_block = [0u8; BLOCK_LEN];
len_block[8..].copy_from_slice(&((nonce.len() as u64) * 8).to_be_bytes());
g.update_padded(&len_block);
g.finalize()
}
}
fn gcm_core<C: BlockCipher>(
cipher: &C,
nonce: &[u8],
aad: &[u8],
in_out: &mut [u8],
encrypting: bool,
) -> Result<[u8; BLOCK_LEN]> {
ensure!(
!nonce.is_empty(),
InvalidParameter,
"gcm nonce must be non-empty"
);
ensure!(
in_out.len() as u64 <= GcmLimits::MAX_PLAINTEXT_BYTES,
CounterExhausted,
"gcm plaintext exceeds 2^39-256 bits"
);
let mut h = [0u8; BLOCK_LEN];
cipher.encrypt_block(&mut h)?;
let j0 = derive_j0(nonce, &h);
let mut g = Ghash::new(h);
g.update_padded(aad);
if !encrypting {
g.update_padded(in_out);
}
const CTR_BATCH: usize = 8;
let mut counter = j0;
increment_be32(&mut counter);
let mut keystream = [0u8; BLOCK_LEN * CTR_BATCH];
for chunk in in_out.chunks_mut(BLOCK_LEN * CTR_BATCH) {
let blocks = chunk.len().div_ceil(BLOCK_LEN);
for i in 0..blocks {
keystream[i * BLOCK_LEN..(i + 1) * BLOCK_LEN].copy_from_slice(&counter);
increment_be32(&mut counter);
}
cipher.encrypt_blocks(&mut keystream[..blocks * BLOCK_LEN])?;
for (d, k) in chunk.iter_mut().zip(keystream.iter()) {
*d ^= k;
}
}
keystream.zeroize();
if encrypting {
g.update_padded(in_out);
}
let mut len_block = [0u8; BLOCK_LEN];
len_block[..8].copy_from_slice(&((aad.len() as u64) * 8).to_be_bytes());
len_block[8..].copy_from_slice(&((in_out.len() as u64) * 8).to_be_bytes());
g.update_padded(&len_block);
let mut tag = g.finalize();
let mut ek_j0 = j0;
cipher.encrypt_block(&mut ek_j0)?;
for j in 0..BLOCK_LEN {
tag[j] ^= ek_j0[j];
}
ek_j0.zeroize();
h.zeroize();
Ok(tag)
}
macro_rules! aes_gcm {
($name:ident, $inner:ty, $id:literal, $disp:literal, $keylen:literal) => {
#[doc = concat!("SP 800-38D ", $disp, ".")]
pub struct $name($inner);
impl Algorithm for $name {
const ID: &'static str = $id;
const NAME: &'static str = $disp;
}
impl Aead for $name {
const KEY_LEN: usize = $keylen;
const NONCE_LEN: usize = 12;
const TAG_LEN: usize = 16;
fn new(key: &[u8]) -> Result<Self> {
Ok(Self(<$inner as BlockCipher>::new(key)?))
}
fn seal_detached(
&self,
nonce: &[u8],
aad: &[u8],
in_out: &mut [u8],
tag: &mut [u8],
) -> Result<()> {
ensure!(tag.len() == 16, InvalidLength, "gcm tag buffer");
let t = gcm_core(&self.0, nonce, aad, in_out, true)?;
tag.copy_from_slice(&t);
Ok(())
}
fn open_detached(
&self,
nonce: &[u8],
aad: &[u8],
in_out: &mut [u8],
tag: &[u8],
) -> Result<()> {
ensure!(tag.len() == 16, InvalidLength, "gcm tag");
let expected = gcm_core(&self.0, nonce, aad, in_out, false)?;
if ic_core::ct::verify(&expected, tag) {
Ok(())
} else {
in_out.zeroize();
Err(ic_core::err!(AuthenticationFailed, $id))
}
}
}
impl SelfTest for $name {
fn self_test() -> Result<()> {
let key = [0u8; $keylen];
let nonce = [0u8; 12];
let c = <Self as Aead>::new(&key)?;
let mut buf = [0u8; 16];
let mut tag = [0u8; 16];
c.seal_detached(&nonce, &[], &mut buf, &mut tag)?;
c.open_detached(&nonce, &[], &mut buf, &tag)?;
ensure!(buf == [0u8; 16], SelfTestFailed, $id);
tag[0] ^= 1;
ensure!(
c.open_detached(&nonce, &[], &mut buf, &tag).is_err(),
SelfTestFailed,
$id
);
Ok(())
}
}
};
}
aes_gcm!(Aes128Gcm, Aes128, "aes-128-gcm", "AES-128-GCM", 16);
aes_gcm!(Aes192Gcm, Aes192, "aes-192-gcm", "AES-192-GCM", 24);
aes_gcm!(Aes256Gcm, Aes256, "aes-256-gcm", "AES-256-GCM", 32);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_batched_ghash_agrees_with_the_serial_one() {
let h = [
0x66, 0xe9, 0x4b, 0xd4, 0xef, 0x8a, 0x2c, 0x3b, 0x88, 0x4c, 0xfa, 0x59, 0xca, 0x34,
0x2b, 0x2e,
];
let mut checked = 0;
for len in [
0usize, 1, 15, 16, 17, 31, 63, 64, 65, 79, 80, 127, 128, 129, 255, 256, 1024, 1025,
] {
let data: std::vec::Vec<u8> = (0..len)
.map(|i| ((i as u64).wrapping_mul(0x9e37_79b9) >> 3) as u8)
.collect();
let mut batched = Ghash::new(h);
batched.update_padded(&data);
let mut serial = Ghash::new(h);
for chunk in data.chunks(BLOCK_LEN) {
let mut block = [0u8; BLOCK_LEN];
block[..chunk.len()].copy_from_slice(chunk);
for j in 0..BLOCK_LEN {
serial.acc[j] ^= block[j];
}
serial.mul_acc();
}
assert_eq!(
batched.acc, serial.acc,
"batched and serial GHASH disagree at {len} bytes"
);
checked += 1;
}
assert_eq!(checked, 18, "the comparison did not run");
let long = std::vec![0xa5u8; BLOCK_LEN * 4];
let mut g = Ghash::new(h);
g.absorb4(&long);
let mut serial = Ghash::new(h);
for chunk in long.chunks(BLOCK_LEN) {
for j in 0..BLOCK_LEN {
serial.acc[j] ^= chunk[j];
}
serial.mul_acc();
}
assert_eq!(g.acc, serial.acc, "absorb4 alone disagrees with four steps");
}
#[test]
fn the_precomputed_powers_are_powers_of_h() {
let h = [0x3cu8; BLOCK_LEN];
let g = Ghash::new(h);
let mut expect = h;
for (i, stored) in g.powers.iter().enumerate() {
g.mul_by(&mut expect, &h);
assert_eq!(*stored, expect, "power {} is not H^{}", i, i + 2);
}
assert_ne!(g.powers[0], g.powers[1]);
assert_ne!(g.powers[1], g.powers[2]);
assert_ne!(g.powers[0], h);
}
use ic_core::codec::{hex, unhex};
fn check(key: &str, nonce: &str, pt: &str, aad: &str, ct: &str, tag: &str) {
let k = unhex(key).unwrap();
let mut buf = unhex(pt).unwrap();
let mut got_tag = [0u8; 16];
let n = unhex(nonce).unwrap();
let a = unhex(aad).unwrap();
match k.len() {
16 => {
let c = Aes128Gcm::new(&k).unwrap();
c.seal_detached(&n, &a, &mut buf, &mut got_tag).unwrap();
}
24 => {
let c = Aes192Gcm::new(&k).unwrap();
c.seal_detached(&n, &a, &mut buf, &mut got_tag).unwrap();
}
_ => {
let c = Aes256Gcm::new(&k).unwrap();
c.seal_detached(&n, &a, &mut buf, &mut got_tag).unwrap();
}
}
assert_eq!(hex(&buf), ct, "ciphertext");
assert_eq!(hex(&got_tag), tag, "tag");
}
#[test]
fn gcm_spec_case_1_empty() {
check(
"00000000000000000000000000000000",
"000000000000000000000000",
"",
"",
"",
"58e2fccefa7e3061367f1d57a4e7455a",
);
}
#[test]
fn gcm_spec_case_2_single_block() {
check(
"00000000000000000000000000000000",
"000000000000000000000000",
"00000000000000000000000000000000",
"",
"0388dace60b6a392f328c2b971b2fe78",
"ab6e47d42cec13bdf53a67b21257bddf",
);
}
#[test]
fn gcm_spec_case_3_multi_block() {
check(
"feffe9928665731c6d6a8f9467308308",
"cafebabefacedbaddecaf888",
"d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255",
"",
"42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091473f5985",
"4d5c2af327cd64a62cf35abd2ba6fab4",
);
}
#[test]
fn gcm_spec_case_4_with_aad() {
check(
"feffe9928665731c6d6a8f9467308308",
"cafebabefacedbaddecaf888",
"d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39",
"feedfacedeadbeeffeedfacedeadbeefabaddad2",
"42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091",
"5bc94fbc3221a5db94fae95ae7121a47",
);
}
#[test]
fn gcm_short_nonce_uses_ghash_j0() {
check(
"feffe9928665731c6d6a8f9467308308",
"cafebabefacedbad",
"d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39",
"feedfacedeadbeeffeedfacedeadbeefabaddad2",
"61353b4c2806934a777ff51fa22a4755699b2a714fcdc6f83766e5f97b6c742373806900e49f24b22b097544d4896b424989b5e1ebac0f07c23f4598",
"3612d2e79e3b0785561be14aaca2fccb",
);
}
#[test]
fn aes256_gcm_vector() {
check(
"feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308",
"cafebabefacedbaddecaf888",
"d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39",
"feedfacedeadbeeffeedfacedeadbeefabaddad2",
"522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662",
"76fc6ece0f4e1768cddf8853bb2d551b",
);
}
#[test]
fn roundtrip_and_tamper_detection() {
let c = Aes256Gcm::new(&[7u8; 32]).unwrap();
let nonce = [9u8; 12];
let aad = b"header";
let plaintext = b"attack at dawn, bring the ontology";
let mut buf = plaintext.to_vec();
let mut tag = [0u8; 16];
c.seal_detached(&nonce, aad, &mut buf, &mut tag).unwrap();
assert_ne!(&buf[..], &plaintext[..]);
let mut ok = buf.clone();
c.open_detached(&nonce, aad, &mut ok, &tag).unwrap();
assert_eq!(&ok[..], &plaintext[..]);
let mut bad = buf.clone();
bad[0] ^= 1;
assert!(c.open_detached(&nonce, aad, &mut bad, &tag).is_err());
assert_eq!(
bad,
vec![0u8; bad.len()],
"plaintext must be wiped on failure"
);
let mut wrong_aad = buf.clone();
assert!(c
.open_detached(&nonce, b"other", &mut wrong_aad, &tag)
.is_err());
let mut wrong_nonce = buf.clone();
assert!(c
.open_detached(&[0u8; 12], aad, &mut wrong_nonce, &tag)
.is_err());
}
#[test]
fn self_tests_pass() {
Aes128Gcm::self_test().unwrap();
Aes192Gcm::self_test().unwrap();
Aes256Gcm::self_test().unwrap();
}
}