v4 / vlib / crypto / aes / aes_gcm.v
290 lines · 259 sloc · 8.69 KB · 63e43d751ce33dd9cd12ac385720ce28764486eb
Raw
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).
11module aes
12
13import crypto.cipher
14
15// gcm_tag_size is the size of the GCM authentication tag in bytes.
16pub 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).
19pub const gcm_nonce_size = 12
20
21// zero_block is a preallocated 16-bytes of zeros block. Its currently sized by Aes block size.
22const zero_block = []u8{len: block_size}
23
24// maximum plaintext length per invocation GCM: MUST be <= 2^36 - 32 octets.
25const max_plaintext_size = u64(1) << 36 - 32
26
27// maximum ciphertext length per invocation GCM: MUST be <= 2^36 - 16 octets.
28const 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.
33const max_aad_size = max_u32
34
35// AesGcm holds the AES block cipher and the derived GHASH subkey for a key.
36@[heap; noinit]
37pub struct AesGcm implements cipher.AEAD {
38mut:
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]
45pub 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).
59pub 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.
64pub 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]
71pub 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]
104pub 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]
138fn (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]
154fn (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]
179fn 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]
227fn 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]
241fn 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.
256fn 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]
269fn 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]
281fn 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