castrum 0.9.3 → 0.9.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/Cargo.lock +2242 -0
  2. package/Cargo.toml +155 -0
  3. package/README.md +14 -6
  4. package/build.rs +5 -0
  5. package/castrum.darwin-arm64.node +0 -0
  6. package/castrum.darwin-x64.node +0 -0
  7. package/castrum.linux-arm64-gnu.node +0 -0
  8. package/castrum.linux-arm64-musl.node +0 -0
  9. package/castrum.linux-x64-gnu.node +0 -0
  10. package/castrum.linux-x64-musl.node +0 -0
  11. package/castrum.linux-x64-v3-gnu.node +0 -0
  12. package/castrum.win32-x64-msvc.node +0 -0
  13. package/dist/index.d.ts +1508 -1291
  14. package/dist/index.js +3391 -2069
  15. package/docs/API.md +2 -1
  16. package/docs/ARCHITECTURE.md +3 -2
  17. package/docs/CASE_STUDY.md +5 -3
  18. package/docs/ENVIRONMENT.md +25 -2
  19. package/docs/FFI_BUN_GUIDE.md +242 -14
  20. package/docs/HOW_TO_ADD_AN_OP.md +10 -0
  21. package/docs/REPO_MAP.md +17 -6
  22. package/docs/RND-CONCURRENCY.md +772 -0
  23. package/docs/bun-builtins-decision-matrix.md +4 -1
  24. package/index.ts +24 -25
  25. package/package.json +173 -156
  26. package/rust/crypto/aead.rs +378 -0
  27. package/rust/crypto/argon2.rs +265 -0
  28. package/rust/crypto/base64.rs +420 -0
  29. package/rust/crypto/bcrypt.rs +89 -0
  30. package/rust/crypto/cookie_sign.rs +169 -0
  31. package/rust/crypto/csrf.rs +106 -0
  32. package/rust/crypto/ed25519.rs +215 -0
  33. package/rust/crypto/hashing.rs +140 -0
  34. package/rust/crypto/hmac_sha256.rs +88 -0
  35. package/rust/crypto/jwt/api.rs +286 -0
  36. package/rust/crypto/jwt/mod.rs +22 -0
  37. package/rust/crypto/jwt/tests.rs +312 -0
  38. package/rust/crypto/jwt/token.rs +360 -0
  39. package/rust/crypto/mod.rs +27 -0
  40. package/rust/crypto/pbkdf2.rs +105 -0
  41. package/rust/crypto/random_token.rs +85 -0
  42. package/rust/crypto/session.rs +222 -0
  43. package/rust/ffi/crypto.rs +708 -0
  44. package/rust/ffi/hashing.rs +285 -0
  45. package/rust/ffi/http.rs +752 -0
  46. package/rust/ffi/ingress.rs +303 -0
  47. package/rust/ffi/json.rs +517 -0
  48. package/rust/ffi/jwt.rs +419 -0
  49. package/rust/ffi/metrics.rs +373 -0
  50. package/rust/ffi/mod.rs +69 -0
  51. package/rust/ffi/payload.rs +275 -0
  52. package/rust/ffi/probe.rs +60 -0
  53. package/rust/ffi/rate_limit.rs +77 -0
  54. package/rust/ffi/route.rs +85 -0
  55. package/rust/ffi/task.rs +172 -0
  56. package/rust/ffi/tests.rs +2333 -0
  57. package/rust/ffi/util.rs +152 -0
  58. package/rust/ffi/validators.rs +123 -0
  59. package/rust/http/accept.rs +606 -0
  60. package/rust/http/cookie_parser.rs +140 -0
  61. package/rust/http/etag.rs +289 -0
  62. package/rust/http/form.rs +136 -0
  63. package/rust/http/headers.rs +267 -0
  64. package/rust/http/http_date.rs +286 -0
  65. package/rust/http/http_parser.rs +252 -0
  66. package/rust/http/media_type.rs +355 -0
  67. package/rust/http/method.rs +200 -0
  68. package/rust/http/mime_lookup.rs +130 -0
  69. package/rust/http/mod.rs +32 -0
  70. package/rust/http/multipart.rs +685 -0
  71. package/rust/http/query_parser.rs +211 -0
  72. package/rust/http/url_codec.rs +296 -0
  73. package/rust/http/url_join.rs +324 -0
  74. package/rust/ingress/api.rs +355 -0
  75. package/rust/ingress/cors.rs +403 -0
  76. package/rust/ingress/ingress_constants.rs +209 -0
  77. package/rust/ingress/ip_trust.rs +312 -0
  78. package/rust/ingress/mod.rs +48 -0
  79. package/rust/ingress/native_route.rs +770 -0
  80. package/rust/ingress/options.rs +176 -0
  81. package/rust/ingress/output.rs +406 -0
  82. package/rust/ingress/packed.rs +191 -0
  83. package/rust/ingress/pipeline.rs +534 -0
  84. package/rust/ingress/proxy.rs +138 -0
  85. package/rust/ingress/rate_limit.rs +538 -0
  86. package/rust/ingress/terminal.rs +114 -0
  87. package/rust/ingress/tests.rs +567 -0
  88. package/rust/ingress/time.rs +70 -0
  89. package/rust/json/fast_schema/capture.rs +336 -0
  90. package/rust/json/fast_schema/compile.rs +556 -0
  91. package/rust/json/fast_schema/cursor.rs +368 -0
  92. package/rust/json/fast_schema/email.rs +282 -0
  93. package/rust/json/fast_schema/errors.rs +247 -0
  94. package/rust/json/fast_schema/mod.rs +57 -0
  95. package/rust/json/fast_schema/tests.rs +1143 -0
  96. package/rust/json/fast_schema/types.rs +140 -0
  97. package/rust/json/fast_schema/validate.rs +931 -0
  98. package/rust/json/json_ops.rs +121 -0
  99. package/rust/json/json_schema.rs +837 -0
  100. package/rust/json/json_ser.rs +883 -0
  101. package/rust/json/mod.rs +19 -0
  102. package/rust/json/napi_marshal.rs +318 -0
  103. package/rust/json/patch/api.rs +60 -0
  104. package/rust/json/patch/engine.rs +153 -0
  105. package/rust/json/patch/mod.rs +47 -0
  106. package/rust/json/patch/ops.rs +270 -0
  107. package/rust/json/patch/pointer.rs +49 -0
  108. package/rust/json/patch/tests.rs +499 -0
  109. package/rust/lib.rs +73 -0
  110. package/rust/metrics/api.rs +148 -0
  111. package/rust/metrics/mod.rs +21 -0
  112. package/rust/metrics/registry.rs +961 -0
  113. package/rust/panic_safety.rs +40 -0
  114. package/rust/payload/compress.rs +512 -0
  115. package/rust/payload/mod.rs +13 -0
  116. package/rust/payload/sse.rs +214 -0
  117. package/rust/payload/template.rs +253 -0
  118. package/rust/payload/websocket.rs +69 -0
  119. package/rust/payload/ws_frames.rs +354 -0
  120. package/rust/proptest_suite.rs +139 -0
  121. package/rust/selection.rs +95 -0
  122. package/rust/src/bin/native-bench.rs +495 -0
  123. package/rust/task/completion.rs +119 -0
  124. package/rust/task/mod.rs +65 -0
  125. package/rust/task/ops.rs +503 -0
  126. package/rust/task/runtime.rs +220 -0
  127. package/rust/task/tests.rs +231 -0
  128. package/rust/test_support.rs +71 -0
  129. package/rust/util/batch/api.rs +266 -0
  130. package/rust/util/batch/core.rs +473 -0
  131. package/rust/util/batch/mod.rs +23 -0
  132. package/rust/util/batch/tests.rs +445 -0
  133. package/rust/util/batch_core.rs +184 -0
  134. package/rust/util/bytes.rs +594 -0
  135. package/rust/util/mod.rs +34 -0
  136. package/rust/util/packed.rs +473 -0
  137. package/rust/util/text.rs +130 -0
  138. package/rust/util/threadpool.rs +128 -0
  139. package/rust/util/validation.rs +419 -0
  140. package/rust-toolchain.toml +17 -0
  141. package/scripts/postinstall.mjs +199 -0
  142. package/src/ingress/constants.ts +1 -1
  143. package/src/ingress/context.ts +2 -2
  144. package/src/ingress/decode/baked-result.ts +13 -13
  145. package/src/ingress/decode/fast-result.ts +17 -17
  146. package/src/ingress/errors.ts +7 -7
  147. package/src/ingress/headers/baked-templates.ts +1 -1
  148. package/src/ingress/headers/fast-templates.ts +3 -3
  149. package/src/ingress/health.ts +1 -2
  150. package/src/ingress/index.ts +1 -1
  151. package/src/ingress/native-route.ts +9 -5
  152. package/src/ingress/options.ts +1 -1
  153. package/src/ingress/packing/gather-raw-headers.ts +1 -1
  154. package/src/ingress/packing/route-wire.ts +1 -5
  155. package/src/ingress/response/error-bodies.ts +5 -5
  156. package/src/ingress/response/terminal.ts +1 -1
  157. package/src/ingress/routes/echo.ts +4 -4
  158. package/src/ingress/routes/fallback.ts +2 -2
  159. package/src/ingress/routes/head.ts +2 -2
  160. package/src/ingress/routes/native.ts +2 -1
  161. package/src/ingress/routes/options.ts +1 -1
  162. package/src/ingress/routes/read.ts +2 -2
  163. package/src/ingress/server.ts +10 -6
  164. package/src/ingress/status.ts +7 -7
  165. package/src/integration/batch.ts +1 -1
  166. package/src/integration/index.ts +9 -9
  167. package/src/integration/pipeline.ts +6 -6
  168. package/src/integration/websocket.ts +2 -2
  169. package/src/loader/index.ts +1 -5
  170. package/src/loader/ops.ts +1 -1
  171. package/src/native/ffi/build/codecs.ts +130 -12
  172. package/src/native/ffi/build/compress.ts +22 -10
  173. package/src/native/ffi/build/instances.ts +46 -4
  174. package/src/native/ffi/build/metrics.ts +247 -0
  175. package/src/native/ffi/build/parse.ts +59 -23
  176. package/src/native/ffi/build/task.ts +79 -0
  177. package/src/native/ffi/build/util.ts +32 -1
  178. package/src/native/ffi/build.ts +4 -0
  179. package/src/native/ffi/selftest.ts +12 -3
  180. package/src/native/ffi/types.ts +151 -9
  181. package/src/native/ffi.ts +160 -15
  182. package/src/native/index.ts +1 -1
  183. package/src/native/loader.ts +55 -5
  184. package/src/native/types/instances.ts +34 -0
  185. package/src/native/types.ts +11 -0
  186. package/src/runtime/codec.ts +53 -9
  187. package/src/runtime/types.ts +7 -0
  188. package/src/rust-ffi/batch/build.ts +16 -9
  189. package/src/rust-ffi/index.ts +7 -6
  190. package/src/rust-ffi/options.ts +1 -3
  191. package/src/rust-ffi/proven.ts +3 -3
  192. package/src/rust-ffi/scalar/crypto.ts +1 -1
  193. package/src/rust-ffi/scalar/factories.ts +73 -0
  194. package/src/rust-ffi/scalar/hashing.ts +1 -1
  195. package/src/rust-ffi/scalar/http.ts +71 -27
  196. package/src/rust-ffi/scalar/index.ts +2 -0
  197. package/src/rust-ffi/scalar/interface.ts +20 -0
  198. package/src/rust-ffi/scalar/payload.ts +11 -3
  199. package/src/rust-ffi/scalar/validation.ts +121 -0
  200. package/src/rust-ffi/text.ts +25 -3
  201. package/src/selection.ts +1 -1
  202. package/src/shared/bytes.ts +24 -4
  203. package/src/shared/codec.ts +18 -3
  204. package/src/task/index.ts +24 -0
  205. package/src/task/op.ts +171 -0
  206. package/src/task/runtime.ts +568 -0
