nx_vault.nx source
↩ module page · 339 lines · 12989 B
1// nx_vault.nx -- sovereign secrets vault CLI (HashiCorp-Vault-class,
2// bits-up; ZERO third-party dependency).
3//
4// Operator 2026-05-27: "stored like hashicorp vault or something from
5// zero up so you can deploy on the nas easily and powerfully."
6//
7// Composes the substrate's at-rest crypto primitives (nx_password_vault:
8// HKDF-SHA256 key derivation + ChaCha20-Poly1305 AEAD). This file adds
9// the on-disk read/write + the put/get/init/list CLI.
10//
11// SEAL MODEL (auto-unseal, HashiCorp-style):
12// The vault store (NXVLT format) is encrypted at rest under a 32-byte
13// master key derived HKDF(salt, unseal_passphrase). The unseal
14// passphrase lives in a SEPARATE 0600 key file (the "unseal key").
15// Possession of the encrypted store alone cannot recover secrets.
16// Auto-unseal = the key file is machine-local, so no operator re-entry.
17// THREAT MODEL: disk theft of BOTH files (key + store together)
18// defeats it -- store them on different media for stronger posture, or
19// switch to passphrase-prompt mode (a future flag).
20//
21// USAGE (paths supplied by caller so the binary is $HOME-agnostic):
22// nx_vault init <keypath> <storepath>
23// nx_vault put <keypath> <storepath> <name> <value>
24// nx_vault get <keypath> <storepath> <name> # prints value to stdout
25// nx_vault list <keypath> <storepath> # prints names, one per line
26//
27// File format (big-endian), NXVLT version 1:
28// magic[8]="NXVLT\0\0\1" salt[32] nonce_base[12] n_entries[4]
29// per entry: name_len[2] name[name_len] ct_len[4] ct[ct_len] tag[16]
30//
31// license_tier: ORIGINAL (sovereign; composes RFC 8439 + RFC 5869 prims)
32
33import "nx_syscalls.nx"
34import "nx_csprng.nx"
35import "nx_password_vault.nx"
36
37const NXV_HDR_FIXED: i64 = 56 // 8 magic + 32 salt + 12 nonce + 4 n_entries
38const NXV_E_OK: i64 = 0
39const NXV_E_USAGE: i64 = 2
40const NXV_E_NO_KEY: i64 = 3
41const NXV_E_NO_STORE: i64 = 4
42const NXV_E_BAD_MAGIC: i64 = 5
43const NXV_E_NOT_FOUND: i64 = 6
44const NXV_E_DECRYPT: i64 = 7
45const NXV_E_IO: i64 = 8
46
47// ---- small helpers -----------------------------------------------
48func v_strlen(s: *u8) -> i64 {
49 var n: i64 = 0
50 while s[n] != 0 { n = n + 1 }
51 return n
52}
53
54func v_eq(a: *u8, alen: i64, b: *u8, blen: i64) -> i64 {
55 if alen != blen { return 0 }
56 var i: i64 = 0
57 while i < alen {
58 if a[i] != b[i] { return 0 }
59 i = i + 1
60 }
61 return 1
62}
63
64// big-endian read/write
65func be_r16(p: *u8, off: i64) -> i64 {
66 return ((p[off] as i64) << 8) | (p[off + 1] as i64)
67}
68func be_r32(p: *u8, off: i64) -> i64 {
69 return ((p[off] as i64) << 24) | ((p[off+1] as i64) << 16) | ((p[off+2] as i64) << 8) | (p[off+3] as i64)
70}
71func be_w16(p: *u8, off: i64, v: i64) -> i64 {
72 p[off] = ((v >> 8) & 0xff) as u8
73 p[off+1] = (v & 0xff) as u8
74 return 0
75}
76func be_w32(p: *u8, off: i64, v: i64) -> i64 {
77 p[off] = ((v >> 24) & 0xff) as u8
78 p[off+1] = ((v >> 16) & 0xff) as u8
79 p[off+2] = ((v >> 8) & 0xff) as u8
80 p[off+3] = (v & 0xff) as u8
81 return 0
82}
83
84func write_file_mode(path: *u8, buf: *u8, n: i64, mode: i64) -> i64 {
85 let fd: i64 = sys_openat_wr(path, mode)
86 if fd < 0 { return NXV_E_IO }
87 var off: i64 = 0
88 while off < n {
89 let w: i64 = sys_write(fd, ((buf as i64) + off) as *u8, n - off)
90 if w <= 0 { sys_close(fd); return NXV_E_IO }
91 off = off + w
92 }
93 sys_close(fd)
94 return NXV_E_OK
95}
96
97// Write the NXVLT magic into p[0..8].
98func put_magic(p: *u8) -> i64 {
99 p[0] = 0x4E as u8 // N
100 p[1] = 0x58 as u8 // X
101 p[2] = 0x56 as u8 // V
102 p[3] = 0x4C as u8 // L
103 p[4] = 0x54 as u8 // T
104 p[5] = 0
105 p[6] = 0
106 p[7] = 1
107 return 0
108}
109func check_magic(p: *u8) -> i64 {
110 if p[0] != (0x4E as u8) { return 0 }
111 if p[1] != (0x58 as u8) { return 0 }
112 if p[2] != (0x56 as u8) { return 0 }
113 if p[3] != (0x4C as u8) { return 0 }
114 if p[4] != (0x54 as u8) { return 0 }
115 return 1
116}
117
118// Derive the master key from the unseal-key-file contents + the store salt.
119func derive_master(keypath: *u8, salt: *u8, key_out: *u8) -> i64 {
120 let klen_box: *i64 = (sys_mmap(8)) as *i64
121 klen_box[0] = 0
122 let pass: *u8 = sys_read_file(keypath, klen_box)
123 if (pass as i64) == 0 { return NXV_E_NO_KEY }
124 if klen_box[0] <= 0 { return NXV_E_NO_KEY }
125 nx_vault_derive_key(pass, klen_box[0], salt, NX_VAULT_SALT_LEN, key_out)
126 return NXV_E_OK
127}
128
129// ---- commands ----------------------------------------------------
130
131func cmd_init(keypath: *u8, storepath: *u8) -> i64 {
132 // Generate a 64-byte random unseal passphrase -> key file (0600).
133 let pass: *u8 = sys_mmap(64)
134 nx_csprng_fill(pass, 64)
135 let krc: i64 = write_file_mode(keypath, pass, 64, 0x180) // 0600
136 if krc != NXV_E_OK { return krc }
137 // Empty store: magic + random salt + random nonce_base + n_entries=0
138 let store: *u8 = sys_mmap(NXV_HDR_FIXED)
139 put_magic(store)
140 nx_csprng_fill(((store as i64) + 8) as *u8, NX_VAULT_SALT_LEN) // salt
141 nx_csprng_fill(((store as i64) + 40) as *u8, NX_VAULT_NONCE_LEN) // nonce_base
142 be_w32(store, 52, 0) // n_entries
143 let src: i64 = write_file_mode(storepath, store, NXV_HDR_FIXED, 0x180)
144 if src != NXV_E_OK { return src }
145 sys_write(1, "vault: initialized (sealed; unseal key written 0600)\n" as *u8, 53)
146 return NXV_E_OK
147}
148
149// Find an entry by name; returns its byte offset in `buf` (offset of the
150// name_len field) or -1. Sets *idx_box to its array index.
151func find_entry(buf: *u8, n: i64, name: *u8, nlen: i64, idx_box: *i64) -> i64 {
152 let n_entries: i64 = be_r32(buf, 52)
153 var off: i64 = NXV_HDR_FIXED
154 var idx: i64 = 0
155 while idx < n_entries {
156 let name_len: i64 = be_r16(buf, off)
157 let name_ptr: *u8 = ((buf as i64) + off + 2) as *u8
158 let ct_len: i64 = be_r32(buf, off + 2 + name_len)
159 if v_eq(name_ptr, name_len, name, nlen) == 1 {
160 idx_box[0] = idx
161 return off
162 }
163 // advance: 2 + name_len + 4 + ct_len + 16(tag)
164 off = off + 2 + name_len + 4 + ct_len + NX_VAULT_TAG_LEN
165 idx = idx + 1
166 }
167 idx_box[0] = n_entries
168 return 0 - 1
169}
170
171func cmd_get(keypath: *u8, storepath: *u8, name: *u8) -> i64 {
172 let len_box: *i64 = (sys_mmap(8)) as *i64
173 len_box[0] = 0
174 let buf: *u8 = sys_read_file(storepath, len_box)
175 if (buf as i64) == 0 { return NXV_E_NO_STORE }
176 if check_magic(buf) != 1 { return NXV_E_BAD_MAGIC }
177 let nlen: i64 = v_strlen(name)
178 let idx_box: *i64 = (sys_mmap(8)) as *i64
179 let off: i64 = find_entry(buf, len_box[0], name, nlen, idx_box)
180 if off < 0 { return NXV_E_NOT_FOUND }
181 let name_len: i64 = be_r16(buf, off)
182 let ct_off: i64 = off + 2 + name_len + 4
183 let ct_len: i64 = be_r32(buf, off + 2 + name_len)
184 let ct: *u8 = ((buf as i64) + ct_off) as *u8
185 let tag: *u8 = ((buf as i64) + ct_off + ct_len) as *u8
186 let key: *u8 = sys_mmap(NX_VAULT_KEY_LEN)
187 let dk: i64 = derive_master(keypath, ((buf as i64) + 8) as *u8, key)
188 if dk != NXV_E_OK { return dk }
189 let nonce_base: *u8 = ((buf as i64) + 40) as *u8
190 let pt: *u8 = sys_mmap(ct_len + 16)
191 let rc: i64 = nx_vault_decrypt_entry(key, nonce_base, idx_box[0], ct, ct_len, tag, pt)
192 if rc != NX_VAULT_VERDICT_OK { return NXV_E_DECRYPT }
193 sys_write(1, pt, ct_len)
194 return NXV_E_OK
195}
196
197func cmd_put(keypath: *u8, storepath: *u8, name: *u8, value: *u8) -> i64 {
198 let len_box: *i64 = (sys_mmap(8)) as *i64
199 len_box[0] = 0
200 let buf: *u8 = sys_read_file(storepath, len_box)
201 if (buf as i64) == 0 { return NXV_E_NO_STORE }
202 if check_magic(buf) != 1 { return NXV_E_BAD_MAGIC }
203 let old_len: i64 = len_box[0]
204 let nlen: i64 = v_strlen(name)
205 let vlen: i64 = v_strlen(value)
206
207 let key: *u8 = sys_mmap(NX_VAULT_KEY_LEN)
208 let dk: i64 = derive_master(keypath, ((buf as i64) + 8) as *u8, key)
209 if dk != NXV_E_OK { return dk }
210 let nonce_base: *u8 = ((buf as i64) + 40) as *u8
211
212 let idx_box: *i64 = (sys_mmap(8)) as *i64
213 let exist_off: i64 = find_entry(buf, old_len, name, nlen, idx_box)
214 let idx: i64 = idx_box[0] // existing idx, or n_entries for append
215
216 // Encrypt value at this idx (so nonce matches on get).
217 let ct: *u8 = sys_mmap(vlen + 16)
218 let tag: *u8 = sys_mmap(NX_VAULT_TAG_LEN)
219 let erc: i64 = nx_vault_encrypt_entry(key, nonce_base, idx, value, vlen, ct, tag)
220 if erc != NX_VAULT_VERDICT_OK { return NXV_E_IO }
221
222 // Rebuild store. If replacing, we must rewrite all entries because
223 // replacing in place only works when sizes match; simplest correct
224 // path: copy header, copy all entries except the replaced one
225 // (preserving their indices), then append the new/updated one LAST.
226 // BUT indices must stay stable for nonce reuse-safety, so for a
227 // REPLACE we keep the same idx + same position; for APPEND we add at
228 // the end. We rebuild linearly preserving order.
229 let new_cap: i64 = old_len + 2 + nlen + 4 + vlen + NX_VAULT_TAG_LEN + 64
230 let out: *u8 = sys_mmap(new_cap)
231 // header copy
232 var h: i64 = 0
233 while h < NXV_HDR_FIXED { out[h] = buf[h]; h = h + 1 }
234 let n_entries: i64 = be_r32(buf, 52)
235
236 var w: i64 = NXV_HDR_FIXED
237 var ridx: i64 = 0
238 var in_off: i64 = NXV_HDR_FIXED
239 var replaced: i64 = 0
240 while ridx < n_entries {
241 let e_namelen: i64 = be_r16(buf, in_off)
242 let e_ctlen: i64 = be_r32(buf, in_off + 2 + e_namelen)
243 let e_total: i64 = 2 + e_namelen + 4 + e_ctlen + NX_VAULT_TAG_LEN
244 if ridx == idx {
245 // replace this entry in place (same idx => same nonce)
246 be_w16(out, w, nlen); w = w + 2
247 var c1: i64 = 0
248 while c1 < nlen { out[w + c1] = name[c1]; c1 = c1 + 1 }
249 w = w + nlen
250 be_w32(out, w, vlen); w = w + 4
251 var c2: i64 = 0
252 while c2 < vlen { out[w + c2] = ct[c2]; c2 = c2 + 1 }
253 w = w + vlen
254 var c3: i64 = 0
255 while c3 < NX_VAULT_TAG_LEN { out[w + c3] = tag[c3]; c3 = c3 + 1 }
256 w = w + NX_VAULT_TAG_LEN
257 replaced = 1
258 } else {
259 // copy verbatim
260 var cc: i64 = 0
261 while cc < e_total { out[w + cc] = buf[in_off + cc]; cc = cc + 1 }
262 w = w + e_total
263 }
264 in_off = in_off + e_total
265 ridx = ridx + 1
266 }
267 var final_n: i64 = n_entries
268 if replaced == 0 {
269 // append new entry at idx == n_entries
270 be_w16(out, w, nlen); w = w + 2
271 var a1: i64 = 0
272 while a1 < nlen { out[w + a1] = name[a1]; a1 = a1 + 1 }
273 w = w + nlen
274 be_w32(out, w, vlen); w = w + 4
275 var a2: i64 = 0
276 while a2 < vlen { out[w + a2] = ct[a2]; a2 = a2 + 1 }
277 w = w + vlen
278 var a3: i64 = 0
279 while a3 < NX_VAULT_TAG_LEN { out[w + a3] = tag[a3]; a3 = a3 + 1 }
280 w = w + NX_VAULT_TAG_LEN
281 final_n = n_entries + 1
282 }
283 be_w32(out, 52, final_n)
284 let src: i64 = write_file_mode(storepath, out, w, 0x180)
285 if src != NXV_E_OK { return src }
286 sys_write(1, "vault: stored\n" as *u8, 14)
287 return NXV_E_OK
288}
289
290func cmd_list(keypath: *u8, storepath: *u8) -> i64 {
291 let len_box: *i64 = (sys_mmap(8)) as *i64
292 len_box[0] = 0
293 let buf: *u8 = sys_read_file(storepath, len_box)
294 if (buf as i64) == 0 { return NXV_E_NO_STORE }
295 if check_magic(buf) != 1 { return NXV_E_BAD_MAGIC }
296 let n_entries: i64 = be_r32(buf, 52)
297 var off: i64 = NXV_HDR_FIXED
298 var idx: i64 = 0
299 while idx < n_entries {
300 let name_len: i64 = be_r16(buf, off)
301 sys_write(1, ((buf as i64) + off + 2) as *u8, name_len)
302 sys_write(1, "\n" as *u8, 1)
303 let ct_len: i64 = be_r32(buf, off + 2 + name_len)
304 off = off + 2 + name_len + 4 + ct_len + NX_VAULT_TAG_LEN
305 idx = idx + 1
306 }
307 return NXV_E_OK
308}
309
310func argp(argv: *i64, i: i64) -> *u8 {
311 return (argv[i]) as *u8
312}
313
314func main(argc: i64, argv: *i64) -> i64 {
315 if argc < 4 {
316 sys_write(2, "usage: nx_vault <init|put|get|list> <keypath> <storepath> [name] [value]\n" as *u8, 72)
317 return NXV_E_USAGE
318 }
319 let cmd: *u8 = argp(argv, 1)
320 let keypath: *u8 = argp(argv, 2)
321 let storepath: *u8 = argp(argv, 3)
322
323 if v_eq(cmd, v_strlen(cmd), "init" as *u8, 4) == 1 {
324 return cmd_init(keypath, storepath)
325 }
326 if v_eq(cmd, v_strlen(cmd), "list" as *u8, 4) == 1 {
327 return cmd_list(keypath, storepath)
328 }
329 if v_eq(cmd, v_strlen(cmd), "get" as *u8, 3) == 1 {
330 if argc < 5 { sys_write(2, "get needs <name>\n" as *u8, 17); return NXV_E_USAGE }
331 return cmd_get(keypath, storepath, argp(argv, 4))
332 }
333 if v_eq(cmd, v_strlen(cmd), "put" as *u8, 3) == 1 {
334 if argc < 6 { sys_write(2, "put needs <name> <value>\n" as *u8, 25); return NXV_E_USAGE }
335 return cmd_put(keypath, storepath, argp(argv, 4), argp(argv, 5))
336 }
337 sys_write(2, "unknown command\n" as *u8, 16)
338 return NXV_E_USAGE
339}