nx_session_id.nx source
↩ module page · 111 lines · 3074 B
1// session_id.nx -- cryptographically strong session identifiers.
2//
3// Session IDs are the primary auth artefact after login. They
4// must be:
5// - Unpredictable (>= 128 bits entropy)
6// - URL + cookie-safe characters
7// - Fixed length (makes brute-force scans inherently bounded)
8// - Cheap to validate format before DB lookup
9//
10// This module generates 32-char session IDs = 192 bits entropy
11// over the URL-safe alphabet (A-Z a-z 0-9 - _). Matches the
12// security profile of Django / Rails / Express session IDs.
13//
14// Composes rand.nx.
15//
16// Invariants:
17// SI1 Every char from the 64-char URL-safe alphabet.
18// SI2 Length fixed at 32 (customisable via session_id_with_len).
19// SI3 Format-validation helper so callers can reject malformed
20// IDs before DB lookup.
21
22// nx_safety_envelope:
23// intended_use: AUTO_APPLIED -- primitive-specific tuning queued
24// sil_target: SIL1
25// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail]
26// verdict: NOT_YET_EVALUATED
27
28import "nx_syscalls.nx"
29import "nx_rand.nx"
30
31const SESSION_ID_LEN: i64 = 32
32
33// Map 6 bits to URL-safe alphabet.
34func si_map(v: i64) -> i64 {
35 if v < 26 { return 0x41 + v }
36 if v < 52 { return 0x61 + (v - 26) }
37 if v < 62 { return 0x30 + (v - 52) }
38 if v == 62 { return 0x2D } // '-'
39 return 0x5F // '_'
40}
41
42// Is byte a valid session-id character?
43func si_is_valid_char(b: i64) -> i64 {
44 if b >= 0x41 {
45 if b <= 0x5A { return 1 }
46 }
47 if b >= 0x61 {
48 if b <= 0x7A { return 1 }
49 }
50 if b >= 0x30 {
51 if b <= 0x39 { return 1 }
52 }
53 if b == 0x2D { return 1 }
54 if b == 0x5F { return 1 }
55 return 0
56}
57
58// Generate an `n`-char session ID. Returns bytes written.
59func session_id_with_len(out: *u8, n: i64) -> i64 {
60 rand_bytes(out, n)
61 var i: i64 = 0
62 while i < n {
63 out[i] = si_map(out[i] & 0x3F)
64 i = i + 1
65 }
66 return n
67}
68
69// Default 32-char session ID.
70func session_id_new(out: *u8) -> i64 {
71 return session_id_with_len(out, SESSION_ID_LEN)
72}
73
74// Format-validate. Returns 1 if id looks well-formed; 0
75// otherwise. Cheap pre-DB-lookup sanity check.
76func session_id_valid(id: *u8, n: i64) -> i64 {
77 if n != SESSION_ID_LEN { return 0 }
78 var i: i64 = 0
79 while i < n {
80 if si_is_valid_char(id[i]) == 0 { return 0 }
81 i = i + 1
82 }
83 return 1
84}
85
86// Compile-only smoke.
87func main() -> i64 {
88 let a: *u8 = sys_mmap(64)
89 let n: i64 = session_id_new(a)
90 if n != 32 { return 1 }
91 if session_id_valid(a, 32) != 1 { return 2 }
92
93 // Two IDs differ.
94 let b: *u8 = sys_mmap(64)
95 session_id_new(b)
96 var diff: i64 = 0
97 var i: i64 = 0
98 while i < 32 {
99 if a[i] != b[i] { diff = 1; break }
100 i = i + 1
101 }
102 if diff != 1 { return 3 }
103
104 // Invalid char rejected.
105 a[0] = 0x21 // '!'
106 if session_id_valid(a, 32) != 0 { return 4 }
107
108 // Wrong length rejected.
109 if session_id_valid(a, 30) != 0 { return 5 }
110 return 0
111}