@@ -0,0 +1,708 @@
1
+ // rust/ffi/crypto.rs — crypto C-ABI exports (hmac / cookies / csrf / passwords /
2
+ // aead / base64 / random token).
3
+ //
4
+ // Compiled keys go through `hmac_key_cached` (per-thread LRU — see `util.rs`);
5
+ // fallible / allocating cores route through `panic_guard`.
6
+
7
+ use aws_lc_rs::hmac;
8
+ use pbkdf2::pbkdf2_hmac;
9
+ use sha2::Sha256;
10
+ use std::slice;
11
+
12
+ use super::util::{aead_alg, cstring_return, hmac_key_cached, panic_guard};
13
+
14
+ /// HMAC-SHA256 verify → 1/0 (constant-time, hex sig compared after
15
+ /// whitespace-trim — mirrors the napi scalar path).
16
+ ///
17
+ /// # Safety
18
+ /// `key`/`data`/`sig` must be valid for reads of their lengths.
19
+ #[no_mangle]
20
+ pub unsafe extern "C" fn castrum_hmac_sha256_verify(
21
+ key: *const u8,
22
+ klen: usize,
23
+ data: *const u8,
24
+ dlen: usize,
25
+ sig: *const u8,
26
+ slen: usize,
27
+ ) -> u8 {
28
+ if key.is_null() || data.is_null() || sig.is_null() {
29
+ return 0;
30
+ }
31
+ let sig_t = crate::util::bytes::trim_ascii_whitespace(slice::from_raw_parts(sig, slen));
32
+ let Some(sig_bytes) = crate::util::bytes::hex_decode_32(sig_t) else {
33
+ return 0;
34
+ };
35
+ let k = hmac_key_cached(slice::from_raw_parts(key, klen));
36
+ u8::from(hmac::verify(&k, slice::from_raw_parts(data, dlen), &sig_bytes).is_ok())
37
+ }
38
+
39
+ /// CSRF constant-time verify → 1/0 (token format `<64-hex>.<64-hex>`).
40
+ ///
41
+ /// # Safety
42
+ /// `token`/`secret` must be valid for reads of their lengths.
43
+ #[no_mangle]
44
+ pub unsafe extern "C" fn castrum_csrf_verify(
45
+ token: *const u8,
46
+ tlen: usize,
47
+ secret: *const u8,
48
+ slen: usize,
49
+ ) -> u8 {
50
+ if token.is_null() || secret.is_null() {
51
+ return 0;
52
+ }
53
+ let k = hmac_key_cached(slice::from_raw_parts(secret, slen));
54
+ u8::from(crate::crypto::csrf::csrf_verify_with_key(
55
+ slice::from_raw_parts(token, tlen),
56
+ &k,
57
+ ))
58
+ }
59
+
60
+ /// Argon2id password verify → 1/0 (constant-time internally).
61
+ ///
62
+ /// # Safety
63
+ /// `password`/`phc` must be valid for reads of their lengths.
64
+ #[no_mangle]
65
+ pub unsafe extern "C" fn castrum_password_verify(
66
+ password: *const u8,
67
+ plen: usize,
68
+ phc: *const u8,
69
+ phclen: usize,
70
+ ) -> u8 {
71
+ if password.is_null() || phc.is_null() {
72
+ return 0;
73
+ }
74
+ u8::from(crate::crypto::argon2::verify_password(
75
+ slice::from_raw_parts(password, plen),
76
+ slice::from_raw_parts(phc, phclen),
77
+ ))
78
+ }
79
+
80
+ /// bcrypt password verify → 1/0 (PHC `$2b$` string).
81
+ ///
82
+ /// `hash` is a `bun:ffi` `cstring` ARG (the engine transcodes the JS PHC string
83
+ /// in-engine; the callee borrows via `CStr::from_ptr`). The password stays a
84
+ /// `(ptr,len)` byte slice — it may contain arbitrary bytes (including NUL).
85
+ ///
86
+ /// # Safety
87
+ /// `password` must be valid for reads of `plen` bytes; `hash` must be a valid
88
+ /// NUL-terminated C string.
89
+ #[no_mangle]
90
+ pub unsafe extern "C" fn castrum_password_verify_bcrypt(
91
+ password: *const u8,
92
+ plen: usize,
93
+ hash: *const std::os::raw::c_char,
94
+ ) -> u8 {
95
+ if password.is_null() || hash.is_null() {
96
+ return 0;
97
+ }
98
+ let Ok(h) = std::ffi::CStr::from_ptr(hash).to_str() else {
99
+ return 0;
100
+ };
101
+ u8::from(bcrypt::verify(slice::from_raw_parts(password, plen), h).unwrap_or(false))
102
+ }
103
+
104
+ /// HMAC-SHA256 hex (64 chars) into `out`.
105
+ ///
106
+ /// # Safety
107
+ /// `key`/`data` must be valid for reads of their lengths; `out` for `out_cap`.
108
+ #[no_mangle]
109
+ pub unsafe extern "C" fn castrum_hmac_sha256(
110
+ key: *const u8,
111
+ klen: usize,
112
+ data: *const u8,
113
+ dlen: usize,
114
+ out: *mut u8,
115
+ out_cap: usize,
116
+ ) -> usize {
117
+ if key.is_null() || data.is_null() || out.is_null() || out_cap < 64 {
118
+ return 0;
119
+ }
120
+ let k = hmac_key_cached(slice::from_raw_parts(key, klen));
121
+ let tag = hmac::sign(&k, slice::from_raw_parts(data, dlen));
122
+ let mut hex = [0u8; 64];
123
+ crate::util::bytes::hex_encode_32(tag.as_ref(), &mut hex);
124
+ slice::from_raw_parts_mut(out, 64).copy_from_slice(&hex);
125
+ 64
126
+ }
127
+
128
+ /// Sign a cookie `value` as `value.<64-hex sig>`, returned as a null-terminated
129
+ /// C string into the per-thread reused buffer (`cstring` return).
130
+ ///
131
+ /// # Safety
132
+ /// `value`/`secret` must be valid for reads of their lengths.
133
+ #[no_mangle]
134
+ pub unsafe extern "C" fn castrum_sign_cookie(
135
+ value: *const u8,
136
+ vlen: usize,
137
+ secret: *const u8,
138
+ slen: usize,
139
+ ) -> *const std::os::raw::c_char {
140
+ if value.is_null() || secret.is_null() {
141
+ return std::ptr::null();
142
+ }
143
+ let v = slice::from_raw_parts(value, vlen);
144
+ let key = hmac_key_cached(slice::from_raw_parts(secret, slen));
145
+ cstring_return(vlen + 65, |out| {
146
+ crate::crypto::cookie_sign::sign_cookie_into(v, &key, out)
147
+ })
148
+ }
149
+
150
+ /// Sign a cookie `value` as `value.<64-hex sig>` written directly into a caller
151
+ /// buffer — the pooled sibling of `castrum_sign_cookie` (no cstring round-trip;
152
+ /// this is the path `signCookieInto` must use). Writes `vlen + 65` bytes;
153
+ /// returns bytes written, the exact required size when `out_cap` is too small,
154
+ /// or 0 on invalid input.
155
+ ///
156
+ /// # Safety
157
+ /// `value`/`secret` must be valid for reads of their lengths; `out` for writes
158
+ /// up to `out_cap`.
159
+ #[no_mangle]
160
+ pub unsafe extern "C" fn castrum_sign_cookie_into(
161
+ value: *const u8,
162
+ vlen: usize,
163
+ secret: *const u8,
164
+ slen: usize,
165
+ out: *mut u8,
166
+ out_cap: usize,
167
+ ) -> usize {
168
+ if value.is_null() || secret.is_null() || out.is_null() {
169
+ return 0;
170
+ }
171
+ let needed = vlen + 65;
172
+ if out_cap < needed {
173
+ return needed;
174
+ }
175
+ let v = slice::from_raw_parts(value, vlen);
176
+ let key = hmac_key_cached(slice::from_raw_parts(secret, slen));
177
+ crate::crypto::cookie_sign::sign_cookie_into(v, &key, slice::from_raw_parts_mut(out, out_cap))
178
+ .unwrap_or_default()
179
+ }
180
+
181
+ /// Verify a signed cookie → the value without its signature, returned as a
182
+ /// null-terminated C string into the per-thread reused buffer. `null` on an
183
+ /// invalid signature / malformed input (the JS side maps to verify-fail).
184
+ ///
185
+ /// # Safety
186
+ /// `signed`/`secret` must be valid for reads of their lengths.
187
+ #[no_mangle]
188
+ pub unsafe extern "C" fn castrum_verify_cookie(
189
+ signed: *const u8,
190
+ slen: usize,
191
+ secret: *const u8,
192
+ klen: usize,
193
+ ) -> *const std::os::raw::c_char {
194
+ if signed.is_null() || secret.is_null() {
195
+ return std::ptr::null();
196
+ }
197
+ let s = slice::from_raw_parts(signed, slen);
198
+ let key = hmac_key_cached(slice::from_raw_parts(secret, klen));
199
+ cstring_return(slen, |out| {
200
+ crate::crypto::cookie_sign::verify_cookie_into(s, &key, out)
201
+ })
202
+ }
203
+
204
+ /// Verify a signed cookie → its value written directly into a caller buffer —
205
+ /// the pooled sibling of `castrum_verify_cookie` (no cstring round-trip; the
206
+ /// value length is unknown a priori, so the caller sizes to `slen` — the value
207
+ /// is always a prefix of `signed`). Returns the value length, the exact
208
+ /// required size when `out_cap` is too small, or 0 on invalid signature /
209
+ /// malformed input.
210
+ ///
211
+ /// # Safety
212
+ /// `signed`/`secret` must be valid for reads of their lengths; `out` for writes
213
+ /// up to `out_cap`.
214
+ #[no_mangle]
215
+ pub unsafe extern "C" fn castrum_verify_cookie_into(
216
+ signed: *const u8,
217
+ slen: usize,
218
+ secret: *const u8,
219
+ klen: usize,
220
+ out: *mut u8,
221
+ out_cap: usize,
222
+ ) -> usize {
223
+ if signed.is_null() || secret.is_null() || out.is_null() {
224
+ return 0;
225
+ }
226
+ let s = slice::from_raw_parts(signed, slen);
227
+ let key = hmac_key_cached(slice::from_raw_parts(secret, klen));
228
+ // The value is a prefix of `signed` (everything before the last `.`), so
229
+ // `slen` is always a sufficient bound — needed-size only differs when the
230
+ // caller passed less.
231
+ if out_cap < slen {
232
+ // Compute the actual needed length without writing (a full verify is
233
+ // wasted if we're just sizing; the caller retries at most once).
234
+ let dot = s.iter().rposition(|&b| b == b'.').unwrap_or(0);
235
+ if dot != 0 {
236
+ return dot;
237
+ }
238
+ return slen;
239
+ }
240
+ crate::crypto::cookie_sign::verify_cookie_into(s, &key, slice::from_raw_parts_mut(out, out_cap))
241
+ .unwrap_or_default()
242
+ }
243
+
244
+ /// CSRF token (`<64-hex(rnd)>.<64-hex(sig)>`, 129 bytes) returned as a
245
+ /// null-terminated C string into the per-thread reused buffer.
246
+ ///
247
+ /// # Safety
248
+ /// `secret` must be valid for reads of `slen` bytes.
249
+ #[no_mangle]
250
+ pub unsafe extern "C" fn castrum_csrf_token(
251
+ secret: *const u8,
252
+ slen: usize,
253
+ ) -> *const std::os::raw::c_char {
254
+ if secret.is_null() {
255
+ return std::ptr::null();
256
+ }
257
+ let k = hmac_key_cached(slice::from_raw_parts(secret, slen));
258
+ let mut rnd = [0u8; 32];
259
+ if getrandom::fill(&mut rnd).is_err() {
260
+ return std::ptr::null();
261
+ }
262
+ let mut rnd_hex = [0u8; 64];
263
+ crate::util::bytes::hex_encode(&rnd, &mut rnd_hex);
264
+ let mut sig_hex = [0u8; 64];
265
+ crate::util::bytes::hex_encode(hmac::sign(&k, &rnd_hex).as_ref(), &mut sig_hex);
266
+ cstring_return(129, |out| {
267
+ out[..64].copy_from_slice(&rnd_hex);
268
+ out[64] = b'.';
269
+ out[65..129].copy_from_slice(&sig_hex);
270
+ Some(129)
271
+ })
272
+ }
273
+
274
+ /// CSRF token (`<64-hex(rnd)>.<64-hex(sig)>`, 129 bytes) written directly into
275
+ /// a caller buffer — the pooled sibling of `castrum_csrf_token` (no cstring
276
+ /// round-trip). Returns 129, the exact required size when `out_cap` is too
277
+ /// small, or 0 on RNG failure.
278
+ ///
279
+ /// # Safety
280
+ /// `secret` must be valid for reads of `slen` bytes; `out` for writes up to `out_cap`.
281
+ #[no_mangle]
282
+ pub unsafe extern "C" fn castrum_csrf_token_into(
283
+ secret: *const u8,
284
+ slen: usize,
285
+ out: *mut u8,
286
+ out_cap: usize,
287
+ ) -> usize {
288
+ if secret.is_null() || out.is_null() {
289
+ return 0;
290
+ }
291
+ if out_cap < 129 {
292
+ return 129;
293
+ }
294
+ let k = hmac_key_cached(slice::from_raw_parts(secret, slen));
295
+ let mut rnd = [0u8; 32];
296
+ if getrandom::fill(&mut rnd).is_err() {
297
+ return 0;
298
+ }
299
+ let mut rnd_hex = [0u8; 64];
300
+ crate::util::bytes::hex_encode(&rnd, &mut rnd_hex);
301
+ let mut sig_hex = [0u8; 64];
302
+ crate::util::bytes::hex_encode(hmac::sign(&k, &rnd_hex).as_ref(), &mut sig_hex);
303
+ let o = slice::from_raw_parts_mut(out, 129);
304
+ o[..64].copy_from_slice(&rnd_hex);
305
+ o[64] = b'.';
306
+ o[65..129].copy_from_slice(&sig_hex);
307
+ 129
308
+ }
309
+
310
+ /// Argon2id password hash → PHC string bytes into `out` (m/t/p/out_len params).
311
+ ///
312
+ /// # Safety
313
+ /// `password`/`salt` must be valid for reads of their lengths; `out` for `out_cap`.
314
+ #[no_mangle]
315
+ pub unsafe extern "C" fn castrum_password_hash(
316
+ password: *const u8,
317
+ plen: usize,
318
+ salt: *const u8,
319
+ slen: usize,
320
+ m_cost: u32,
321
+ t_cost: u32,
322
+ p_cost: u32,
323
+ out_len: u32,
324
+ out: *mut u8,
325
+ out_cap: usize,
326
+ ) -> usize {
327
+ if password.is_null() || salt.is_null() || out.is_null() {
328
+ return 0;
329
+ }
330
+ let Some(phc) = panic_guard(
331
+ || {
332
+ crate::crypto::argon2::hash_password(
333
+ slice::from_raw_parts(password, plen),
334
+ slice::from_raw_parts(salt, slen),
335
+ m_cost,
336
+ t_cost,
337
+ p_cost,
338
+ out_len,
339
+ )
340
+ .ok()
341
+ },
342
+ None,
343
+ ) else {
344
+ return 0;
345
+ };
346
+ let bytes = phc.as_bytes();
347
+ if bytes.len() > out_cap {
348
+ // Needed-size convention: report the exact PHC length so the JS side
349
+ // can pre-size (or retry once) instead of re-running the hash.
350
+ return bytes.len();
351
+ }
352
+ slice::from_raw_parts_mut(out, bytes.len()).copy_from_slice(bytes);
353
+ bytes.len()
354
+ }
355
+
356
+ /// bcrypt password hash → `$2b$` PHC string into `out` (cost clamped 4..=31).
357
+ ///
358
+ /// # Safety
359
+ /// `password` must be valid for reads of `plen` bytes; `out` for `out_cap`.
360
+ #[no_mangle]
361
+ pub unsafe extern "C" fn castrum_password_hash_bcrypt(
362
+ password: *const u8,
363
+ plen: usize,
364
+ cost: u32,
365
+ out: *mut u8,
366
+ out_cap: usize,
367
+ ) -> usize {
368
+ if password.is_null() || out.is_null() {
369
+ return 0;
370
+ }
371
+ let Some(h) = panic_guard(
372
+ || {
373
+ bcrypt::hash(
374
+ slice::from_raw_parts(password, plen),
375
+ crate::crypto::bcrypt::clamp_cost(cost),
376
+ )
377
+ .ok()
378
+ },
379
+ None,
380
+ ) else {
381
+ return 0;
382
+ };
383
+ let bytes = h.as_bytes();
384
+ if bytes.len() > out_cap {
385
+ return 0;
386
+ }
387
+ slice::from_raw_parts_mut(out, bytes.len()).copy_from_slice(bytes);
388
+ bytes.len()
389
+ }
390
+
391
+ /// PBKDF2-HMAC-SHA256 → `dk_len` bytes into `out` (dk_len clamped 1..=1MiB).
392
+ ///
393
+ /// # Safety
394
+ /// `password`/`salt` must be valid for reads of their lengths; `out` for `out_cap`.
395
+ #[no_mangle]
396
+ pub unsafe extern "C" fn castrum_pbkdf2_sha256(
397
+ password: *const u8,
398
+ plen: usize,
399
+ salt: *const u8,
400
+ slen: usize,
401
+ rounds: u32,
402
+ dk_len: u32,
403
+ out: *mut u8,
404
+ out_cap: usize,
405
+ ) -> usize {
406
+ if password.is_null() || salt.is_null() || out.is_null() {
407
+ return 0;
408
+ }
409
+ let dk_len = dk_len.clamp(
410
+ crate::crypto::pbkdf2::PBKDF2_MIN_LEN,
411
+ crate::crypto::pbkdf2::PBKDF2_MAX_LEN,
412
+ ) as usize;
413
+ if out_cap < dk_len {
414
+ return 0;
415
+ }
416
+ pbkdf2_hmac::<Sha256>(
417
+ slice::from_raw_parts(password, plen),
418
+ slice::from_raw_parts(salt, slen),
419
+ rounds.max(1),
420
+ slice::from_raw_parts_mut(out, dk_len),
421
+ );
422
+ dk_len
423
+ }
424
+
425
+ /// AEAD encrypt → ciphertext+tag into `out`; `alg` 0 = AES-256-GCM, 1 = ChaCha20.
426
+ ///
427
+ /// # Safety
428
+ /// `key`/`nonce`/`plaintext` must be valid for reads of their lengths; `out` for `out_cap`.
429
+ #[no_mangle]
430
+ pub unsafe extern "C" fn castrum_aead_encrypt(
431
+ key: *const u8,
432
+ klen: usize,
433
+ nonce: *const u8,
434
+ nlen: usize,
435
+ plaintext: *const u8,
436
+ plen: usize,
437
+ alg: u8,
438
+ out: *mut u8,
439
+ out_cap: usize,
440
+ ) -> usize {
441
+ let Some(alg) = aead_alg(alg) else {
442
+ return 0;
443
+ };
444
+ if key.is_null() || nonce.is_null() || plaintext.is_null() || out.is_null() {
445
+ return 0;
446
+ }
447
+ let Some(ct) = panic_guard(
448
+ || {
449
+ crate::crypto::aead::encrypt(
450
+ alg,
451
+ slice::from_raw_parts(key, klen),
452
+ slice::from_raw_parts(nonce, nlen),
453
+ slice::from_raw_parts(plaintext, plen),
454
+ )
455
+ .ok()
456
+ },
457
+ None,
458
+ ) else {
459
+ return 0;
460
+ };
461
+ if ct.len() > out_cap {
462
+ return 0;
463
+ }
464
+ slice::from_raw_parts_mut(out, ct.len()).copy_from_slice(&ct);
465
+ ct.len()
466
+ }
467
+
468
+ /// AEAD decrypt → plaintext into `out`; returns 0 on auth failure / malformed.
469
+ ///
470
+ /// # Safety
471
+ /// `key`/`nonce`/`ciphertext` must be valid for reads of their lengths; `out` for `out_cap`.
472
+ #[no_mangle]
473
+ pub unsafe extern "C" fn castrum_aead_decrypt(
474
+ key: *const u8,
475
+ klen: usize,
476
+ nonce: *const u8,
477
+ nlen: usize,
478
+ ciphertext: *const u8,
479
+ clen: usize,
480
+ alg: u8,
481
+ out: *mut u8,
482
+ out_cap: usize,
483
+ ) -> usize {
484
+ let Some(alg) = aead_alg(alg) else {
485
+ return 0;
486
+ };
487
+ if key.is_null() || nonce.is_null() || ciphertext.is_null() || out.is_null() {
488
+ return 0;
489
+ }
490
+ let Some(pt) = panic_guard(
491
+ || {
492
+ crate::crypto::aead::decrypt(
493
+ alg,
494
+ slice::from_raw_parts(key, klen),
495
+ slice::from_raw_parts(nonce, nlen),
496
+ slice::from_raw_parts(ciphertext, clen),
497
+ )
498
+ .ok()
499
+ },
500
+ None,
501
+ ) else {
502
+ return 0;
503
+ };
504
+ if pt.len() > out_cap {
505
+ return 0;
506
+ }
507
+ slice::from_raw_parts_mut(out, pt.len()).copy_from_slice(&pt);
508
+ pt.len()
509
+ }
510
+
511
+ /// base64 encode into `out` with `url_safe`/`padding` u8 flags.
512
+ ///
513
+ /// # Safety
514
+ /// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
515
+ #[no_mangle]
516
+ pub unsafe extern "C" fn castrum_base64_encode(
517
+ data: *const u8,
518
+ len: usize,
519
+ out: *mut u8,
520
+ out_cap: usize,
521
+ url_safe: u8,
522
+ padding: u8,
523
+ ) -> usize {
524
+ if data.is_null() || out.is_null() {
525
+ return 0;
526
+ }
527
+ // Zero-alloc core (`Engine::encode_slice`) — no intermediate Vec. A
528
+ // too-small buffer yields 0 (the JS wrappers size exactly, so a miss is a
529
+ // caller bug / real error).
530
+ crate::crypto::base64::base64_encode_into_slice(
531
+ slice::from_raw_parts(data, len),
532
+ slice::from_raw_parts_mut(out, out_cap),
533
+ url_safe != 0,
534
+ padding != 0,
535
+ )
536
+ .unwrap_or_default()
537
+ }
538
+
539
+ /// base64 decode into `out`; returns 0 on invalid input / too-small buffer.
540
+ ///
541
+ /// # Safety
542
+ /// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
543
+ #[no_mangle]
544
+ pub unsafe extern "C" fn castrum_base64_decode(
545
+ data: *const u8,
546
+ len: usize,
547
+ out: *mut u8,
548
+ out_cap: usize,
549
+ url_safe: u8,
550
+ padding: u8,
551
+ ) -> usize {
552
+ if data.is_null() || out.is_null() {
553
+ return 0;
554
+ }
555
+ // Zero-alloc core (`Engine::decode_slice`) — no intermediate Vec.
556
+ // Invalid input OR too-small buffer → 0 (napi parity).
557
+ crate::crypto::base64::base64_decode_into_slice(
558
+ slice::from_raw_parts(data, len),
559
+ slice::from_raw_parts_mut(out, out_cap),
560
+ url_safe != 0,
561
+ padding != 0,
562
+ )
563
+ .unwrap_or_default()
564
+ }
565
+
566
+ /// Random hex token: `byte_len` random bytes → `byte_len*2` hex chars, returned
567
+ /// as a null-terminated C string into the per-thread reused buffer.
568
+ ///
569
+ /// # Safety
570
+ /// `byte_len` must be ≤ 16 MiB (else `null`).
571
+ #[no_mangle]
572
+ pub unsafe extern "C" fn castrum_random_token(byte_len: u32) -> *const std::os::raw::c_char {
573
+ const MAX: usize = 16 * 1024 * 1024;
574
+ let len = byte_len as usize;
575
+ let Some(out_len) = len.checked_mul(2) else {
576
+ return std::ptr::null();
577
+ };
578
+ if len > MAX {
579
+ return std::ptr::null();
580
+ }
581
+ let mut bytes = vec![0u8; len];
582
+ if getrandom::fill(&mut bytes).is_err() {
583
+ return std::ptr::null();
584
+ }
585
+ cstring_return(out_len, |out| {
586
+ crate::util::bytes::hex_encode(&bytes, out);
587
+ Some(out_len)
588
+ })
589
+ }
590
+
591
+ /// Random hex token written directly into a caller buffer — the pooled sibling
592
+ /// of `castrum_random_token` (no cstring round-trip on the Bun side). Writes
593
+ /// `byte_len*2` hex chars for `byte_len` random bytes. Returns bytes written
594
+ /// (`byte_len*2`), the exact required size when `out_cap` is too small, or 0 on
595
+ /// a real error (over the 16 MiB cap or the random source failing).
596
+ ///
597
+ /// # Safety
598
+ /// `out` must be valid for writes up to `out_cap` (and `out` non-null).
599
+ #[no_mangle]
600
+ pub unsafe extern "C" fn castrum_random_token_into(
601
+ byte_len: u32,
602
+ out: *mut u8,
603
+ out_cap: usize,
604
+ ) -> usize {
605
+ const MAX: usize = 16 * 1024 * 1024;
606
+ let len = byte_len as usize;
607
+ let Some(out_len) = len.checked_mul(2) else {
608
+ return 0;
609
+ };
610
+ if len > MAX || out.is_null() {
611
+ return 0;
612
+ }
613
+ if out_cap < out_len {
614
+ // Needed-size convention: report the exact required size.
615
+ return out_len;
616
+ }
617
+ let mut bytes = vec![0u8; len];
618
+ if getrandom::fill(&mut bytes).is_err() {
619
+ return 0;
620
+ }
621
+ let o = slice::from_raw_parts_mut(out, out_len);
622
+ crate::util::bytes::hex_encode(&bytes, o);
623
+ out_len
624
+ }
625
+
626
+ // ── Session envelope (fused JSON + HMAC) ────────────────────────────
627
+
628
+ /// Seal a session envelope NATIVELY: build
629
+ /// `{"id":"…","data":<data_json>,"exp":exp}` and HMAC-sign it into the
630
+ /// `payload.<64-hex>` cookie token — ONE crossing replaces the JS
631
+ /// `signCookie(JSON.stringify(envelope))` pair. `null` = empty id/secret /
632
+ /// panic. `data_json` is embedded verbatim (caller passes valid JSON).
633
+ ///
634
+ /// # Safety
635
+ /// All four args must be valid NUL-terminated C strings.
636
+ #[no_mangle]
637
+ pub unsafe extern "C" fn castrum_session_seal(
638
+ id: *const std::os::raw::c_char,
639
+ data_json: *const std::os::raw::c_char,
640
+ exp_secs: i64,
641
+ secret: *const std::os::raw::c_char,
642
+ ) -> *const std::os::raw::c_char {
643
+ if id.is_null() || data_json.is_null() || secret.is_null() {
644
+ return std::ptr::null();
645
+ }
646
+ let id_b = std::ffi::CStr::from_ptr(id).to_bytes();
647
+ let data_b = std::ffi::CStr::from_ptr(data_json).to_bytes();
648
+ let sec_b = std::ffi::CStr::from_ptr(secret).to_bytes();
649
+ let sealed = super::util::panic_guard(
650
+ || crate::crypto::session::seal_core(id_b, data_b, exp_secs, sec_b),
651
+ None,
652
+ );
653
+ let Some(tok) = sealed else {
654
+ return std::ptr::null();
655
+ };
656
+ super::util::cstring_return(tok.len(), move |buf| {
657
+ if buf.len() < tok.len() {
658
+ return None;
659
+ }
660
+ buf[..tok.len()].copy_from_slice(&tok);
661
+ Some(tok.len())
662
+ })
663
+ }
664
+
665
+ /// Open a sealed session token: verify the HMAC and extract the envelope in
666
+ /// ONE crossing. Packed output: `[u8 ok=1][i64 exp][u32 idLen][id][u32
667
+ /// dataLen][dataJson]`. Needed-size convention (`w > out_cap` = exact size);
668
+ /// `0` = bad signature / malformed / too-small-with-error semantics per the
669
+ /// caller contract below.
670
+ ///
671
+ /// # Safety
672
+ /// `token`/`secret` valid NUL-terminated C strings; `out` for writes up to
673
+ /// `out_cap`.
674
+ #[no_mangle]
675
+ pub unsafe extern "C" fn castrum_session_open(
676
+ token: *const std::os::raw::c_char,
677
+ secret: *const std::os::raw::c_char,
678
+ out: *mut u8,
679
+ out_cap: usize,
680
+ ) -> usize {
681
+ if token.is_null() || secret.is_null() || out.is_null() {
682
+ return 0;
683
+ }
684
+ let tok = std::ffi::CStr::from_ptr(token).to_bytes();
685
+ let sec = std::ffi::CStr::from_ptr(secret).to_bytes();
686
+ let opened = panic_guard(|| crate::crypto::session::open_core(tok, sec), None);
687
+ let Some((exp, id, data)) = opened else {
688
+ return 0;
689
+ };
690
+ // Layout size: 1 + 8 + 4+id + 4+data
691
+ let need = 1 + 8 + 4 + id.len() + 4 + data.len();
692
+ if need > out_cap {
693
+ return need;
694
+ }
695
+ let o = slice::from_raw_parts_mut(out, need);
696
+ o[0] = 1;
697
+ o[1..9].copy_from_slice(&exp.to_le_bytes());
698
+ let id_len = id.len() as u32;
699
+ o[9..13].copy_from_slice(&id_len.to_le_bytes());
700
+ let mut p = 13usize;
701
+ o[p..p + id.len()].copy_from_slice(&id);
702
+ p += id.len();
703
+ let d_len = data.len() as u32;
704
+ o[p..p + 4].copy_from_slice(&d_len.to_le_bytes());
705
+ p += 4;
706
+ o[p..p + data.len()].copy_from_slice(&data);
707
+ need
708
+ }