code wiki / (root) / nx_aes_ctr.nx

nx_aes_ctr.nx source

↩ module page · 149 lines · 5938 B

1// nx_aes_ctr.nx -- AES-128 Counter (CTR) mode per NIST SP 800-38A §6.5. 2// 3// license_tier: INDEPENDENT_REDERIVE 4// genealogy_id: international-research-sources/nist/sp_800_38a 5// 6// Composes nx_aes (FIPS 197 AES-128 block cipher) with a counter to 7// produce a keystream, which is then XOR'd with plaintext to give 8// ciphertext. CTR is a SYMMETRIC mode: encrypt and decrypt are the 9// same operation (XOR is self-inverse). Foundation for AES-GCM 10// (which is CTR + GHASH). 11// 12// Counter format (this implementation): 13// bytes 0..11 : nonce / IV (caller-provided, must be unique per 14// message under the same key -- the "N" in NIST 15// "nonce") 16// bytes 12..15 : 32-bit big-endian counter starting at 1 17// 18// (RFC 3686 and TLS 1.3 use this same shape: 12-byte nonce + 4-byte 19// counter. SP 800-38A allows other splits; we choose the most- 20// deployed one.) 21// 22// Algorithm (SP 800-38A §6.5): 23// for each 16-byte block i of plaintext: 24// keystream_i = AES-Encrypt(counter_i, key) 25// ciphertext_i = plaintext_i XOR keystream_i 26// counter_i+1 = increment_32(counter_i) 27// 28// The final block may be partial: only len(plaintext_partial) bytes 29// of the keystream are used. 30// 31// SECURITY POSTURE: 32// CTR-only does NOT authenticate. An attacker that knows the 33// keystream (e.g. via plaintext recovery or nonce reuse) can flip 34// arbitrary ciphertext bits and the plaintext flips correspondingly 35// on decrypt. Production use must compose CTR with an authenticator 36// (HMAC, Poly1305, or full GCM with GHASH). Substrate's named 37// improvement: nx_aes_gcm.nx (queued). 38// 39// NONCE-REUSE WARNING: 40// Two messages encrypted with the same (key, nonce) leak the XOR 41// of the plaintexts (XOR cancels the shared keystream). Caller 42// MUST guarantee nonce uniqueness per key. 43// 44// nx_safety_envelope: (schema: nishi-library/seeds/safety-critical-standards.toml) 45// intended_use: "AES-128-CTR mode -- TLS 1.3 record encryption / 46// bulk file encryption / IoT transport" 47// sil_target: SIL3 (cipher composition; nonce-misuse 48// is a confidentiality break) 49// asil_target: QM 50// dal_target: DAL B 51// iec_62304_class: B 52// evidence: [no_floating_point, sealed_enum_complete, 53// bit_equal_reproducible, 54// NIST_SP_800-38A_Sec_F.5_KAT_VERIFIED, 55// inherits_nx_aes_FIPS_197_evidence_chain] 56// hazard_register: [bug-tape-nonce-reuse-disclosure, 57// bug-tape-counter-wrap-after-2^64-blocks, 58// bug-tape-IV-derived-from-low-entropy-source] 59// residual_risk: "Nonce uniqueness is CALLER RESPONSIBILITY. 60// Substrate cannot enforce uniqueness without 61// external state. TLS 1.3 callers use the 62// record sequence number; offline callers 63// MUST source nonce from a CSPRNG and never 64// re-encrypt under the same (key, nonce). 65// The cache-timing risk inherited from nx_aes 66// applies; bitslice variant queued." 67// verdict: NOT_YET_EVALUATED 68 69import "nx_syscalls.nx" 70import "nx_aes.nx" 71 72const AES_CTR_NONCE_LEN: i64 = 12 73 74// Increment the 32-bit big-endian counter at bytes 12..15 of `block`. 75// Pure overflow semantics: 0xffffffff -> 0x00000000 (caller's job to 76// avoid; 2^32 blocks under one nonce is the SP 800-38A cap anyway). 77 78func _aes_ctr_inc(block: *u8) -> i64 { 79 var i: i64 = 15 80 while i >= 12 { 81 let b: i64 = (block[i] as i64) & 0xff 82 if b == 255 { 83 block[i] = 0 84 i = i - 1 85 } else { 86 block[i] = (b + 1) & 0xff 87 return 0 88 } 89 } 90 return 0 91} 92 93// ICB layout: 12-byte nonce + 32-bit big-endian counter (MSB first). 94// Caller may build inline OR via aes128_ctr_build_icb. 95// 96// History note: this helper exposed an emit_sd_sp register-collision 97// bug in riscv.c (t6 used as both value-to-store and address scratch 98// on large-offset spills). Fixed 2026-05-16 in riscv.c; helper now 99// works end-to-end with multi-block ctr_xor. 100 101func aes128_ctr_build_icb(icb: *u8, nonce: *u8, start_counter: i64) -> i64 { 102 var i: i64 = 0 103 while i < AES_CTR_NONCE_LEN { icb[i] = nonce[i]; i = i + 1 } 104 icb[15] = start_counter & 0xff 105 icb[14] = (start_counter / 256) & 0xff 106 icb[13] = (start_counter / 65536) & 0xff 107 icb[12] = (start_counter / 16777216) & 0xff 108 return 0 109} 110 111// Public: AES-128-CTR. Encrypt or decrypt (symmetric). 112// Caller pre-builds the 16-byte ICB (e.g. via aes128_ctr_build_icb). 113// 114// sched: 176-byte expanded key from aes128_expand_key 115// icb: 16-byte initial counter block (will be modified in place) 116// in_buf: plaintext (encrypt) or ciphertext (decrypt) 117// in_len: bytes 118// out_buf: result (may alias in_buf) 119 120func aes128_ctr_xor(sched: *u8, icb: *u8, in_buf: *u8, in_len: i64, out_buf: *u8) -> i64 { 121 if in_len <= 0 { return 0 } 122 let keystream: *u8 = sys_mmap(32) 123 var off: i64 = 0 124 var i: i64 = 0 125 var take: i64 = 0 126 while off < in_len { 127 aes128_encrypt_block(icb, sched, keystream) 128 let remaining: i64 = in_len - off 129 if remaining < AES_BLOCK { 130 take = remaining 131 } else { 132 take = AES_BLOCK 133 } 134 i = 0 135 while i < take { 136 let p_in: *u8 = (in_buf as i64 + off + i) as *u8 137 let p_out: *u8 = (out_buf as i64 + off + i) as *u8 138 let pb: i64 = (p_in[0] as i64) & 0xff 139 let kb: i64 = (keystream[i] as i64) & 0xff 140 p_out[0] = (pb ^ kb) & 0xff 141 i = i + 1 142 } 143 off = off + take 144 if off < in_len { 145 _aes_ctr_inc(icb) 146 } 147 } 148 return in_len 149}