castrum 0.9.2 → 0.9.4
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.
- package/Cargo.lock +2259 -0
- package/Cargo.toml +138 -0
- package/README.md +6 -0
- package/build.rs +5 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/dist/index.d.ts +1348 -1295
- package/dist/index.js +4307 -3776
- package/docs/API.md +1 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +2 -2
- package/docs/ENVIRONMENT.md +16 -0
- package/docs/FFI_BUN_GUIDE.md +89 -6
- package/docs/REPO_MAP.md +2 -2
- package/index.ts +19 -25
- package/package.json +9 -1
- package/rust/crypto/aead.rs +378 -0
- package/rust/crypto/argon2.rs +265 -0
- package/rust/crypto/base64.rs +420 -0
- package/rust/crypto/bcrypt.rs +89 -0
- package/rust/crypto/cookie_sign.rs +169 -0
- package/rust/crypto/csrf.rs +106 -0
- package/rust/crypto/ed25519.rs +215 -0
- package/rust/crypto/hashing.rs +140 -0
- package/rust/crypto/hmac_sha256.rs +88 -0
- package/rust/crypto/jwt/api.rs +286 -0
- package/rust/crypto/jwt/mod.rs +22 -0
- package/rust/crypto/jwt/tests.rs +312 -0
- package/rust/crypto/jwt/token.rs +360 -0
- package/rust/crypto/mod.rs +27 -0
- package/rust/crypto/pbkdf2.rs +96 -0
- package/rust/crypto/random_token.rs +85 -0
- package/rust/crypto/session.rs +222 -0
- package/rust/ffi/crypto.rs +708 -0
- package/rust/ffi/hashing.rs +285 -0
- package/rust/ffi/http.rs +752 -0
- package/rust/ffi/ingress.rs +303 -0
- package/rust/ffi/json.rs +517 -0
- package/rust/ffi/jwt.rs +419 -0
- package/rust/ffi/metrics.rs +373 -0
- package/rust/ffi/mod.rs +68 -0
- package/rust/ffi/payload.rs +275 -0
- package/rust/ffi/probe.rs +60 -0
- package/rust/ffi/rate_limit.rs +77 -0
- package/rust/ffi/route.rs +85 -0
- package/rust/ffi/tests.rs +2333 -0
- package/rust/ffi/util.rs +152 -0
- package/rust/ffi/validators.rs +123 -0
- package/rust/http/accept.rs +606 -0
- package/rust/http/cookie_parser.rs +140 -0
- package/rust/http/etag.rs +289 -0
- package/rust/http/form.rs +132 -0
- package/rust/http/headers.rs +267 -0
- package/rust/http/http_date.rs +286 -0
- package/rust/http/http_parser.rs +252 -0
- package/rust/http/media_type.rs +355 -0
- package/rust/http/method.rs +200 -0
- package/rust/http/mime_lookup.rs +130 -0
- package/rust/http/mod.rs +32 -0
- package/rust/http/multipart.rs +685 -0
- package/rust/http/query_parser.rs +180 -0
- package/rust/http/url_codec.rs +296 -0
- package/rust/http/url_join.rs +324 -0
- package/rust/ingress/api.rs +355 -0
- package/rust/ingress/cors.rs +403 -0
- package/rust/ingress/ingress_constants.rs +209 -0
- package/rust/ingress/ip_trust.rs +312 -0
- package/rust/ingress/mod.rs +48 -0
- package/rust/ingress/native_route.rs +779 -0
- package/rust/ingress/options.rs +176 -0
- package/rust/ingress/output.rs +406 -0
- package/rust/ingress/packed.rs +191 -0
- package/rust/ingress/pipeline.rs +534 -0
- package/rust/ingress/proxy.rs +138 -0
- package/rust/ingress/rate_limit.rs +538 -0
- package/rust/ingress/terminal.rs +114 -0
- package/rust/ingress/tests.rs +566 -0
- package/rust/ingress/time.rs +70 -0
- package/rust/json/fast_schema/capture.rs +336 -0
- package/rust/json/fast_schema/compile.rs +556 -0
- package/rust/json/fast_schema/cursor.rs +368 -0
- package/rust/json/fast_schema/email.rs +282 -0
- package/rust/json/fast_schema/errors.rs +247 -0
- package/rust/json/fast_schema/mod.rs +57 -0
- package/rust/json/fast_schema/tests.rs +1143 -0
- package/rust/json/fast_schema/types.rs +140 -0
- package/rust/json/fast_schema/validate.rs +931 -0
- package/rust/json/json_ops.rs +121 -0
- package/rust/json/json_schema.rs +837 -0
- package/rust/json/json_ser.rs +881 -0
- package/rust/json/mod.rs +19 -0
- package/rust/json/napi_marshal.rs +318 -0
- package/rust/json/patch/api.rs +60 -0
- package/rust/json/patch/engine.rs +153 -0
- package/rust/json/patch/mod.rs +47 -0
- package/rust/json/patch/ops.rs +270 -0
- package/rust/json/patch/pointer.rs +49 -0
- package/rust/json/patch/tests.rs +499 -0
- package/rust/lib.rs +72 -0
- package/rust/metrics/api.rs +148 -0
- package/rust/metrics/mod.rs +21 -0
- package/rust/metrics/registry.rs +961 -0
- package/rust/panic_safety.rs +40 -0
- package/rust/payload/compress.rs +512 -0
- package/rust/payload/mod.rs +13 -0
- package/rust/payload/sse.rs +214 -0
- package/rust/payload/template.rs +253 -0
- package/rust/payload/websocket.rs +69 -0
- package/rust/payload/ws_frames.rs +354 -0
- package/rust/proptest_suite.rs +139 -0
- package/rust/selection.rs +95 -0
- package/rust/src/bin/native-bench.rs +495 -0
- package/rust/test_support.rs +71 -0
- package/rust/util/batch/api.rs +266 -0
- package/rust/util/batch/core.rs +473 -0
- package/rust/util/batch/mod.rs +23 -0
- package/rust/util/batch/tests.rs +445 -0
- package/rust/util/batch_core.rs +184 -0
- package/rust/util/bytes.rs +407 -0
- package/rust/util/mod.rs +34 -0
- package/rust/util/packed.rs +473 -0
- package/rust/util/text.rs +130 -0
- package/rust/util/threadpool.rs +128 -0
- package/rust/util/validation.rs +419 -0
- package/rust-toolchain.toml +17 -0
- package/scripts/postinstall.mjs +199 -0
- package/src/ingress/constants.ts +1 -1
- package/src/ingress/context.ts +2 -2
- package/src/ingress/decode/baked-result.ts +13 -13
- package/src/ingress/decode/fast-result.ts +17 -17
- package/src/ingress/errors.ts +7 -7
- package/src/ingress/headers/baked-templates.ts +1 -1
- package/src/ingress/headers/fast-templates.ts +3 -3
- package/src/ingress/index.ts +1 -1
- package/src/ingress/native-route.ts +9 -5
- package/src/ingress/options.ts +1 -1
- package/src/ingress/packing/gather-raw-headers.ts +1 -1
- package/src/ingress/packing/route-wire.ts +1 -5
- package/src/ingress/response/error-bodies.ts +5 -5
- package/src/ingress/response/terminal.ts +1 -1
- package/src/ingress/routes/echo.ts +4 -4
- package/src/ingress/routes/fallback.ts +2 -2
- package/src/ingress/routes/head.ts +2 -2
- package/src/ingress/routes/native.ts +2 -1
- package/src/ingress/routes/options.ts +1 -1
- package/src/ingress/routes/read.ts +2 -2
- package/src/ingress/server.ts +10 -6
- package/src/ingress/status.ts +7 -7
- package/src/integration/batch.ts +1 -1
- package/src/integration/index.ts +9 -9
- package/src/integration/pipeline.ts +6 -6
- package/src/integration/websocket.ts +2 -2
- package/src/loader/index.ts +1 -5
- package/src/loader/ops.ts +1 -1
- package/src/native/ffi/build/codecs.ts +104 -8
- package/src/native/ffi/build/compress.ts +14 -10
- package/src/native/ffi/build/instances.ts +40 -3
- package/src/native/ffi/build/metrics.ts +224 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/util.ts +14 -1
- package/src/native/ffi/build.ts +2 -0
- package/src/native/ffi/selftest.ts +10 -3
- package/src/native/ffi/types.ts +111 -9
- package/src/native/ffi.ts +87 -1
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +2 -2
- package/src/native/types/instances.ts +34 -0
- package/src/native/types.ts +11 -0
- package/src/runtime/codec.ts +53 -9
- package/src/runtime/types.ts +7 -0
- package/src/rust-ffi/batch/build.ts +16 -9
- package/src/rust-ffi/index.ts +7 -6
- package/src/rust-ffi/options.ts +1 -3
- package/src/rust-ffi/proven.ts +3 -3
- package/src/rust-ffi/scalar/crypto.ts +1 -1
- package/src/rust-ffi/scalar/factories.ts +73 -0
- package/src/rust-ffi/scalar/hashing.ts +1 -1
- package/src/rust-ffi/scalar/http.ts +17 -11
- package/src/rust-ffi/scalar/index.ts +2 -0
- package/src/rust-ffi/scalar/interface.ts +20 -0
- package/src/rust-ffi/scalar/payload.ts +11 -3
- package/src/rust-ffi/scalar/validation.ts +121 -0
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +5 -4
- package/src/shared/codec.ts +18 -3
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// rust/crypto/hashing.rs — FNV-1a / XXH3 checksums.
|
|
2
|
+
|
|
3
|
+
use napi::bindgen_prelude::*;
|
|
4
|
+
use napi_derive::napi;
|
|
5
|
+
|
|
6
|
+
use xxhash_rust::xxh3::xxh3_64;
|
|
7
|
+
|
|
8
|
+
pub const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
|
|
9
|
+
pub const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
|
|
10
|
+
|
|
11
|
+
// ── FNV-1a (byte-by-byte, correct per spec) ──────────────────────
|
|
12
|
+
|
|
13
|
+
/// FNV-1a in the "continue" style. FNV-1a is inherently byte-serial,
|
|
14
|
+
/// so we process each byte individually (this is the correct algorithm).
|
|
15
|
+
#[inline]
|
|
16
|
+
pub fn fnv1a64_continue(mut hash: u64, input: &[u8]) -> u64 {
|
|
17
|
+
for &b in input {
|
|
18
|
+
hash ^= u64::from(b);
|
|
19
|
+
hash = hash.wrapping_mul(FNV_PRIME);
|
|
20
|
+
}
|
|
21
|
+
hash
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// FNV-1a on a full byte slice, with fast path for small inputs.
|
|
25
|
+
#[inline]
|
|
26
|
+
pub fn fnv1a64_bytes(input: &[u8]) -> u64 {
|
|
27
|
+
fnv1a64_continue(FNV_OFFSET_BASIS, input)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ── XXHash wrappers ────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
#[inline]
|
|
33
|
+
pub fn fast_hash_bytes(input: &[u8]) -> u64 {
|
|
34
|
+
xxh3_64(input)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Napi exports ───────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/// CRC32 over a raw byte slice (pure core, shared by the napi boundary and
|
|
40
|
+
/// the pure-core unit tests).
|
|
41
|
+
#[inline]
|
|
42
|
+
pub fn crc32_bytes(input: &[u8]) -> u32 {
|
|
43
|
+
crc32fast::hash(input)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#[napi]
|
|
47
|
+
pub fn crc32(input: Uint8Array) -> u32 {
|
|
48
|
+
crc32_bytes(input.as_ref())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[napi(js_name = "fnv1a64")]
|
|
52
|
+
pub fn fnv1a64(input: Uint8Array) -> u64 {
|
|
53
|
+
fnv1a64_bytes(input.as_ref())
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// XXH3-64 over raw bytes. High-throughput non-cryptographic hash; the same
|
|
57
|
+
/// core the ingress IP-trust hasher uses. Exposed publicly so callers can
|
|
58
|
+
/// race it against `Bun.hash.xxHash3` — see docs/bun-builtins-decision-matrix.md.
|
|
59
|
+
#[napi]
|
|
60
|
+
pub fn xxh3(input: Uint8Array) -> u64 {
|
|
61
|
+
fast_hash_bytes(input.as_ref())
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// CRC32 batch — delegates to the shared packed-u32 writer (same wire format
|
|
65
|
+
/// as the `_into` variant; validation + parallelism live in
|
|
66
|
+
/// `util::packed::write_u32_batch_into`, single source of truth).
|
|
67
|
+
#[napi]
|
|
68
|
+
pub fn crc32_batch_packed(input: Uint8Array) -> Result<Buffer> {
|
|
69
|
+
let data = input.as_ref();
|
|
70
|
+
let count = crate::util::packed::PackedIter::new(data)?.len();
|
|
71
|
+
let mut out = vec![0u8; 4 + count.saturating_mul(4)];
|
|
72
|
+
let written = crate::util::write_u32_batch_into(data, &mut out, crc32_bytes)?;
|
|
73
|
+
out.truncate(written);
|
|
74
|
+
Ok(Buffer::from(out))
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Reusable-output CRC32 batch: writes `[u32 count][u32…]` into `output` and
|
|
78
|
+
/// returns bytes written. Wire format is byte-identical to
|
|
79
|
+
/// [`crc32_batch_packed`]; the JS loader uses this with a pooled buffer.
|
|
80
|
+
#[napi]
|
|
81
|
+
pub fn crc32_batch_packed_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
|
|
82
|
+
crate::util::run_packed_into(&input, &mut output, |data, out| {
|
|
83
|
+
crate::util::write_u32_batch_into(data, out, crc32_bytes)
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#[cfg(test)]
|
|
88
|
+
mod tests {
|
|
89
|
+
use super::*;
|
|
90
|
+
|
|
91
|
+
#[test]
|
|
92
|
+
fn fnv1a_known_vectors() {
|
|
93
|
+
// Standard FNV-1a 64-bit test vectors.
|
|
94
|
+
assert_eq!(fnv1a64_bytes(b""), 0xcbf2_9ce4_8422_2325);
|
|
95
|
+
assert_eq!(fnv1a64_bytes(b"a"), 0xaf63_dc4c_8601_ec8c);
|
|
96
|
+
assert_eq!(fnv1a64_bytes(b"foobar"), 0x8594_4171_f739_67e8);
|
|
97
|
+
// Continue-style matches one-shot.
|
|
98
|
+
let h = fnv1a64_continue(fnv1a64_bytes(b"foo"), b"bar");
|
|
99
|
+
assert_eq!(h, fnv1a64_bytes(b"foobar"));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#[test]
|
|
103
|
+
fn crc32_known_vector() {
|
|
104
|
+
assert_eq!(crc32fast::hash(b""), 0);
|
|
105
|
+
// Standard CRC-32 check value.
|
|
106
|
+
assert_eq!(crc32fast::hash(b"123456789"), 0xcbf4_3926);
|
|
107
|
+
// The napi wrapper agrees.
|
|
108
|
+
assert_eq!(crc32(Uint8Array::new(b"123456789".to_vec())), 0xcbf4_3926);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[test]
|
|
112
|
+
fn fnv1a_napi_agrees_with_core() {
|
|
113
|
+
assert_eq!(
|
|
114
|
+
fnv1a64(Uint8Array::new(b"foobar".to_vec())),
|
|
115
|
+
fnv1a64_bytes(b"foobar")
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
#[test]
|
|
120
|
+
fn xxh3_stable_and_distinct() {
|
|
121
|
+
let empty = fast_hash_bytes(b"");
|
|
122
|
+
assert_eq!(empty, fast_hash_bytes(b""));
|
|
123
|
+
assert_ne!(empty, fast_hash_bytes(b"a"));
|
|
124
|
+
assert_eq!(fast_hash_bytes(b"castrum"), fast_hash_bytes(b"castrum"));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
#[test]
|
|
128
|
+
fn xxh3_napi_agrees_with_core() {
|
|
129
|
+
let input = b"castrum xxh3 known-vector check 0123456789";
|
|
130
|
+
assert_eq!(
|
|
131
|
+
xxh3(Uint8Array::new(input.to_vec())),
|
|
132
|
+
fast_hash_bytes(input)
|
|
133
|
+
);
|
|
134
|
+
// Deterministic across calls on the same bytes.
|
|
135
|
+
assert_eq!(
|
|
136
|
+
xxh3(Uint8Array::new(input.to_vec())),
|
|
137
|
+
xxh3(Uint8Array::new(input.to_vec()))
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// rust/crypto/hmac_sha256.rs — HMAC-SHA256 sign/verify (scalar + HmacSigner instance).
|
|
2
|
+
|
|
3
|
+
use crate::util::bytes::{hex_decode_32, hex_encode_32, trim_ascii_whitespace};
|
|
4
|
+
use aws_lc_rs::hmac;
|
|
5
|
+
use napi::bindgen_prelude::*;
|
|
6
|
+
use napi_derive::napi;
|
|
7
|
+
|
|
8
|
+
#[napi]
|
|
9
|
+
pub fn hmac_sha256(key: Uint8Array, data: Buffer) -> Buffer {
|
|
10
|
+
let key = hmac::Key::new(hmac::HMAC_SHA256, key.as_ref());
|
|
11
|
+
let tag = hmac::sign(&key, data.as_ref());
|
|
12
|
+
|
|
13
|
+
let mut out = [0u8; 64];
|
|
14
|
+
hex_encode_32(tag.as_ref(), &mut out);
|
|
15
|
+
|
|
16
|
+
Buffer::from(out.to_vec())
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[napi]
|
|
20
|
+
pub fn hmac_sha256_verify(key: Uint8Array, data: Buffer, sig: Buffer) -> bool {
|
|
21
|
+
let sig = trim_ascii_whitespace(sig.as_ref());
|
|
22
|
+
|
|
23
|
+
let Some(sig_bytes) = hex_decode_32(sig) else {
|
|
24
|
+
return false;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
let key = hmac::Key::new(hmac::HMAC_SHA256, key.as_ref());
|
|
28
|
+
hmac::verify(&key, data.as_ref(), &sig_bytes).is_ok()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#[napi]
|
|
32
|
+
pub struct HmacSigner {
|
|
33
|
+
key: hmac::Key,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#[napi]
|
|
37
|
+
impl HmacSigner {
|
|
38
|
+
#[napi(constructor)]
|
|
39
|
+
pub fn new(key: Uint8Array) -> Self {
|
|
40
|
+
Self {
|
|
41
|
+
key: hmac::Key::new(hmac::HMAC_SHA256, key.as_ref()),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
#[napi]
|
|
46
|
+
pub fn sign(&self, data: Buffer) -> Buffer {
|
|
47
|
+
let tag = hmac::sign(&self.key, data.as_ref());
|
|
48
|
+
|
|
49
|
+
let mut out = [0u8; 64];
|
|
50
|
+
hex_encode_32(tag.as_ref(), &mut out);
|
|
51
|
+
|
|
52
|
+
Buffer::from(out.to_vec())
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
#[napi]
|
|
56
|
+
pub fn verify(&self, data: Buffer, sig: Buffer) -> bool {
|
|
57
|
+
let sig = trim_ascii_whitespace(sig.as_ref());
|
|
58
|
+
|
|
59
|
+
let Some(sig_bytes) = hex_decode_32(sig) else {
|
|
60
|
+
return false;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
hmac::verify(&self.key, data.as_ref(), &sig_bytes).is_ok()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#[cfg(test)]
|
|
68
|
+
mod tests {
|
|
69
|
+
#[test]
|
|
70
|
+
fn hmac_sign_then_verify_roundtrip() {
|
|
71
|
+
let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, b"secret-key");
|
|
72
|
+
let data = b"hello world";
|
|
73
|
+
let tag = aws_lc_rs::hmac::sign(&key, data);
|
|
74
|
+
|
|
75
|
+
let mut hex = [0u8; 64];
|
|
76
|
+
crate::util::bytes::hex_encode_32(tag.as_ref(), &mut hex);
|
|
77
|
+
let decoded = crate::util::bytes::hex_decode_32(&hex).expect("valid hex decodes");
|
|
78
|
+
|
|
79
|
+
assert!(aws_lc_rs::hmac::verify(&key, data, &decoded).is_ok());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[test]
|
|
83
|
+
fn hex_decode_32_rejects_bad_length_and_digits() {
|
|
84
|
+
assert!(crate::util::bytes::hex_decode_32(b"abc").is_none());
|
|
85
|
+
assert!(crate::util::bytes::hex_decode_32(&[b'0'; 64]).is_some());
|
|
86
|
+
assert!(crate::util::bytes::hex_decode_32(&[b'g'; 64]).is_none());
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// rust/crypto/jwt/api.rs — napi entry points for JWT signing/verification.
|
|
2
|
+
//
|
|
3
|
+
// Thin napi boundary over the pure token core in `token.rs`: scalar sign/
|
|
4
|
+
// verify, the precompiled-key `JwtSigner` instance, the C-ABI `_core` helpers
|
|
5
|
+
// consumed by `rust/ffi.rs`, and the packed sign/verify batches.
|
|
6
|
+
|
|
7
|
+
use napi::bindgen_prelude::*;
|
|
8
|
+
use napi_derive::napi;
|
|
9
|
+
|
|
10
|
+
use crate::crypto::base64::base64url_encode_bytes;
|
|
11
|
+
|
|
12
|
+
use super::{
|
|
13
|
+
build_token, build_token_with_key, inject_and_payload_b64, inject_and_payload_b64_sonic,
|
|
14
|
+
inject_ttl_claims, jwt_header_b64, sign_eddsa, verify_token, verify_token_eddsa,
|
|
15
|
+
verify_token_with_key,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/// Sign a JWT (HS256). `claims` is any JSON value; when it is an object and a
|
|
19
|
+
/// positive `ttlSeconds` is given, `iat`/`exp` are injected (unless present).
|
|
20
|
+
/// `nowSeconds` is the caller-supplied current epoch seconds (keeps the core
|
|
21
|
+
/// time-free and testable). Returns the compact token bytes.
|
|
22
|
+
#[napi]
|
|
23
|
+
pub fn jwt_sign(
|
|
24
|
+
claims: serde_json::Value,
|
|
25
|
+
secret: Uint8Array,
|
|
26
|
+
ttl_seconds: Option<i64>,
|
|
27
|
+
now_seconds: i64,
|
|
28
|
+
) -> Result<Buffer> {
|
|
29
|
+
let mut claims = claims;
|
|
30
|
+
let payload_b64 = inject_and_payload_b64(&mut claims, ttl_seconds, now_seconds)?;
|
|
31
|
+
Ok(Buffer::from(build_token(
|
|
32
|
+
jwt_header_b64(),
|
|
33
|
+
&payload_b64,
|
|
34
|
+
secret.as_ref(),
|
|
35
|
+
)))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Sign a JWT (HS256) from pre-serialized claim JSON bytes. Avoids the napi
|
|
39
|
+
/// `serde_json::Value` DOM marshal of `jwt_sign` for callers that already hold
|
|
40
|
+
/// the claim bytes (e.g. JSON.stringify'd on the JS side). The claims are
|
|
41
|
+
/// parsed straight into a `sonic_rs::Value` (compact, no per-key heap
|
|
42
|
+
/// `String`). Semantics are identical to `jwt_sign` (incl. `iat`/`exp`
|
|
43
|
+
/// injection).
|
|
44
|
+
#[napi]
|
|
45
|
+
pub fn jwt_sign_bytes(
|
|
46
|
+
claims_json: Uint8Array,
|
|
47
|
+
secret: Uint8Array,
|
|
48
|
+
ttl_seconds: Option<i64>,
|
|
49
|
+
now_seconds: i64,
|
|
50
|
+
) -> Result<Buffer> {
|
|
51
|
+
let mut claims: sonic_rs::Value =
|
|
52
|
+
sonic_rs::from_slice(claims_json.as_ref()).map_err(napi_err)?;
|
|
53
|
+
let payload_b64 = inject_and_payload_b64_sonic(&mut claims, ttl_seconds, now_seconds)?;
|
|
54
|
+
Ok(Buffer::from(build_token(
|
|
55
|
+
jwt_header_b64(),
|
|
56
|
+
&payload_b64,
|
|
57
|
+
secret.as_ref(),
|
|
58
|
+
)))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Verify a JWT (HS256 signature + time claims). Returns the decoded claims
|
|
62
|
+
/// object (marshaled from the compact sonic Value), or `null` on any failure
|
|
63
|
+
/// (malformed, bad signature, wrong `alg`, expired, not-yet-valid `nbf`/`iat`,
|
|
64
|
+
/// non-JSON payload).
|
|
65
|
+
#[napi]
|
|
66
|
+
pub fn jwt_verify(
|
|
67
|
+
env: Env,
|
|
68
|
+
token: Uint8Array,
|
|
69
|
+
secret: Uint8Array,
|
|
70
|
+
now_seconds: i64,
|
|
71
|
+
) -> Result<Unknown<'static>> {
|
|
72
|
+
let Some(payload) = verify_token(token.as_ref(), secret.as_ref(), now_seconds) else {
|
|
73
|
+
return crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new());
|
|
74
|
+
};
|
|
75
|
+
match sonic_rs::from_slice::<sonic_rs::Value>(&payload) {
|
|
76
|
+
Ok(v) => crate::json::napi_marshal::sonic_value_to_js(&env, &v),
|
|
77
|
+
// The payload already parsed for the time-claim check — unreachable.
|
|
78
|
+
Err(_) => crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new()),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// Sign an EdDSA (Ed25519) JWT from pre-serialized claim JSON bytes. Semantics
|
|
83
|
+
/// identical to `jwt_sign_bytes` (incl. `iat`/`exp` injection); the signature
|
|
84
|
+
/// is Ed25519 (RFC 8032) under the `EdDSA` alg.
|
|
85
|
+
#[napi]
|
|
86
|
+
pub fn jwt_sign_eddsa(
|
|
87
|
+
claims_json: Uint8Array,
|
|
88
|
+
private_key: Uint8Array,
|
|
89
|
+
ttl_seconds: Option<i64>,
|
|
90
|
+
now_seconds: i64,
|
|
91
|
+
) -> Result<Buffer> {
|
|
92
|
+
match sign_eddsa(
|
|
93
|
+
claims_json.as_ref(),
|
|
94
|
+
private_key.as_ref(),
|
|
95
|
+
ttl_seconds,
|
|
96
|
+
now_seconds,
|
|
97
|
+
) {
|
|
98
|
+
Some(token) => Ok(Buffer::from(token)),
|
|
99
|
+
None => Err(Error::from_reason(
|
|
100
|
+
"EdDSA sign failed (invalid claims or private key)",
|
|
101
|
+
)),
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Verify an EdDSA (Ed25519) JWT (signature + time claims). Returns the decoded
|
|
106
|
+
/// claims object, or `null` on any failure.
|
|
107
|
+
#[napi]
|
|
108
|
+
pub fn jwt_verify_eddsa(
|
|
109
|
+
env: Env,
|
|
110
|
+
token: Uint8Array,
|
|
111
|
+
public_key: Uint8Array,
|
|
112
|
+
now_seconds: i64,
|
|
113
|
+
) -> Result<Unknown<'static>> {
|
|
114
|
+
let Some(payload) = verify_token_eddsa(token.as_ref(), public_key.as_ref(), now_seconds) else {
|
|
115
|
+
return crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new());
|
|
116
|
+
};
|
|
117
|
+
match sonic_rs::from_slice::<sonic_rs::Value>(&payload) {
|
|
118
|
+
Ok(v) => crate::json::napi_marshal::sonic_value_to_js(&env, &v),
|
|
119
|
+
// The payload already parsed for the time-claim check — unreachable.
|
|
120
|
+
Err(_) => crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new()),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// Higher-order instance: precompiles the HMAC-SHA256 key once from `secret`,
|
|
125
|
+
/// so `sign`/`verify` never re-derive the key on the per-call path. `ttl` is
|
|
126
|
+
/// fixed at construction (0/absent → no `iat`/`exp` injection).
|
|
127
|
+
#[napi]
|
|
128
|
+
pub struct JwtSigner {
|
|
129
|
+
pub(crate) key: aws_lc_rs::hmac::Key,
|
|
130
|
+
ttl_seconds: Option<i64>,
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[napi]
|
|
134
|
+
impl JwtSigner {
|
|
135
|
+
#[napi(constructor)]
|
|
136
|
+
pub fn new(secret: Uint8Array, ttl_seconds: Option<i64>) -> Self {
|
|
137
|
+
Self {
|
|
138
|
+
key: aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, secret.as_ref()),
|
|
139
|
+
ttl_seconds,
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Opaque handle to the precompiled key + ttl, for the `bun:ffi` C-ABI
|
|
144
|
+
/// fast path (`castrum_jwt_signer_*` in rust/ffi.rs). Only valid while THIS
|
|
145
|
+
/// instance is alive; the JS wrapper holds the instance.
|
|
146
|
+
#[napi]
|
|
147
|
+
pub fn inner_ptr(&self) -> u64 {
|
|
148
|
+
self as *const JwtSigner as u64
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/// Sign a JWT with the precompiled key. When `ttl` was set at construction,
|
|
152
|
+
/// `iat`/`exp` are injected into object claims (unless already present).
|
|
153
|
+
#[napi]
|
|
154
|
+
pub fn sign(&self, claims: serde_json::Value, now_seconds: i64) -> Result<Buffer> {
|
|
155
|
+
let mut claims = claims;
|
|
156
|
+
let payload_b64 = inject_and_payload_b64(&mut claims, self.ttl_seconds, now_seconds)?;
|
|
157
|
+
Ok(Buffer::from(build_token_with_key(
|
|
158
|
+
jwt_header_b64(),
|
|
159
|
+
&payload_b64,
|
|
160
|
+
&self.key,
|
|
161
|
+
)))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Sign a JWT from pre-serialized claim JSON bytes with the precompiled
|
|
165
|
+
/// key — no napi `serde_json::Value` marshal, no per-call key derivation.
|
|
166
|
+
#[napi]
|
|
167
|
+
pub fn sign_bytes(&self, claims_json: Uint8Array, now_seconds: i64) -> Result<Buffer> {
|
|
168
|
+
let mut claims: sonic_rs::Value =
|
|
169
|
+
sonic_rs::from_slice(claims_json.as_ref()).map_err(napi_err)?;
|
|
170
|
+
let payload_b64 = inject_and_payload_b64_sonic(&mut claims, self.ttl_seconds, now_seconds)?;
|
|
171
|
+
Ok(Buffer::from(build_token_with_key(
|
|
172
|
+
jwt_header_b64(),
|
|
173
|
+
&payload_b64,
|
|
174
|
+
&self.key,
|
|
175
|
+
)))
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// Verify a JWT with the precompiled key (signature + `alg` + time claims).
|
|
179
|
+
/// Returns the decoded claims object, or `null` on any failure.
|
|
180
|
+
#[napi]
|
|
181
|
+
pub fn verify(
|
|
182
|
+
&self,
|
|
183
|
+
env: Env,
|
|
184
|
+
token: Uint8Array,
|
|
185
|
+
now_seconds: i64,
|
|
186
|
+
) -> Result<Unknown<'static>> {
|
|
187
|
+
let Some(payload) = verify_token_with_key(token.as_ref(), &self.key, now_seconds) else {
|
|
188
|
+
return crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new());
|
|
189
|
+
};
|
|
190
|
+
match sonic_rs::from_slice::<sonic_rs::Value>(&payload) {
|
|
191
|
+
Ok(v) => crate::json::napi_marshal::sonic_value_to_js(&env, &v),
|
|
192
|
+
// The payload already parsed for the time-claim check — unreachable.
|
|
193
|
+
Err(_) => crate::json::napi_marshal::sonic_value_to_js(&env, &sonic_rs::Value::new()),
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// C-ABI support: sign pre-serialized claim JSON with the precompiled key.
|
|
199
|
+
/// `Err(())` on invalid claims JSON (napi parity: throws).
|
|
200
|
+
///
|
|
201
|
+
/// # Safety
|
|
202
|
+
/// `p` must be a valid `*const JwtSigner` from `inner_ptr`, alive for the call.
|
|
203
|
+
pub(crate) unsafe fn jwt_signer_sign_bytes_core(
|
|
204
|
+
p: *const JwtSigner,
|
|
205
|
+
claims_json: &[u8],
|
|
206
|
+
now_seconds: i64,
|
|
207
|
+
) -> std::result::Result<Vec<u8>, ()> {
|
|
208
|
+
let this = &*p;
|
|
209
|
+
let mut claims: sonic_rs::Value = sonic_rs::from_slice(claims_json).map_err(|_| ())?;
|
|
210
|
+
let payload_b64 =
|
|
211
|
+
inject_and_payload_b64_sonic(&mut claims, this.ttl_seconds, now_seconds).map_err(|_| ())?;
|
|
212
|
+
Ok(build_token_with_key(
|
|
213
|
+
jwt_header_b64(),
|
|
214
|
+
&payload_b64,
|
|
215
|
+
&this.key,
|
|
216
|
+
))
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// C-ABI support: verify a JWT with the precompiled key → the decoded payload
|
|
220
|
+
/// BYTES (valid JSON claims), or None on invalid signature / expired /
|
|
221
|
+
/// malformed. No `serde_json::Value` DOM, no re-serialize.
|
|
222
|
+
///
|
|
223
|
+
/// # Safety
|
|
224
|
+
/// `p` must be a valid `*const JwtSigner` from `inner_ptr`, alive for the call.
|
|
225
|
+
pub(crate) unsafe fn jwt_signer_verify_core(
|
|
226
|
+
p: *const JwtSigner,
|
|
227
|
+
token: &[u8],
|
|
228
|
+
now_seconds: i64,
|
|
229
|
+
) -> Option<Vec<u8>> {
|
|
230
|
+
let this = &*p;
|
|
231
|
+
verify_token_with_key(token, &this.key, now_seconds)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fn napi_err(e: impl std::fmt::Display) -> Error {
|
|
235
|
+
Error::from_reason(e.to_string())
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// Parallel sign batch: packed `[u32 count]{[u32 len][claims-json]}` in →
|
|
239
|
+
/// packed `[u32 count]{[u32 len][token]}` out (same secret/ttl/now for all).
|
|
240
|
+
#[napi]
|
|
241
|
+
pub fn jwt_sign_batch_packed(
|
|
242
|
+
data: Uint8Array,
|
|
243
|
+
secret: Uint8Array,
|
|
244
|
+
ttl_seconds: Option<i64>,
|
|
245
|
+
now_seconds: i64,
|
|
246
|
+
) -> Result<Buffer> {
|
|
247
|
+
// Derive the HMAC key ONCE for the whole batch (build_token derives a
|
|
248
|
+
// fresh key per call), then route through the shared packed batch helper
|
|
249
|
+
// (zero-alloc serial PackedIter path; rayon direct-write when justified).
|
|
250
|
+
let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, secret.as_ref());
|
|
251
|
+
|
|
252
|
+
let sign_one = |claims_json: &[u8]| -> Vec<u8> {
|
|
253
|
+
let Ok(mut claims) = sonic_rs::from_slice::<sonic_rs::Value>(claims_json) else {
|
|
254
|
+
return Vec::new();
|
|
255
|
+
};
|
|
256
|
+
inject_ttl_claims(&mut claims, ttl_seconds, now_seconds);
|
|
257
|
+
let header_b64 = jwt_header_b64();
|
|
258
|
+
let Ok(payload_bytes) = sonic_rs::to_vec(&claims) else {
|
|
259
|
+
return Vec::new();
|
|
260
|
+
};
|
|
261
|
+
let payload_b64 = base64url_encode_bytes(&payload_bytes);
|
|
262
|
+
build_token_with_key(header_b64, &payload_b64, &key)
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
crate::util::run_packed_batch(data.as_ref(), sign_one).map(Buffer::from)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/// Parallel verify batch: packed `[u32 count]{[u32 len][token]}` in → bitset
|
|
269
|
+
/// (`[u32 count][ceil(n/8) bytes]`) of valid tokens.
|
|
270
|
+
#[napi]
|
|
271
|
+
pub fn jwt_verify_batch_packed(
|
|
272
|
+
data: Uint8Array,
|
|
273
|
+
secret: Uint8Array,
|
|
274
|
+
now_seconds: i64,
|
|
275
|
+
) -> Result<Buffer> {
|
|
276
|
+
// Derive the HMAC key ONCE for the whole batch (verify_token derives a
|
|
277
|
+
// fresh key per call), then route through the shared bitset batch helper
|
|
278
|
+
// (zero-alloc serial PackedIter path; rayon direct-write when justified).
|
|
279
|
+
let key = aws_lc_rs::hmac::Key::new(aws_lc_rs::hmac::HMAC_SHA256, secret.as_ref());
|
|
280
|
+
crate::util::run_bitset_batch(
|
|
281
|
+
data.as_ref(),
|
|
282
|
+
|token| verify_token_with_key(token, &key, now_seconds).is_some(),
|
|
283
|
+
4096,
|
|
284
|
+
)
|
|
285
|
+
.map(Buffer::from)
|
|
286
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
//! HS256 / EdDSA JSON Web Tokens.
|
|
2
|
+
//!
|
|
3
|
+
//! Split into a small folder so the token machinery is navigable:
|
|
4
|
+
//! - `token.rs` — pure token core (headers, HMAC, split/build/verify, zero-DOM
|
|
5
|
+
//! time-claim checks, `iat`/`exp` injection). Reused by the napi boundary,
|
|
6
|
+
//! the C-ABI fast paths in `rust/ffi.rs`, and `crate::crypto::ed25519`.
|
|
7
|
+
//! - `api.rs` — napi entry points (scalar sign/verify, precompiled-key
|
|
8
|
+
//! `JwtSigner` instance, C-ABI `_core` helpers, packed sign/verify batches).
|
|
9
|
+
//! - `tests.rs` — unit tests.
|
|
10
|
+
//!
|
|
11
|
+
//! All items are re-exported at this module root (`pub use self::token::*` +
|
|
12
|
+
//! `pub use self::api::*`) so existing `crate::crypto::jwt::*` call sites keep
|
|
13
|
+
//! working unchanged.
|
|
14
|
+
|
|
15
|
+
mod api;
|
|
16
|
+
mod token;
|
|
17
|
+
|
|
18
|
+
#[cfg(test)]
|
|
19
|
+
mod tests;
|
|
20
|
+
|
|
21
|
+
pub use self::api::*;
|
|
22
|
+
pub use self::token::*;
|