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,360 @@
1
+ // rust/crypto/jwt/token.rs — HS256/EdDSA JWT token core (assembly + verification).
2
+ //
3
+ // Backend-framework feature: JWTs for auth. We implement HS256 by hand on top
4
+ // of the existing aws-lc-rs HMAC + base64 crates (no `jsonwebtoken` dep) to
5
+ // keep the dependency tree small and match this crate's byte-machinery style.
6
+ // RS256 (RSA via aws-lc-rs) can be layered on later.
7
+ //
8
+ // This is the pure token machinery: header constants, base64url (shared via
9
+ // `crate::crypto::base64`), HMAC-SHA256, split/build/verify, zero-DOM
10
+ // time-claim checks, and `iat`/`exp` injection. It is reused by the napi entry
11
+ // points (`napi.rs`), the C-ABI fast paths in `rust/ffi.rs`, and
12
+ // `crate::crypto::ed25519` keypair serialization. Only the
13
+ // `inject_and_payload_b64*` helpers touch napi types (`napi::Result`), because
14
+ // `rust/ffi.rs` reuses them; everything else is napi-free and unit-testable.
15
+
16
+ use memchr::memchr;
17
+ use napi::{Error, Result};
18
+ use sonic_rs::{JsonValueMutTrait, JsonValueTrait};
19
+ use std::sync::OnceLock;
20
+
21
+ use crate::crypto::base64::{base64url_decode_bytes, base64url_encode_bytes};
22
+
23
+ /// Clock-skew tolerance (seconds) for the `iat` claim in `jwt_verify`.
24
+ const CLOCK_SKEW_LEEWAY_SECS: i64 = 60;
25
+
26
+ /// The canonical HS256 JWT header (`{"alg":"HS256","typ":"JWT"}`), serialized
27
+ /// and base64url-encoded ONCE (lazily). `JwtSigner::sign`, the scalar
28
+ /// `jwt_sign` and the batch sign all reuse it instead of rebuilding +
29
+ /// re-serializing the constant header on every call.
30
+ static JWT_HEADER_B64: OnceLock<Vec<u8>> = OnceLock::new();
31
+
32
+ pub fn jwt_header_b64() -> &'static [u8] {
33
+ JWT_HEADER_B64.get_or_init(|| {
34
+ let header = serde_json::json!({ "alg": "HS256", "typ": "JWT" });
35
+ base64url_encode_bytes(
36
+ &serde_json::to_vec(&header).expect("constant JWT header serializes"),
37
+ )
38
+ })
39
+ }
40
+
41
+ /// The canonical EdDSA (Ed25519) JWT header (`{"alg":"EdDSA","typ":"JWT"}`),
42
+ /// serialized + base64url-encoded ONCE (lazily) — twin of `jwt_header_b64`.
43
+ static EDDSA_HEADER_B64: OnceLock<Vec<u8>> = OnceLock::new();
44
+
45
+ pub fn eddsa_header_b64() -> &'static [u8] {
46
+ EDDSA_HEADER_B64.get_or_init(|| {
47
+ let header = serde_json::json!({ "alg": "EdDSA", "typ": "JWT" });
48
+ base64url_encode_bytes(
49
+ &serde_json::to_vec(&header).expect("constant JWT header serializes"),
50
+ )
51
+ })
52
+ }
53
+
54
+ // ── HMAC-SHA256 (aws-lc-rs) ────────────────────────────────────
55
+
56
+ /// HMAC-SHA256 with a PRE-COMPILED key — avoids re-deriving the key on every
57
+ /// call (the win the `JwtSigner`/`HmacSigner` instances exploit).
58
+ pub fn hmac_sha256_with_key(key: &aws_lc_rs::hmac::Key, data: &[u8]) -> [u8; 32] {
59
+ let tag = aws_lc_rs::hmac::sign(key, data);
60
+ let mut out = [0u8; 32];
61
+ out.copy_from_slice(tag.as_ref());
62
+ out
63
+ }
64
+
65
+ // ── Token assembly / splitting ─────────────────────────────────
66
+
67
+ pub struct TokenParts<'a> {
68
+ pub header_b64: &'a [u8],
69
+ pub payload_b64: &'a [u8],
70
+ pub sig_b64: &'a [u8],
71
+ }
72
+
73
+ /// Split a compact JWT into its three dot-separated segments.
74
+ pub fn split_token(token: &[u8]) -> Option<TokenParts<'_>> {
75
+ let first = memchr(b'.', token)?;
76
+ let header_b64 = &token[..first];
77
+ let rest = &token[first + 1..];
78
+ let second = memchr(b'.', rest)?;
79
+ let payload_b64 = &rest[..second];
80
+ let sig_b64 = &rest[second + 1..];
81
+ if memchr(b'.', sig_b64).is_some() {
82
+ return None;
83
+ }
84
+ Some(TokenParts {
85
+ header_b64,
86
+ payload_b64,
87
+ sig_b64,
88
+ })
89
+ }
90
+
91
+ /// Build a full HS256 token from base64url-encoded header + payload segments.
92
+ pub fn build_token(header_b64: &[u8], payload_b64: &[u8], secret: &[u8]) -> Vec<u8> {
93
+ let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, secret);
94
+ build_token_with_key(header_b64, payload_b64, &key)
95
+ }
96
+
97
+ /// Build a token with a PRE-COMPILED key (per-call key derivation removed).
98
+ pub fn build_token_with_key(
99
+ header_b64: &[u8],
100
+ payload_b64: &[u8],
101
+ key: &aws_lc_rs::hmac::Key,
102
+ ) -> Vec<u8> {
103
+ let mut signing_input = Vec::with_capacity(header_b64.len() + 1 + payload_b64.len());
104
+ signing_input.extend_from_slice(header_b64);
105
+ signing_input.push(b'.');
106
+ signing_input.extend_from_slice(payload_b64);
107
+
108
+ let sig = hmac_sha256_with_key(key, &signing_input);
109
+ let sig_b64 = base64url_encode_bytes(&sig);
110
+
111
+ signing_input.push(b'.');
112
+ signing_input.extend_from_slice(&sig_b64);
113
+ signing_input
114
+ }
115
+
116
+ /// Constant-time byte comparison (no early exit on length mismatch beyond the
117
+ /// required guard — lengths differ → false).
118
+ fn ct_eq(a: &[u8], b: &[u8]) -> bool {
119
+ if a.len() != b.len() {
120
+ return false;
121
+ }
122
+ let mut diff = 0u8;
123
+ for (x, y) in a.iter().zip(b.iter()) {
124
+ diff |= x ^ y;
125
+ }
126
+ diff == 0
127
+ }
128
+
129
+ /// Verify an HS256 signature (constant-time). Does NOT check claims/expiry.
130
+ ///
131
+ /// Test-only convenience over the fresh-key derivation: production code uses
132
+ /// `verify_signature_with_key` with a precompiled key.
133
+ #[cfg(test)]
134
+ pub fn verify_signature(token: &[u8], secret: &[u8]) -> bool {
135
+ let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, secret);
136
+ verify_signature_with_key(token, &key)
137
+ }
138
+
139
+ /// Verify an HS256 signature (constant-time) with a PRE-COMPILED key.
140
+ pub fn verify_signature_with_key(token: &[u8], key: &aws_lc_rs::hmac::Key) -> bool {
141
+ let Some(parts) = split_token(token) else {
142
+ return false;
143
+ };
144
+
145
+ let mut signing_input =
146
+ Vec::with_capacity(parts.header_b64.len() + 1 + parts.payload_b64.len());
147
+ signing_input.extend_from_slice(parts.header_b64);
148
+ signing_input.push(b'.');
149
+ signing_input.extend_from_slice(parts.payload_b64);
150
+
151
+ let sig = hmac_sha256_with_key(key, &signing_input);
152
+
153
+ match base64url_decode_bytes(parts.sig_b64) {
154
+ Some(provided) => ct_eq(&sig, &provided),
155
+ None => false,
156
+ }
157
+ }
158
+
159
+ // ── Claim injection (iat/exp) ──────────────────────────────────
160
+
161
+ /// Inject `iat`/`exp` into object claims when `ttl_seconds` is set, then
162
+ /// serialize + base64url-encode the payload. Shared by the scalar, instance,
163
+ /// and byte-JSON sign paths so the injection semantics can't drift.
164
+ pub fn inject_and_payload_b64(
165
+ claims: &mut serde_json::Value,
166
+ ttl_seconds: Option<i64>,
167
+ now_seconds: i64,
168
+ ) -> Result<Vec<u8>> {
169
+ if let Some(obj) = claims.as_object_mut() {
170
+ if let Some(ttl) = ttl_seconds {
171
+ if ttl > 0 {
172
+ obj.entry("iat")
173
+ .or_insert_with(|| serde_json::json!(now_seconds));
174
+ obj.entry("exp")
175
+ .or_insert_with(|| serde_json::json!(now_seconds + ttl));
176
+ }
177
+ }
178
+ }
179
+ Ok(base64url_encode_bytes(
180
+ &serde_json::to_vec(claims).map_err(napi_err)?,
181
+ ))
182
+ }
183
+
184
+ /// Inject `iat`/`exp` into a sonic object when `ttl_seconds` is set (unless
185
+ /// already present) — shared by the sonic byte-sign paths.
186
+ pub(crate) fn inject_ttl_claims(
187
+ claims: &mut sonic_rs::Value,
188
+ ttl_seconds: Option<i64>,
189
+ now_seconds: i64,
190
+ ) {
191
+ if let Some(ttl) = ttl_seconds {
192
+ if ttl > 0 {
193
+ if let Some(obj) = claims.as_object_mut() {
194
+ if obj.get(&"iat").is_none() {
195
+ obj.insert("iat", now_seconds);
196
+ }
197
+ if obj.get(&"exp").is_none() {
198
+ obj.insert("exp", now_seconds + ttl);
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ /// Sonic twin of [`inject_and_payload_b64`] for the pre-serialized-claim-byte
206
+ /// sign paths (no `serde_json::Value` DOM): parse the claims once into a
207
+ /// compact `sonic_rs::Value`, inject `iat`/`exp`, serialize + base64url.
208
+ pub fn inject_and_payload_b64_sonic(
209
+ claims: &mut sonic_rs::Value,
210
+ ttl_seconds: Option<i64>,
211
+ now_seconds: i64,
212
+ ) -> Result<Vec<u8>> {
213
+ inject_ttl_claims(claims, ttl_seconds, now_seconds);
214
+ Ok(base64url_encode_bytes(
215
+ &sonic_rs::to_vec(claims).map_err(napi_err)?,
216
+ ))
217
+ }
218
+
219
+ fn napi_err(e: impl std::fmt::Display) -> Error {
220
+ Error::from_reason(e.to_string())
221
+ }
222
+
223
+ // ── Verification ───────────────────────────────────────────────
224
+
225
+ /// Pure-core JWT verification: HS256 signature + `alg` allowlist + time claims
226
+ /// (`exp`/`nbf`/`iat`). Returns the decoded payload BYTES (valid JSON claims),
227
+ /// or `None` on any failure — no `serde_json::Value` DOM and no re-serialize.
228
+ pub fn verify_token(token: &[u8], secret: &[u8], now_seconds: i64) -> Option<Vec<u8>> {
229
+ let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, secret);
230
+ verify_token_with_key(token, &key, now_seconds)
231
+ }
232
+
233
+ /// Zero-DOM time-claim checks over a base64url payload segment: `exp`
234
+ /// (reject when `now >= exp`), `nbf` (reject when not yet valid), `iat`
235
+ /// (reject tokens issued beyond the clock-skew leeway). Returns the decoded
236
+ /// payload bytes (valid JSON claims) or `None`. Shared by the HS256 and
237
+ /// EdDSA verify paths so the claim semantics can't drift.
238
+ pub(crate) fn verify_time_claims(payload_b64: &[u8], now_seconds: i64) -> Option<Vec<u8>> {
239
+ let payload = base64url_decode_bytes(payload_b64)?;
240
+ let value: sonic_rs::Value = sonic_rs::from_slice(&payload).ok()?;
241
+
242
+ // `exp`: reject when now >= exp.
243
+ if let Some(exp) = value.get("exp").and_then(|v| v.as_i64()) {
244
+ if now_seconds >= exp {
245
+ return None;
246
+ }
247
+ }
248
+
249
+ // `nbf` (not before): reject when the token is not yet valid.
250
+ if let Some(nbf) = value.get("nbf").and_then(|v| v.as_i64()) {
251
+ if now_seconds < nbf {
252
+ return None;
253
+ }
254
+ }
255
+
256
+ // `iat` (issued at): reject tokens issued in the future beyond a small
257
+ // clock-skew leeway.
258
+ if let Some(iat) = value.get("iat").and_then(|v| v.as_i64()) {
259
+ if now_seconds < iat.saturating_sub(CLOCK_SKEW_LEEWAY_SECS) {
260
+ return None;
261
+ }
262
+ }
263
+
264
+ Some(payload)
265
+ }
266
+
267
+ /// Verify with a PRE-COMPILED key (no per-call key derivation). Time-claim
268
+ /// checks run over a compact `sonic_rs::Value` (zero per-key heap `String`);
269
+ /// the decoded payload bytes are returned verbatim.
270
+ pub fn verify_token_with_key(
271
+ token: &[u8],
272
+ key: &aws_lc_rs::hmac::Key,
273
+ now_seconds: i64,
274
+ ) -> Option<Vec<u8>> {
275
+ if !verify_signature_with_key(token, key) {
276
+ return None;
277
+ }
278
+
279
+ let parts = split_token(token)?;
280
+
281
+ // ── Header: enforce the alg allowlist (prevents alg-confusion) ──
282
+ // Fast path: the canonical HS256 header matches — accept without parsing
283
+ // the header into a DOM. Otherwise fall back to the full parse (which
284
+ // accepts alternative key order / extra header fields), so behavior is
285
+ // identical to the previous always-parse path.
286
+ if parts.header_b64 != jwt_header_b64() {
287
+ let header: sonic_rs::Value =
288
+ sonic_rs::from_slice(&base64url_decode_bytes(parts.header_b64)?).ok()?;
289
+ if header.get("alg").and_then(|v| v.as_str()) != Some("HS256") {
290
+ return None;
291
+ }
292
+ }
293
+
294
+ // ── Payload: decode once; zero-DOM time-claim checks (shared) ──
295
+ verify_time_claims(parts.payload_b64, now_seconds)
296
+ }
297
+
298
+ // ── EdDSA (Ed25519) ────────────────────────────────────────────
299
+
300
+ /// Sign an EdDSA (Ed25519) JWT from pre-serialized claim JSON bytes. Claims
301
+ /// parsing/`iat`/`exp` injection is shared with the HS256 byte path; only the
302
+ /// signature primitive differs. Returns the compact token bytes, or `None` on
303
+ /// invalid claims JSON / invalid private key.
304
+ pub fn sign_eddsa(
305
+ claims_json: &[u8],
306
+ private_der: &[u8],
307
+ ttl_seconds: Option<i64>,
308
+ now_seconds: i64,
309
+ ) -> Option<Vec<u8>> {
310
+ let mut claims: sonic_rs::Value = sonic_rs::from_slice(claims_json).ok()?;
311
+ let payload_b64 = inject_and_payload_b64_sonic(&mut claims, ttl_seconds, now_seconds).ok()?;
312
+ build_eddsa_token(eddsa_header_b64(), &payload_b64, private_der)
313
+ }
314
+
315
+ /// Assemble an EdDSA token from base64url header + payload segments: sign
316
+ /// `header.payload` with the Ed25519 private key, append the base64url sig.
317
+ pub fn build_eddsa_token(
318
+ header_b64: &[u8],
319
+ payload_b64: &[u8],
320
+ private_der: &[u8],
321
+ ) -> Option<Vec<u8>> {
322
+ let mut signing_input = Vec::with_capacity(header_b64.len() + 1 + payload_b64.len());
323
+ signing_input.extend_from_slice(header_b64);
324
+ signing_input.push(b'.');
325
+ signing_input.extend_from_slice(payload_b64);
326
+ let sig = crate::crypto::ed25519::sign(private_der, &signing_input)?;
327
+ signing_input.push(b'.');
328
+ signing_input.extend_from_slice(&base64url_encode_bytes(&sig));
329
+ Some(signing_input)
330
+ }
331
+
332
+ /// Verify an EdDSA (Ed25519) JWT: signature + `alg` allowlist + time claims
333
+ /// (shared with the HS256 path). Returns the decoded payload bytes (valid JSON
334
+ /// claims), or `None` on any failure.
335
+ pub fn verify_token_eddsa(token: &[u8], public_der: &[u8], now_seconds: i64) -> Option<Vec<u8>> {
336
+ let parts = split_token(token)?;
337
+
338
+ // ── Header: enforce the alg allowlist (prevents alg-confusion) ──
339
+ // Fast path: the canonical EdDSA header matches — accept without parsing.
340
+ if parts.header_b64 != eddsa_header_b64() {
341
+ let header: sonic_rs::Value =
342
+ sonic_rs::from_slice(&base64url_decode_bytes(parts.header_b64)?).ok()?;
343
+ if header.get("alg").and_then(|v| v.as_str()) != Some("EdDSA") {
344
+ return None;
345
+ }
346
+ }
347
+
348
+ // ── Signature: constant-time Ed25519 verify over `header.payload` ──
349
+ let sig = base64url_decode_bytes(parts.sig_b64)?;
350
+ let mut signing_input =
351
+ Vec::with_capacity(parts.header_b64.len() + 1 + parts.payload_b64.len());
352
+ signing_input.extend_from_slice(parts.header_b64);
353
+ signing_input.push(b'.');
354
+ signing_input.extend_from_slice(parts.payload_b64);
355
+ if !crate::crypto::ed25519::verify(public_der, &signing_input, &sig) {
356
+ return None;
357
+ }
358
+
359
+ verify_time_claims(parts.payload_b64, now_seconds)
360
+ }
@@ -0,0 +1,27 @@
1
+ //! Auth & hashing (pure-Rust primitives + compiled-once napi instances).
2
+ //
3
+ // Pure-Rust crypto primitives + compiled-once napi instances (each instance
4
+ // precompiles its key/engine/params once at construction):
5
+ // - hmac_sha256.rs HMAC-SHA256 (+ HmacSigner instance)
6
+ // - cookie_sign.rs signed cookies `value.signature` (CookieSigner)
7
+ // - csrf.rs CSRF tokens, random hex + HMAC (CsrfProtector)
8
+ // - jwt/ hand-rolled HS256 + EdDSA JWT (token.rs / napi.rs)
9
+ // - aead.rs AES-256-GCM / chacha20-poly1305 (AeadCipher) + batch
10
+ // - argon2.rs argon2id password hashing (Argon2Hasher) + batch//! - bcrypt.rs bcrypt password hashing (PHC `$2b$`) + verify
11
+ //! - pbkdf2.rs PBKDF2-HMAC-SHA256 key derivation// - base64.rs base64/base64url/hex codecs (Base64Codec)
12
+ // - hashing.rs FNV-1a / XXH3 / crc32 checksums
13
+ // - random_token.rs random hex tokens
14
+
15
+ pub mod aead;
16
+ pub mod argon2;
17
+ pub mod base64;
18
+ pub mod bcrypt;
19
+ pub mod cookie_sign;
20
+ pub mod csrf;
21
+ pub mod ed25519;
22
+ pub mod hashing;
23
+ pub mod hmac_sha256;
24
+ pub mod jwt;
25
+ pub mod pbkdf2;
26
+ pub mod random_token;
27
+ pub mod session;
@@ -0,0 +1,105 @@
1
+ // rust/crypto/pbkdf2.rs — PBKDF2-HMAC-SHA256 key derivation.
2
+ //
3
+ // Pure-Rust (RustCrypto pbkdf2 + sha2). Standard NIST 800-132 KDF, exposed as
4
+ // a NAPI scalar. Bun has no synchronous PBKDF2 built-in (crypto.subtle.deriveBits
5
+ // is async), so this fills a real gap — the honest baseline is node:crypto's
6
+ // pbkdf2Sync, and @flux/native can prefer Bun's async path where a caller wants
7
+ // it. Output length is caller-chosen (default 32 bytes).
8
+
9
+ use napi::bindgen_prelude::*;
10
+ use napi_derive::napi;
11
+
12
+ use pbkdf2::pbkdf2_hmac;
13
+ use sha2::Sha256;
14
+
15
+ /// Minimum output length (1 byte) / maximum sanity cap (1 MiB).
16
+ pub const PBKDF2_MIN_LEN: u32 = 1;
17
+ /// Cap derived-key length so a hostile caller can't request a huge allocation.
18
+ pub const PBKDF2_MAX_LEN: u32 = 1024 * 1024;
19
+
20
+ /// Pure core (no napi types) for [`pbkdf2_sha256`] and the off-thread task
21
+ /// runtime: derive `dk_len` bytes, clamping the length to the documented range.
22
+ pub fn pbkdf2_sha256_into(password: &[u8], salt: &[u8], rounds: u32, dk_len: u32) -> Vec<u8> {
23
+ let dk_len = dk_len.clamp(PBKDF2_MIN_LEN, PBKDF2_MAX_LEN) as usize;
24
+ let mut out = vec![0u8; dk_len];
25
+ pbkdf2_hmac::<Sha256>(password, salt, rounds.max(1), &mut out);
26
+ out
27
+ }
28
+
29
+ /// Derive a PBKDF2-HMAC-SHA256 key: `out = PBKDF2(password, salt, rounds, dkLen)`.
30
+ #[napi]
31
+ pub fn pbkdf2_sha256(
32
+ password: Uint8Array,
33
+ salt: Uint8Array,
34
+ rounds: u32,
35
+ dk_len: u32,
36
+ ) -> Result<Buffer> {
37
+ let dk_len = dk_len.clamp(PBKDF2_MIN_LEN, PBKDF2_MAX_LEN);
38
+ let mut out = vec![0u8; dk_len as usize];
39
+ // PBKDF2 rounds must be >= 1 (a 0-round call would be meaningless).
40
+ let rounds = rounds.max(1);
41
+ pbkdf2_hmac::<Sha256>(password.as_ref(), salt.as_ref(), rounds, &mut out);
42
+ Ok(out.into())
43
+ }
44
+
45
+ #[cfg(test)]
46
+ mod tests {
47
+ use super::*;
48
+
49
+ #[test]
50
+ fn pbkdf2_sha256_known_vector() {
51
+ // RFC 7914 / NIST-style known answer: password "password", salt "salt",
52
+ // 1 iteration, 32 bytes — cross-checked with node:crypto pbkdf2Sync.
53
+ let out = pbkdf2_sha256(
54
+ Uint8Array::new(b"password".to_vec()),
55
+ Uint8Array::new(b"salt".to_vec()),
56
+ 1,
57
+ 32,
58
+ )
59
+ .unwrap();
60
+ let expected: [u8; 32] = [
61
+ 0x12, 0x0f, 0xb6, 0xcf, 0xfc, 0xf8, 0xb3, 0x2c, 0x43, 0xe7, 0x22, 0x52, 0x56, 0xc4,
62
+ 0xf8, 0x37, 0xa8, 0x65, 0x48, 0xc9, 0x2c, 0xcc, 0x35, 0x48, 0x08, 0x05, 0x98, 0x7c,
63
+ 0xb7, 0x0b, 0xe1, 0x7b,
64
+ ];
65
+ assert_eq!(out.as_ref(), &expected);
66
+ }
67
+
68
+ #[test]
69
+ fn pbkdf2_is_iteration_sensitive() {
70
+ let a = pbkdf2_sha256(
71
+ Uint8Array::new(b"pw".to_vec()),
72
+ Uint8Array::new(b"salt".to_vec()),
73
+ 1,
74
+ 16,
75
+ )
76
+ .unwrap();
77
+ let b = pbkdf2_sha256(
78
+ Uint8Array::new(b"pw".to_vec()),
79
+ Uint8Array::new(b"salt".to_vec()),
80
+ 2,
81
+ 16,
82
+ )
83
+ .unwrap();
84
+ assert_ne!(a.as_ref(), b.as_ref());
85
+ }
86
+
87
+ #[test]
88
+ fn pbkdf2_zero_rounds_treated_as_one() {
89
+ let one = pbkdf2_sha256(
90
+ Uint8Array::new(b"pw".to_vec()),
91
+ Uint8Array::new(b"salt".to_vec()),
92
+ 1,
93
+ 16,
94
+ )
95
+ .unwrap();
96
+ let zero = pbkdf2_sha256(
97
+ Uint8Array::new(b"pw".to_vec()),
98
+ Uint8Array::new(b"salt".to_vec()),
99
+ 0,
100
+ 16,
101
+ )
102
+ .unwrap();
103
+ assert_eq!(one.as_ref(), zero.as_ref());
104
+ }
105
+ }
@@ -0,0 +1,85 @@
1
+ // rust/crypto/random_token.rs — random hex tokens (16/32/64 bytes use stack buffers)
2
+ use crate::util::bytes::hex_encode;
3
+ use napi::bindgen_prelude::*;
4
+ use napi_derive::napi;
5
+
6
+ #[napi]
7
+ pub fn random_token(byte_len: u32) -> Result<Buffer> {
8
+ // Enterprise guard: never allocate more than this from a single call, even
9
+ // on API misuse — a u32 byte_len would otherwise permit a ~4 GiB alloc.
10
+ const MAX_BYTE_LEN: usize = 16 * 1024 * 1024;
11
+ if byte_len as usize > MAX_BYTE_LEN {
12
+ return Err(Error::from_reason(format!(
13
+ "random_token: byte_len {byte_len} exceeds max {MAX_BYTE_LEN}"
14
+ )));
15
+ }
16
+
17
+ let len = byte_len as usize;
18
+
19
+ // ⭐ Common sizes (16, 32, 64) use stack buffers — no heap alloc.
20
+ match len {
21
+ 16 => {
22
+ let mut bytes = [0u8; 16];
23
+ getrandom::fill(&mut bytes).map_err(|e| Error::from_reason(e.to_string()))?;
24
+ let mut out = [0u8; 32];
25
+ hex_encode(&bytes, &mut out);
26
+ Ok(Buffer::from(out.to_vec()))
27
+ }
28
+ 32 => {
29
+ let mut bytes = [0u8; 32];
30
+ getrandom::fill(&mut bytes).map_err(|e| Error::from_reason(e.to_string()))?;
31
+ let mut out = [0u8; 64];
32
+ hex_encode(&bytes, &mut out);
33
+ Ok(Buffer::from(out.to_vec()))
34
+ }
35
+ 64 => {
36
+ let mut bytes = [0u8; 64];
37
+ getrandom::fill(&mut bytes).map_err(|e| Error::from_reason(e.to_string()))?;
38
+ let mut out = [0u8; 128];
39
+ hex_encode(&bytes, &mut out);
40
+ Ok(Buffer::from(out.to_vec()))
41
+ }
42
+ _ => {
43
+ let out_len = len
44
+ .checked_mul(2)
45
+ .ok_or_else(|| Error::from_reason("byte_len too large"))?;
46
+ let mut bytes = vec![0u8; len];
47
+ getrandom::fill(&mut bytes).map_err(|e| Error::from_reason(e.to_string()))?;
48
+ let mut out = vec![0u8; out_len];
49
+ hex_encode(&bytes, &mut out);
50
+ Ok(Buffer::from(out))
51
+ }
52
+ }
53
+ }
54
+
55
+ #[cfg(test)]
56
+ mod tests {
57
+ use super::*;
58
+
59
+ #[test]
60
+ fn common_sizes_return_hex_of_requested_length() {
61
+ // The token is hex-encoded, so the output is exactly `len * 2` bytes
62
+ // of lowercase hex characters.
63
+ for len in [16u32, 32, 64, 7, 128] {
64
+ let out = random_token(len).expect("valid len must succeed");
65
+ assert_eq!(out.len(), len as usize * 2);
66
+ for b in out.iter() {
67
+ assert!(b.is_ascii_hexdigit(), "byte 0x{b:02x} is not hex");
68
+ }
69
+ }
70
+ }
71
+
72
+ #[test]
73
+ fn tokens_are_random() {
74
+ let a = random_token(32).unwrap();
75
+ let b = random_token(32).unwrap();
76
+ assert_ne!(a.to_vec(), b.to_vec(), "two tokens must not collide");
77
+ }
78
+
79
+ #[test]
80
+ fn huge_length_is_rejected() {
81
+ // The 16 MiB guard prevents a ~4 GiB single allocation from a u32 arg.
82
+ assert!(random_token(16 * 1024 * 1024 + 1).is_err());
83
+ assert!(random_token(u32::MAX).is_err());
84
+ }
85
+ }