code wiki / _hdl_build / nx_doc_crypto.nx

nx_doc_crypto.nx source

↩ module page · 179 lines · 9031 B

1// nx_doc_crypto.nx -- ENCRYPTION-AT-REST core for CONFIDENTIAL docportal shards (client -prv-/-secret-/uploads; 2// NOT the public corpus). Envelope encryption (NIST SP 800-57): a master KEK wraps per-tenant DEKs; documents 3// are sealed with the DEK under a REAL AEAD. Grounds ENTIRELY in shipped, KAT-proven sovereign primitives -- 4// invents NO crypto: 5// AEAD = ChaCha20-Poly1305 (RFC 8439 2.8; nx_chacha20_poly1305, KAT-verified vs the 2.8.2 Sunscreen vector) 6// cipher = ChaCha20 (256-bit key) ; MAC = Poly1305 (constant-time compare) 7// CSPRNG = nx_csprng_fill (getrandom(2), /dev/urandom fallback) 8// 9// CRYPTO-SHRED reconciles Rule 13 (never-delete DATA) with GDPR Art 17 (erasure): the DATA store stays additive 10// (ciphertext history preserved), but the KEY store is the ONE place true deletion is correct -- destroying the 11// wrapped DEK erases ACCESS while the ciphertext bytes remain, permanently unrecoverable. So DEKs live in a 12// per-domain FILE (overwrite-with-random + truncate + unlink), NOT the additive seg-store, precisely because the 13// seg-store's never-delete law is wrong for a key that must be destroyable. 14// 15// STANDALONE lib -- wires into no daemon. HONEST LIMITATION: the KEK is a software file (0600) on the NAS, not an 16// HSM/TPM -- root or a disk image can read it (flag for the operator). license_tier: ORIGINAL 17import "nx_chacha20_poly1305.nx" // nx_chacha20_poly1305_encrypt/_decrypt + NX_AEAD_* consts + verdict codes 18import "nx_csprng.nx" // nx_csprng_fill (CSPRNG: getrandom(2)) 19import "nx_syscalls.nx" // file IO + AT_FDCWD + __syscall 20 21const DC_WRAP_BYTES: i64 = 60 // wrapped DEK = nonce(12) || ct(32) || tag(16) 22const DC_MODE_0600: i64 = 0x180 // rw------- : DEK + KEK files are owner-only 23 24func dc_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 25func dc_cat(out: *u8, o: i64, s: *u8) -> i64 { var i: i64 = 0; while s[i] != (0 as u8) { out[o] = s[i]; o = o + 1; i = i + 1 } return o } 26// per-domain wrapped-DEK path: knowledge/store/dek-<domain>.wrap (a FILE, so crypto_shred can truly destroy it) 27func dc_dek_path(domain: *u8, out: *u8) -> i64 { 28 var o: i64 = dc_cat(out, 0, "knowledge/store/dek-" as *u8) 29 o = dc_cat(out, o, domain) 30 o = dc_cat(out, o, ".wrap" as *u8) 31 out[o] = 0 as u8 32 return o 33} 34// read up to cap bytes of `path` into buf; returns bytes read, or -1 if the file cannot be opened. 35func dc_readfile(path: *u8, buf: *u8, cap: i64) -> i64 { 36 let fd: i64 = sys_openat_rd(path) 37 if fd < 0 { return 0 - 1 } 38 var total: i64 = 0 39 var go: i64 = 1 40 while go == 1 { 41 if total >= cap { go = 0 } else { 42 let m: i64 = sys_read(fd, (buf as i64 + total) as *u8, cap - total) 43 if m <= 0 { go = 0 } else { total = total + m } 44 } 45 } 46 sys_close(fd) 47 return total 48} 49// write n bytes to `path` (O_CREAT|O_WRONLY|O_TRUNC, mode), fsync. 0 ok / -1 fail. 50func dc_writefile(path: *u8, buf: *u8, n: i64, mode: i64) -> i64 { 51 let fd: i64 = sys_openat_wr(path, mode) 52 if fd < 0 { return 0 - 1 } 53 var off: i64 = 0 54 var go: i64 = 1 55 while go == 1 { 56 if off >= n { go = 0 } else { 57 let m: i64 = sys_write(fd, (buf as i64 + off) as *u8, n - off) 58 if m <= 0 { sys_close(fd); return 0 - 1 } else { off = off + m } 59 } 60 } 61 sys_fsync(fd) 62 sys_close(fd) 63 return 0 64} 65 66// Load the master KEK (32 bytes) into out32. If the KEK file is ABSENT, generate a fresh one via CSPRNG and 67// persist it 0600 (first-use bootstrap). NEVER regenerates over an existing KEK (a corrupt/short KEK -> FAIL, 68// not silent re-key which would orphan every DEK). 0 ok / <0 fail. 69func kek_ensure(out32: *u8) -> i64 { 70 let path: *u8 = "knowledge/store/doc_kek.master" as *u8 71 let fd: i64 = sys_openat_rd(path) 72 if fd >= 0 { 73 var total: i64 = 0 74 var go: i64 = 1 75 while go == 1 { 76 if total >= 32 { go = 0 } else { 77 let m: i64 = sys_read(fd, (out32 as i64 + total) as *u8, 32 - total) 78 if m <= 0 { go = 0 } else { total = total + m } 79 } 80 } 81 sys_close(fd) 82 if total == 32 { return 0 } 83 return 0 - 1 84 } 85 if nx_csprng_fill(out32, 32) != 0 { return 0 - 2 } 86 if dc_writefile(path, out32, 32, DC_MODE_0600) != 0 { return 0 - 3 } 87 return 0 88} 89 90// Provision a per-tenant DEK: CSPRNG 32-byte DEK -> AEAD-wrap under the KEK (AAD = domain, binds the wrap to the 91// tenant) -> persist [nonce||ct||tag] to the domain's .wrap file 0600. 0 ok / <0 fail. Idempotent-by-overwrite. 92func dek_gen(domain: *u8) -> i64 { 93 let kek: *u8 = sys_mmap(32) 94 if kek_ensure(kek) != 0 { return 0 - 1 } 95 let dek: *u8 = sys_mmap(32) 96 if nx_csprng_fill(dek, 32) != 0 { return 0 - 2 } 97 let wnonce: *u8 = sys_mmap(12) 98 if nx_csprng_fill(wnonce, 12) != 0 { return 0 - 3 } 99 let wrapped: *u8 = sys_mmap(64) 100 var i: i64 = 0 101 while i < 12 { wrapped[i] = wnonce[i]; i = i + 1 } 102 nx_chacha20_poly1305_encrypt(kek, wnonce, domain, dc_slen(domain), dek, 32, (wrapped as i64 + 12) as *u8, (wrapped as i64 + 44) as *u8) 103 let path: *u8 = sys_mmap(512); dc_dek_path(domain, path) 104 if dc_writefile(path, wrapped, DC_WRAP_BYTES, DC_MODE_0600) != 0 { return 0 - 4 } 105 return 0 106} 107 108// Unwrap the per-tenant DEK into out32. Reads the .wrap file, AEAD-opens it under the KEK (AAD = domain). 109// 0 ok / <0 fail -- a MISSING file (shredded/absent) or a failed tag (corrupt/overwritten) both FAIL, which is 110// exactly the crypto-shred guarantee: no DEK -> no decrypt. 111func dek_load(domain: *u8, out32: *u8) -> i64 { 112 let path: *u8 = sys_mmap(512); dc_dek_path(domain, path) 113 let wrapped: *u8 = sys_mmap(64) 114 let got: i64 = dc_readfile(path, wrapped, 64) 115 if got < DC_WRAP_BYTES { return 0 - 1 } 116 let kek: *u8 = sys_mmap(32) 117 if kek_ensure(kek) != 0 { return 0 - 2 } 118 let v: i64 = nx_chacha20_poly1305_decrypt(kek, wrapped, domain, dc_slen(domain), (wrapped as i64 + 12) as *u8, 32, (wrapped as i64 + 44) as *u8, out32) 119 if v != NX_AEAD_VERDICT_OK { return 0 - 3 } 120 return 0 121} 122 123// Seal a document: out = nonce(12) || ciphertext(ptlen) || tag(16). FRESH CSPRNG nonce per call (nonce reuse is 124// CATASTROPHIC for ChaCha20-Poly1305 -- Joux 2006). AAD = domain. Returns total bytes written, or <0 (no DEK). 125func doc_encrypt(domain: *u8, pt: *u8, ptlen: i64, out: *u8) -> i64 { 126 let dek: *u8 = sys_mmap(32) 127 if dek_load(domain, dek) != 0 { return 0 - 1 } 128 let nonce: *u8 = sys_mmap(16) 129 if nx_csprng_fill(nonce, 12) != 0 { return 0 - 2 } 130 var i: i64 = 0 131 while i < 12 { out[i] = nonce[i]; i = i + 1 } 132 nx_chacha20_poly1305_encrypt(dek, nonce, domain, dc_slen(domain), pt, ptlen, (out as i64 + 12) as *u8, (out as i64 + 12 + ptlen) as *u8) 133 return 12 + ptlen + 16 134} 135 136// Open a document (nonce||ct||tag). AEAD verifies the tag BEFORE releasing plaintext. Returns plaintext length, 137// or <0 on FAIL (no DEK, too short, or tag mismatch -- tamper/wrong-key). out gets the plaintext only on success. 138func doc_decrypt(domain: *u8, ct: *u8, ctlen: i64, out: *u8) -> i64 { 139 if ctlen < 28 { return 0 - 1 } 140 let dek: *u8 = sys_mmap(32) 141 if dek_load(domain, dek) != 0 { return 0 - 2 } 142 let bodylen: i64 = ctlen - 12 - 16 143 let v: i64 = nx_chacha20_poly1305_decrypt(dek, ct, domain, dc_slen(domain), (ct as i64 + 12) as *u8, bodylen, (ct as i64 + 12 + bodylen) as *u8, out) 144 if v != NX_AEAD_VERDICT_OK { return 0 - 3 } 145 return bodylen 146} 147 148// CRYPTO-SHRED: truly destroy the per-tenant DEK so its documents become permanently unrecoverable while their 149// ciphertext bytes remain in the additive data store. Overwrite the wrapped-DEK bytes with CSPRNG random (in 150// place), truncate to zero, then unlink -- then VERIFY dek_load fails. 0 ok / -1 if the DEK is still loadable. 151func crypto_shred(domain: *u8) -> i64 { 152 let path: *u8 = sys_mmap(512); dc_dek_path(domain, path) 153 // 1. overwrite the key material in place with random (defense-in-depth vs. raw-disk recovery) 154 let fd: i64 = sys_openat_rdwr(path, DC_MODE_0600) 155 if fd >= 0 { 156 let rnd: *u8 = sys_mmap(64) 157 nx_csprng_fill(rnd, DC_WRAP_BYTES) 158 sys_lseek(fd, 0, 0) 159 var off: i64 = 0 160 var go: i64 = 1 161 while go == 1 { 162 if off >= DC_WRAP_BYTES { go = 0 } else { 163 let m: i64 = sys_write(fd, (rnd as i64 + off) as *u8, DC_WRAP_BYTES - off) 164 if m <= 0 { go = 0 } else { off = off + m } 165 } 166 } 167 sys_fsync(fd) 168 sys_close(fd) 169 } 170 // 2. truncate the (now-random) bytes to zero length 171 let fdt: i64 = sys_openat_wr(path, DC_MODE_0600) 172 if fdt >= 0 { sys_close(fdt) } 173 // 3. unlink the entry (raw x86_64 unlinkat = 263, passes untranslated -- the fsync-74/unlinkat-263 precedent) 174 __syscall(263, AT_FDCWD, path as i64, 0, 0, 0, 0) 175 // 4. verify-gone: the DEK must no longer load 176 let dek: *u8 = sys_mmap(32) 177 if dek_load(domain, dek) == 0 { return 0 - 1 } 178 return 0 179}