code wiki / _hdl_build / nx_doc_authz.nx

nx_doc_authz.nx source

↩ module page · 80 lines · 5263 B

1// nx_doc_authz.nx -- PER-CLIENT EYES-ONLY authorization for confidential docs (the P2 vault read gate). Closes the 2// audit's most-dangerous gaps: no per-doc ownership, no authenticated private-read route (a WRITE-ONLY vault), and 3// the free domain request param. HIPAA minimum-necessary 164.502(b) + ABA Model Rule 1.6(c) + iManage ethical 4// walls: a confidential doc is readable ONLY by its OWNER (the authenticated subject who uploaded it) -- or a 5// future attorney/partner tier -- and DENIED to everyone else, FAIL-CLOSED. Composes nx_doc_at_rest (doc_get 6// decrypts) + the sovereign seg_store (own:<cid> ownership row, additive alongside doc:<cid> / pol:<cid>). 7// STANDALONE lib -- wires into no daemon (the operator-approved swap is a later step, like P1). license_tier: ORIGINAL 8import "nx_doc_at_rest.nx" // doc_get (decrypt confidential / passthrough public) + at_rest_confidential 9import "nx_docportal_lib.nx" // dp_prefix + DP_VIS_* + ss_begin/ss_add/ss_commit/ss_next_segid/ss_open/ss_hget 10 11// Attorney/partner tier -- the OR path in the need-to-know rule. Set ABOVE the daemon's CURRENT hardcoded session 12// level (2), so it is UNREACHABLE today => read is OWNER-ONLY until a REAL role model assigns level >= 3. This is a 13// deliberately fail-safe forward hook: the tier bypass cannot fire until roles exist (see the gate + flags). 14const AUTHZ_TIER_OWNER: i64 = 3 15 16func az_slen(s: *u8) -> i64 { var n: i64 = 0; while s[n] != (0 as u8) { n = n + 1 } return n } 17func az_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 } 18func az_eqbytes(a: *u8, b: *u8, n: i64) -> i64 { var i: i64 = 0; while i < n { if a[i] != b[i] { return 0 } i = i + 1 } return 1 } 19// "own:<cid>" ownership key 20func az_ownkey(cid: i64, out: *u8) -> i64 { 21 var o: i64 = az_cat(out, 0, "own:" as *u8) 22 if cid == 0 { out[o] = 48 as u8; o = o + 1; out[o] = 0 as u8; return o } 23 let t: *u8 = sys_mmap(28); var k: i64 = 0; var m: i64 = cid 24 while m > 0 { t[k] = (48 + (m % 10)) as u8; m = m / 10; k = k + 1 } 25 var i: i64 = 0; while i < k { out[o] = t[k - 1 - i]; o = o + 1; i = i + 1 } 26 out[o] = 0 as u8; return o 27} 28 29// Record the owner (the authenticated subject) of a doc -- own:<cid> = subject, additive in the (domain,vis) shard. 30// Call at CONFIDENTIAL upload time (the daemon, having the session subject, wires this next to doc_put). 0 ok / <0. 31func authz_set_owner(domain: *u8, vis: i64, cid: i64, subject: *u8) -> i64 { 32 let prefix: *u8 = sys_mmap(512); dp_prefix(domain, vis, prefix) 33 let okey: *u8 = sys_mmap(64); az_ownkey(cid, okey) 34 let sl: i64 = az_slen(subject) 35 let w: *i64 = ss_begin() 36 if ss_add(w, 1, okey, subject, sl) < 0 { return 0 - 1 } 37 let segid: i64 = ss_next_segid(prefix) 38 if ss_commit(prefix, w, segid) != 0 { return 0 - 2 } 39 return 0 40} 41 42// Read the owner subject into out (null-terminated). Returns length, 0 if NO ownership record (-> fail-closed). 43func authz_get_owner(domain: *u8, vis: i64, cid: i64, out: *u8, outcap: i64) -> i64 { 44 let prefix: *u8 = sys_mmap(512); dp_prefix(domain, vis, prefix) 45 let h: *i64 = ss_open(prefix) 46 if (h as i64) == 0 { return 0 } 47 let okey: *u8 = sys_mmap(64); az_ownkey(cid, okey) 48 let pq: *i64 = sys_mmap(16) as *i64 49 let lq: *i64 = sys_mmap(16) as *i64 50 if ss_hget(h, okey, pq, lq) != 1 { return 0 } 51 let vp: *u8 = pq[0] as *u8 52 let vlen: i64 = lq[0] 53 var i: i64 = 0 54 while i < vlen { if i < outcap { out[i] = vp[i] } i = i + 1 } 55 if vlen < outcap { out[vlen] = 0 as u8 } 56 return vlen 57} 58 59// THE DECISION (pure, gate-lockable). PUBLIC -> anyone. CONFIDENTIAL -> owner-only (own:<cid> == subject) OR the 60// attorney/partner tier (subject_level >= AUTHZ_TIER_OWNER, inert today). FAIL-CLOSED: no owner record / no match 61// -> 0. The caller MUST pass the SESSION-BOUND domain (never a free request param), so a subject can only reach its 62// OWN domain's shard -- cross-domain isolation is then inherent: this shard holds no owner row for another firm's cid. 63func authz_may_read(domain: *u8, vis: i64, cid: i64, subject: *u8, subject_level: i64) -> i64 { 64 if at_rest_confidential(vis) == 0 { return 1 } // PUBLIC corpus -> readable by anyone 65 let owner: *u8 = sys_mmap(256) 66 let olen: i64 = authz_get_owner(domain, vis, cid, owner, 255) 67 if olen <= 0 { return 0 } // FAIL-CLOSED: unknown owner -> deny 68 let slen: i64 = az_slen(subject) 69 if olen == slen { if az_eqbytes(owner, subject, olen) == 1 { return 1 } } // owner match 70 if subject_level >= AUTHZ_TIER_OWNER { return 1 } // attorney/partner tier (inert today) 71 return 0 72} 73 74// THE READ ROUTE (the missing authenticated private-read; it also wires the encryption DECRYPT side). If permitted 75// -> doc_get (nx_doc_at_rest decrypts confidential, passes public through) : DENY. Returns plaintext length, or <0 76// on DENY / not-found / decrypt-fail. out receives plaintext ONLY on success. 77func authz_doc_get(domain: *u8, vis: i64, cid: i64, subject: *u8, subject_level: i64, out: *u8) -> i64 { 78 if authz_may_read(domain, vis, cid, subject, subject_level) == 0 { return 0 - 1 } 79 return doc_get(domain, vis, cid, out) 80}