session_id.nx source
↩ module page · 105 lines · 2922 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
22import "syscalls.nx"
23import "rand.nx"
24
25const SESSION_ID_LEN: i64 = 32
26
27// Map 6 bits to URL-safe alphabet.
28func si_map(v: i64) -> i64 {
29 if v < 26 { return 0x41 + v }
30 if v < 52 { return 0x61 + (v - 26) }
31 if v < 62 { return 0x30 + (v - 52) }
32 if v == 62 { return 0x2D } // '-'
33 return 0x5F // '_'
34}
35
36// Is byte a valid session-id character?
37func si_is_valid_char(b: i64) -> i64 {
38 if b >= 0x41 {
39 if b <= 0x5A { return 1 }
40 }
41 if b >= 0x61 {
42 if b <= 0x7A { return 1 }
43 }
44 if b >= 0x30 {
45 if b <= 0x39 { return 1 }
46 }
47 if b == 0x2D { return 1 }
48 if b == 0x5F { return 1 }
49 return 0
50}
51
52// Generate an `n`-char session ID. Returns bytes written.
53func session_id_with_len(out: *u8, n: i64) -> i64 {
54 rand_bytes(out, n)
55 var i: i64 = 0
56 while i < n {
57 out[i] = si_map(out[i] & 0x3F)
58 i = i + 1
59 }
60 return n
61}
62
63// Default 32-char session ID.
64func session_id_new(out: *u8) -> i64 {
65 return session_id_with_len(out, SESSION_ID_LEN)
66}
67
68// Format-validate. Returns 1 if id looks well-formed; 0
69// otherwise. Cheap pre-DB-lookup sanity check.
70func session_id_valid(id: *u8, n: i64) -> i64 {
71 if n != SESSION_ID_LEN { return 0 }
72 var i: i64 = 0
73 while i < n {
74 if si_is_valid_char(id[i]) == 0 { return 0 }
75 i = i + 1
76 }
77 return 1
78}
79
80// Compile-only smoke.
81func main() -> i64 {
82 let a: *u8 = sys_mmap(64)
83 let n: i64 = session_id_new(a)
84 if n != 32 { return 1 }
85 if session_id_valid(a, 32) != 1 { return 2 }
86
87 // Two IDs differ.
88 let b: *u8 = sys_mmap(64)
89 session_id_new(b)
90 var diff: i64 = 0
91 var i: i64 = 0
92 while i < 32 {
93 if a[i] != b[i] { diff = 1; break }
94 i = i + 1
95 }
96 if diff != 1 { return 3 }
97
98 // Invalid char rejected.
99 a[0] = 0x21 // '!'
100 if session_id_valid(a, 32) != 0 { return 4 }
101
102 // Wrong length rejected.
103 if session_id_valid(a, 30) != 0 { return 5 }
104 return 0
105}