| 1 | // Copyright (c) 2026 blackshirt. All rights reserved. |
| 2 | // Use of this source code is governed by an MIT license |
| 3 | // that can be found in the LICENSE file. |
| 4 | // |
| 5 | // AES-GCM (NIST SP 800-38D) built on top of the pure-V `crypto.aes` block |
| 6 | // cipher. Its build to support for QUIC protocol and another cases. |
| 7 | // QUIC (RFC 9001) mandates AES-128-GCM for Initial packets and allows |
| 8 | // AES-128-GCM / AES-256-GCM for 1-RTT packets. The GHASH multiplication uses |
| 9 | // the straightforward bit-by-bit algorithm, which is constant in structure and |
| 10 | // easy to audit (performance is adequate for handshakes and modest streams). |
| 11 | module aes |
| 12 | |
| 13 | import crypto.cipher |
| 14 | |
| 15 | // gcm_tag_size is the size of the GCM authentication tag in bytes. |
| 16 | pub const gcm_tag_size = 16 |
| 17 | |
| 18 | // gcm_nonce_size is the size of the GCM nonce in bytes (only support 96-bit nonce variant). |
| 19 | pub const gcm_nonce_size = 12 |
| 20 | |
| 21 | // zero_block is a preallocated 16-bytes of zeros block. Its currently sized by Aes block size. |
| 22 | const zero_block = []u8{len: block_size} |
| 23 | |
| 24 | // maximum plaintext length per invocation GCM: MUST be <= 2^36 - 32 octets. |
| 25 | const max_plaintext_size = u64(1) << 36 - 32 |
| 26 | |
| 27 | // maximum ciphertext length per invocation GCM: MUST be <= 2^36 - 16 octets. |
| 28 | const max_ciphertext_size = u64(1) << 36 - 16 |
| 29 | |
| 30 | // maximum length of additional authenticated data. Even technically its was not limited, |
| 31 | // we give it a limit to reduce the risk. Intended purposes of additional data was |
| 32 | // as an additional authenticated data included into output, not act as a primary input. |
| 33 | const max_aad_size = max_u32 |
| 34 | |
| 35 | // AesGcm holds the AES block cipher and the derived GHASH subkey for a key. |
| 36 | @[heap; noinit] |
| 37 | pub struct AesGcm implements cipher.AEAD { |
| 38 | mut: |
| 39 | block cipher.Block |
| 40 | h []u8 // GHASH subkey H = E(K, 0^128) |
| 41 | } |
| 42 | |
| 43 | // new_aes_gcm builds AES-GCM state for a 16, 24 or 32-byte key. |
| 44 | @[direct_array_access] |
| 45 | pub fn new_aes_gcm(key []u8) !&AesGcm { |
| 46 | if key.len != 16 && key.len != 24 && key.len != 32 { |
| 47 | return error('AES-GCM key must be 16, 24 or 32 bytes, got ${key.len}') |
| 48 | } |
| 49 | block := new_cipher(key) |
| 50 | mut h := zero_block.clone() |
| 51 | block.encrypt(mut h, zero_block) |
| 52 | return &AesGcm{ |
| 53 | block: block |
| 54 | h: h |
| 55 | } |
| 56 | } |
| 57 | |
| 58 | // nonce_size returns the size of nonce (in bytes). |
| 59 | pub fn (g &AesGcm) nonce_size() int { |
| 60 | return gcm_nonce_size |
| 61 | } |
| 62 | |
| 63 | // overhead returns the maximum difference between the lengths of a plaintext and its ciphertext. |
| 64 | pub fn (g &AesGcm) overhead() int { |
| 65 | return gcm_tag_size |
| 66 | } |
| 67 | |
| 68 | // encrypt encrypts `plaintext` with the given 12-byte `nonce` and additional |
| 69 | // authenticated data `ad`, returning ciphertext with the 16-byte tag appended. |
| 70 | @[direct_array_access] |
| 71 | pub fn (g &AesGcm) encrypt(plaintext []u8, nonce []u8, ad []u8) ![]u8 { |
| 72 | // In V language, unlike C and Go, int is always a 32 bit integer, so technically safe |
| 73 | // to assume if the arrays length not would exceed 32-bit integer (its maybe changed on the future). |
| 74 | // For safety purposes, we check agains the limit. |
| 75 | if u64(plaintext.len) > max_plaintext_size { |
| 76 | return error('AES-GCM encrypt: plaintext size exceed the limit') |
| 77 | } |
| 78 | if u64(ad.len) > max_aad_size { |
| 79 | return error('AES-GCM encrypt: additional data size exceed the limit') |
| 80 | } |
| 81 | if nonce.len != gcm_nonce_size { |
| 82 | return error('AES-GCM nonce must be ${gcm_nonce_size} bytes') |
| 83 | } |
| 84 | j0 := j0_from_nonce(nonce) |
| 85 | mut ctr := j0.clone() |
| 86 | inc32(mut ctr) |
| 87 | ciphertext := g.gctr(ctr, plaintext) |
| 88 | |
| 89 | mut ghash_in := pad_block(ad) |
| 90 | ghash_in << pad_block(ciphertext) |
| 91 | ghash_in << len_block(ad.len, ciphertext.len) |
| 92 | s := g.ghash(zero_block, ghash_in) |
| 93 | tag := g.gctr(j0, s) |
| 94 | |
| 95 | mut out := []u8{cap: ciphertext.len + gcm_tag_size} |
| 96 | out << ciphertext |
| 97 | out << tag |
| 98 | return out |
| 99 | } |
| 100 | |
| 101 | // decrypt verifies and decrypts `ciphertext` (which must include the trailing |
| 102 | // 16-byte tag) using `nonce` and additional authenticated data `ad`. |
| 103 | @[direct_array_access] |
| 104 | pub fn (g &AesGcm) decrypt(ciphertext []u8, nonce []u8, ad []u8) ![]u8 { |
| 105 | // Check the array size for safety |
| 106 | if u64(ciphertext.len) > max_ciphertext_size { |
| 107 | return error('AES-GCM decrypt: ciphertext size exceed the limit') |
| 108 | } |
| 109 | if u64(ad.len) > max_aad_size { |
| 110 | return error('AES-GCM decrypt: additional data size exceed the limit') |
| 111 | } |
| 112 | if nonce.len != gcm_nonce_size { |
| 113 | return error('AES-GCM nonce must be ${gcm_nonce_size} bytes') |
| 114 | } |
| 115 | if ciphertext.len < gcm_tag_size { |
| 116 | return error('AES-GCM ciphertext shorter than tag') |
| 117 | } |
| 118 | ct := ciphertext[..ciphertext.len - gcm_tag_size] |
| 119 | tag := ciphertext[ciphertext.len - gcm_tag_size..] |
| 120 | j0 := j0_from_nonce(nonce) |
| 121 | |
| 122 | mut ghash_in := pad_block(ad) |
| 123 | ghash_in << pad_block(ct) |
| 124 | ghash_in << len_block(ad.len, ct.len) |
| 125 | s := g.ghash(zero_block, ghash_in) |
| 126 | expected := g.gctr(j0, s) |
| 127 | if !bytes_equal(expected, tag) { |
| 128 | return error('AES-GCM authentication failed') |
| 129 | } |
| 130 | mut ctr := j0.clone() |
| 131 | inc32(mut ctr) |
| 132 | return g.gctr(ctr, ct) |
| 133 | } |
| 134 | |
| 135 | // ghash computes GHASH_H over `data`, which the caller must already have padded |
| 136 | // to a multiple of the block size, starting from the running value `y`. |
| 137 | @[direct_array_access] |
| 138 | fn (g &AesGcm) ghash(y []u8, data []u8) []u8 { |
| 139 | mut acc := y.clone() |
| 140 | mut off := 0 |
| 141 | for off < data.len { |
| 142 | for j in 0 .. block_size { |
| 143 | acc[j] ^= data[off + j] |
| 144 | } |
| 145 | acc = gf_mult(acc, g.h) |
| 146 | off += block_size |
| 147 | } |
| 148 | return acc |
| 149 | } |
| 150 | |
| 151 | // gctr applies the GCM counter mode to `input` starting from counter block |
| 152 | // `icb`, returning the keystream-XORed output. |
| 153 | @[direct_array_access] |
| 154 | fn (g &AesGcm) gctr(icb []u8, input []u8) []u8 { |
| 155 | if input.len == 0 { |
| 156 | return []u8{} |
| 157 | } |
| 158 | mut out := []u8{len: input.len} |
| 159 | mut ctr := icb.clone() |
| 160 | mut ks := zero_block.clone() |
| 161 | mut off := 0 |
| 162 | for off < input.len { |
| 163 | g.block.encrypt(mut ks, ctr) |
| 164 | n := if input.len - off < block_size { input.len - off } else { block_size } |
| 165 | for j in 0 .. n { |
| 166 | out[off + j] = input[off + j] ^ ks[j] |
| 167 | } |
| 168 | inc32(mut ctr) |
| 169 | off += block_size |
| 170 | } |
| 171 | return out |
| 172 | } |
| 173 | |
| 174 | // Helpers |
| 175 | |
| 176 | // gf_mult multiplies two 16-byte blocks in GF(2^128) using the reduction |
| 177 | // polynomial from NIST SP 800-38D (the bit-reflected x^128 + x^7 + x^2 + x + 1). |
| 178 | @[direct_array_access] |
| 179 | fn gf_mult(x []u8, y []u8) []u8 { |
| 180 | mut z := zero_block.clone() |
| 181 | mut v := y.clone() |
| 182 | |
| 183 | // Process all 128 bits |
| 184 | for i in 0 .. 128 { |
| 185 | // Extract bit i from x (MSB first, left to right) |
| 186 | // byte index: i >> 3 or (i / 8) |
| 187 | // bit position: 7 - (i & 7) (7, 6, 5, ..., 0) |
| 188 | bit := (x[i >> 3] >> (7 - u8(i & 7))) & 1 |
| 189 | |
| 190 | // Create constant-time mask |
| 191 | // If bit = 1: mask = 0xFF (all 1s) |
| 192 | // If bit = 0: mask = 0x00 (all 0s) |
| 193 | // This is constant-time: -(i8(bit)) produces correct result |
| 194 | mask := u8(-(i8(bit))) |
| 195 | |
| 196 | // Conditional XOR: z ^= v * bit (constant-time) |
| 197 | // Instead of: if bit == 1 { z[j] ^= v[j] } |
| 198 | // We do: z[j] ^= (v[j] & mask) |
| 199 | for j in 0 .. block_size { |
| 200 | z[j] ^= (v[j] & mask) |
| 201 | } |
| 202 | |
| 203 | // Right shift v by 1 bit (always performed) |
| 204 | // This is constant-time - no branches |
| 205 | lsb := v[15] & 1 // Save LSB before shift |
| 206 | |
| 207 | for j := 15; j > 0; j-- { |
| 208 | v[j] = (v[j] >> 1) | ((v[j - 1] & 1) << 7) |
| 209 | } |
| 210 | v[0] >>= 1 |
| 211 | |
| 212 | // Polynomial reduction (constant-time) |
| 213 | // If lsb = 1: v[0] ^= 0xe1 |
| 214 | // If lsb = 0: v[0] unchanged |
| 215 | // Polynomial: x^128 + x^7 + x^2 + x + 1 = 0xe1 in lowest bits |
| 216 | // |
| 217 | // Instead of: if lsb == 1 { v[0] ^= 0xe1 } |
| 218 | // We do: v[0] ^= (0xe1 & mask_for_lsb) |
| 219 | red_mask := u8(-(i8(lsb))) |
| 220 | v[0] ^= (0xe1 & red_mask) |
| 221 | } |
| 222 | return z |
| 223 | } |
| 224 | |
| 225 | // pad_block returns `data` right-padded with zeros to a multiple of 16 bytes. |
| 226 | @[direct_array_access] |
| 227 | fn pad_block(data []u8) []u8 { |
| 228 | temp := data.clone() |
| 229 | rem := data.len % block_size |
| 230 | if rem == 0 { |
| 231 | return temp |
| 232 | } |
| 233 | mut out := []u8{cap: data.len + block_size - rem} |
| 234 | out << temp |
| 235 | out << []u8{len: block_size - rem} // padding |
| 236 | return out |
| 237 | } |
| 238 | |
| 239 | // inc32 increments the last 32 bits of the 16-byte counter block in place. |
| 240 | @[direct_array_access] |
| 241 | fn inc32(mut ctr []u8) { |
| 242 | mut c := (u32(ctr[12]) << 24) | (u32(ctr[13]) << 16) | (u32(ctr[14]) << 8) | u32(ctr[15]) |
| 243 | // detect for wrapping u32 counter |
| 244 | if (c + 1) == 0 { |
| 245 | panic('AES-GCM: inc32 overflow the counter') |
| 246 | } |
| 247 | c += 1 |
| 248 | ctr[12] = u8(c >> 24) |
| 249 | ctr[13] = u8(c >> 16) |
| 250 | ctr[14] = u8(c >> 8) |
| 251 | ctr[15] = u8(c) |
| 252 | } |
| 253 | |
| 254 | // len_block returns the GHASH length block: the bit lengths of the AAD and the |
| 255 | // ciphertext, each encoded as a big-endian 64-bit integer. |
| 256 | fn len_block(ad_len int, ct_len int) []u8 { |
| 257 | a := u64(ad_len) * 8 |
| 258 | c := u64(ct_len) * 8 |
| 259 | mut b := zero_block.clone() |
| 260 | for i in 0 .. 8 { |
| 261 | b[7 - i] = u8(a >> (8 * u32(i))) |
| 262 | b[15 - i] = u8(c >> (8 * u32(i))) |
| 263 | } |
| 264 | return b |
| 265 | } |
| 266 | |
| 267 | // j0 derives the pre-counter block for a 96-bit nonce: nonce || 0^31 || 1. |
| 268 | @[direct_array_access] |
| 269 | fn j0_from_nonce(nonce []u8) []u8 { |
| 270 | mut j := zero_block.clone() |
| 271 | for i in 0 .. 12 { |
| 272 | j[i] = nonce[i] |
| 273 | } |
| 274 | j[15] = 1 |
| 275 | return j |
| 276 | } |
| 277 | |
| 278 | // bytes_equal compares two equal-length byte slices in constant time. |
| 279 | // similar to `crypto.internal.subtle.constant_time_compare()` ones |
| 280 | @[direct_array_access] |
| 281 | fn bytes_equal(a []u8, b []u8) bool { |
| 282 | if a.len != b.len { |
| 283 | return false |
| 284 | } |
| 285 | mut diff := u8(0) |
| 286 | for i in 0 .. a.len { |
| 287 | diff |= a[i] ^ b[i] |
| 288 | } |
| 289 | return diff == 0 |
| 290 | } |
| 291 | |