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.
- package/Cargo.lock +2242 -0
- package/Cargo.toml +155 -0
- package/README.md +14 -6
- package/build.rs +5 -0
- package/castrum.darwin-arm64.node +0 -0
- package/castrum.darwin-x64.node +0 -0
- package/castrum.linux-arm64-gnu.node +0 -0
- package/castrum.linux-arm64-musl.node +0 -0
- package/castrum.linux-x64-gnu.node +0 -0
- package/castrum.linux-x64-musl.node +0 -0
- package/castrum.linux-x64-v3-gnu.node +0 -0
- package/castrum.win32-x64-msvc.node +0 -0
- package/dist/index.d.ts +1508 -1291
- package/dist/index.js +3391 -2069
- package/docs/API.md +2 -1
- package/docs/ARCHITECTURE.md +3 -2
- package/docs/CASE_STUDY.md +5 -3
- package/docs/ENVIRONMENT.md +25 -2
- package/docs/FFI_BUN_GUIDE.md +242 -14
- package/docs/HOW_TO_ADD_AN_OP.md +10 -0
- package/docs/REPO_MAP.md +17 -6
- package/docs/RND-CONCURRENCY.md +772 -0
- package/docs/bun-builtins-decision-matrix.md +4 -1
- package/index.ts +24 -25
- package/package.json +173 -156
- 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 +105 -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 +69 -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/task.rs +172 -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 +136 -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 +211 -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 +770 -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 +567 -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 +883 -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 +73 -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/task/completion.rs +119 -0
- package/rust/task/mod.rs +65 -0
- package/rust/task/ops.rs +503 -0
- package/rust/task/runtime.rs +220 -0
- package/rust/task/tests.rs +231 -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 +594 -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/health.ts +1 -2
- 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 +130 -12
- package/src/native/ffi/build/compress.ts +22 -10
- package/src/native/ffi/build/instances.ts +46 -4
- package/src/native/ffi/build/metrics.ts +247 -0
- package/src/native/ffi/build/parse.ts +59 -23
- package/src/native/ffi/build/task.ts +79 -0
- package/src/native/ffi/build/util.ts +32 -1
- package/src/native/ffi/build.ts +4 -0
- package/src/native/ffi/selftest.ts +12 -3
- package/src/native/ffi/types.ts +151 -9
- package/src/native/ffi.ts +160 -15
- package/src/native/index.ts +1 -1
- package/src/native/loader.ts +55 -5
- 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 +71 -27
- 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/rust-ffi/text.ts +25 -3
- package/src/selection.ts +1 -1
- package/src/shared/bytes.ts +24 -4
- package/src/shared/codec.ts +18 -3
- package/src/task/index.ts +24 -0
- package/src/task/op.ts +171 -0
- package/src/task/runtime.ts +568 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
// rust/crypto/base64.rs — base64 (standard/url-safe) + hex encode/decode.
|
|
2
|
+
//
|
|
3
|
+
// Uses the `base64` crate engines (Cargo dep already present). The
|
|
4
|
+
// `Base64Codec` higher-order instance precompiles the alphabet/decoding
|
|
5
|
+
// configuration once in its constructor and reuses it across calls.
|
|
6
|
+
|
|
7
|
+
use napi::bindgen_prelude::*;
|
|
8
|
+
use napi_derive::napi;
|
|
9
|
+
|
|
10
|
+
use crate::util::bytes::{hex_val, HEX_LOWER};
|
|
11
|
+
use base64::Engine as _;
|
|
12
|
+
|
|
13
|
+
fn engine(url_safe: bool, padding: bool) -> base64::engine::general_purpose::GeneralPurpose {
|
|
14
|
+
use base64::engine::general_purpose::*;
|
|
15
|
+
match (url_safe, padding) {
|
|
16
|
+
(false, true) => STANDARD,
|
|
17
|
+
(false, false) => STANDARD_NO_PAD,
|
|
18
|
+
(true, true) => URL_SAFE,
|
|
19
|
+
(true, false) => URL_SAFE_NO_PAD,
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#[napi]
|
|
24
|
+
pub fn base64_encode(input: Uint8Array, url_safe: Option<bool>, padding: Option<bool>) -> Buffer {
|
|
25
|
+
let out = engine(url_safe.unwrap_or(false), padding.unwrap_or(true)).encode(input.as_ref());
|
|
26
|
+
Buffer::from(out.into_bytes())
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#[napi]
|
|
30
|
+
pub fn base64_decode(
|
|
31
|
+
input: Uint8Array,
|
|
32
|
+
url_safe: Option<bool>,
|
|
33
|
+
padding: Option<bool>,
|
|
34
|
+
) -> Result<Buffer> {
|
|
35
|
+
engine(url_safe.unwrap_or(false), padding.unwrap_or(true))
|
|
36
|
+
.decode(input.as_ref())
|
|
37
|
+
.map(Buffer::from)
|
|
38
|
+
.map_err(|e| Error::from_reason(e.to_string()))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
#[napi]
|
|
42
|
+
pub fn base64url_encode(input: Uint8Array) -> Buffer {
|
|
43
|
+
Buffer::from(
|
|
44
|
+
base64::engine::general_purpose::URL_SAFE_NO_PAD
|
|
45
|
+
.encode(input.as_ref())
|
|
46
|
+
.into_bytes(),
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#[napi]
|
|
51
|
+
pub fn base64url_decode(input: Uint8Array) -> Result<Buffer> {
|
|
52
|
+
base64::engine::general_purpose::URL_SAFE_NO_PAD
|
|
53
|
+
.decode(input.as_ref())
|
|
54
|
+
.map(Buffer::from)
|
|
55
|
+
.map_err(|e| Error::from_reason(e.to_string()))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Base64-encode to raw bytes (no Buffer wrapping). Shared by the scalar and
|
|
59
|
+
/// packed-batch paths.
|
|
60
|
+
pub fn base64_encode_bytes(input: &[u8], url_safe: bool, padding: bool) -> Vec<u8> {
|
|
61
|
+
engine(url_safe, padding).encode(input).into_bytes()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Base64-decode from raw bytes. Errors on invalid input. Shared by the scalar
|
|
65
|
+
/// and packed-batch paths.
|
|
66
|
+
pub fn base64_decode_bytes(input: &[u8], url_safe: bool, padding: bool) -> Result<Vec<u8>> {
|
|
67
|
+
engine(url_safe, padding)
|
|
68
|
+
.decode(input)
|
|
69
|
+
.map_err(|e| Error::from_reason(e.to_string()))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Base64url (RFC 7515 §2 — URL-safe alphabet, NO padding) encode to raw
|
|
73
|
+
/// bytes. The single shared source of truth for JWT segment encoding
|
|
74
|
+
/// (`crate::crypto::jwt`) and Ed25519 keypair serialization
|
|
75
|
+
/// (`crate::crypto::ed25519`) — keeps the url-safe alphabet defined in exactly
|
|
76
|
+
/// one place.
|
|
77
|
+
#[inline]
|
|
78
|
+
pub fn base64url_encode_bytes(data: &[u8]) -> Vec<u8> {
|
|
79
|
+
base64::engine::general_purpose::URL_SAFE_NO_PAD
|
|
80
|
+
.encode(data)
|
|
81
|
+
.into_bytes()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Base64url (RFC 7515 §2 — URL-safe alphabet, NO padding) decode from raw
|
|
85
|
+
/// bytes. Returns `None` on invalid UTF-8 or non-base64url input.
|
|
86
|
+
#[inline]
|
|
87
|
+
pub fn base64url_decode_bytes(data: &[u8]) -> Option<Vec<u8>> {
|
|
88
|
+
let text = std::str::from_utf8(data).ok()?;
|
|
89
|
+
base64::engine::general_purpose::URL_SAFE_NO_PAD
|
|
90
|
+
.decode(text)
|
|
91
|
+
.ok()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── hex ──────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
/// Encode bytes as lowercase hex (uses the shared `HEX_LOWER` table — direct
|
|
97
|
+
/// byte writes, no per-nibble `char` push).
|
|
98
|
+
///
|
|
99
|
+
/// # Safety
|
|
100
|
+
///
|
|
101
|
+
/// Hex output is always ASCII (`HEX_LOWER` is a byte string of `0-9a-f`), so
|
|
102
|
+
/// the built `Vec<u8>` is valid UTF-8 by construction; `from_utf8_unchecked`
|
|
103
|
+
/// skips a redundant validation pass on the allocating hot path.
|
|
104
|
+
pub fn hex_encode_bytes(input: &[u8]) -> String {
|
|
105
|
+
let mut out = Vec::with_capacity(input.len() * 2);
|
|
106
|
+
for &b in input {
|
|
107
|
+
out.push(HEX_LOWER[(b >> 4) as usize]);
|
|
108
|
+
out.push(HEX_LOWER[(b & 0x0f) as usize]);
|
|
109
|
+
}
|
|
110
|
+
// SAFETY: every byte pushed is an ASCII hex digit from `HEX_LOWER`.
|
|
111
|
+
unsafe { String::from_utf8_unchecked(out) }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Decode lowercase/uppercase hex to bytes.
|
|
115
|
+
pub fn hex_decode_bytes(input: &[u8]) -> std::result::Result<Vec<u8>, &'static str> {
|
|
116
|
+
if !input.len().is_multiple_of(2) {
|
|
117
|
+
return Err("odd hex length");
|
|
118
|
+
}
|
|
119
|
+
let mut out = Vec::with_capacity(input.len() / 2);
|
|
120
|
+
for i in (0..input.len()).step_by(2) {
|
|
121
|
+
let hi = hex_val(input[i]).ok_or("invalid hex digit")?;
|
|
122
|
+
let lo = hex_val(input[i + 1]).ok_or("invalid hex digit")?;
|
|
123
|
+
out.push((hi << 4) | lo);
|
|
124
|
+
}
|
|
125
|
+
Ok(out)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[napi]
|
|
129
|
+
pub fn hex_encode(input: Uint8Array) -> Buffer {
|
|
130
|
+
Buffer::from(hex_encode_bytes(input.as_ref()).into_bytes())
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#[napi]
|
|
134
|
+
pub fn hex_decode(input: Uint8Array) -> Result<Buffer> {
|
|
135
|
+
hex_decode_bytes(input.as_ref())
|
|
136
|
+
.map(Buffer::from)
|
|
137
|
+
.map_err(|e| Error::from_reason(e.to_string()))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Reusable-output (_into) variants ────────────────────────────
|
|
141
|
+
//
|
|
142
|
+
// These write into a caller-provided output buffer and return the number of
|
|
143
|
+
// bytes written (u32), so hot loops can pool the buffer instead of allocating
|
|
144
|
+
// a fresh Vec + napi Buffer per call. They error on a buffer that is too small;
|
|
145
|
+
// input/output overlap is handled by `crate::util::run_packed_into`.
|
|
146
|
+
|
|
147
|
+
/// Lowercase-hex encode directly into `out`. Returns bytes written
|
|
148
|
+
/// (`input.len() * 2`). Errors if `out` is too small.
|
|
149
|
+
#[inline]
|
|
150
|
+
pub fn hex_encode_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
|
|
151
|
+
let needed = input
|
|
152
|
+
.len()
|
|
153
|
+
.checked_mul(2)
|
|
154
|
+
.ok_or_else(|| Error::from_reason("hex encode: input too large (length overflow)"))?;
|
|
155
|
+
if out.len() < needed {
|
|
156
|
+
return Err(Error::from_reason("hex encode: output buffer too small"));
|
|
157
|
+
}
|
|
158
|
+
let mut pos = 0usize;
|
|
159
|
+
for &b in input {
|
|
160
|
+
out[pos] = HEX_LOWER[(b >> 4) as usize];
|
|
161
|
+
out[pos + 1] = HEX_LOWER[(b & 0x0f) as usize];
|
|
162
|
+
pos += 2;
|
|
163
|
+
}
|
|
164
|
+
Ok(pos)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[napi]
|
|
168
|
+
pub fn hex_encode_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
|
|
169
|
+
crate::util::run_packed_into(&input, &mut output, hex_encode_into_slice)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// Hex-decode directly into `out`. Returns bytes written (`input.len() / 2`).
|
|
173
|
+
/// Errors on odd length, invalid digits, or an output buffer that is too small.
|
|
174
|
+
#[inline]
|
|
175
|
+
pub fn hex_decode_into_slice(input: &[u8], out: &mut [u8]) -> Result<usize> {
|
|
176
|
+
if !input.len().is_multiple_of(2) {
|
|
177
|
+
return Err(Error::from_reason("odd hex length"));
|
|
178
|
+
}
|
|
179
|
+
let needed = input.len() / 2;
|
|
180
|
+
if out.len() < needed {
|
|
181
|
+
return Err(Error::from_reason("hex decode: output buffer too small"));
|
|
182
|
+
}
|
|
183
|
+
let mut pos = 0usize;
|
|
184
|
+
for i in (0..input.len()).step_by(2) {
|
|
185
|
+
let hi = hex_val(input[i]).ok_or_else(|| Error::from_reason("invalid hex digit"))?;
|
|
186
|
+
let lo = hex_val(input[i + 1]).ok_or_else(|| Error::from_reason("invalid hex digit"))?;
|
|
187
|
+
out[pos] = (hi << 4) | lo;
|
|
188
|
+
pos += 1;
|
|
189
|
+
}
|
|
190
|
+
Ok(pos)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#[napi]
|
|
194
|
+
pub fn hex_decode_into(input: Uint8Array, mut output: Uint8Array) -> Result<u32> {
|
|
195
|
+
crate::util::run_packed_into(&input, &mut output, hex_decode_into_slice)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// Pure core: base64-encode `input` into `out` (zero-alloc `encode_slice`).
|
|
199
|
+
/// Returns bytes written; errors if `out` is too small. Shared by the `_into`
|
|
200
|
+
/// napi path and the pure-core unit tests.
|
|
201
|
+
pub fn base64_encode_into_slice(
|
|
202
|
+
input: &[u8],
|
|
203
|
+
out: &mut [u8],
|
|
204
|
+
url_safe: bool,
|
|
205
|
+
padding: bool,
|
|
206
|
+
) -> Result<usize> {
|
|
207
|
+
engine(url_safe, padding)
|
|
208
|
+
.encode_slice(input, out)
|
|
209
|
+
.map_err(|e| Error::from_reason(format!("base64 encode: {e}")))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// Base64-encode into a caller-provided output buffer (zero-alloc via
|
|
213
|
+
/// `Engine::encode_slice`). Returns bytes written; errors if `output` is too
|
|
214
|
+
/// small.
|
|
215
|
+
#[napi]
|
|
216
|
+
pub fn base64_encode_into(
|
|
217
|
+
input: Uint8Array,
|
|
218
|
+
mut output: Uint8Array,
|
|
219
|
+
url_safe: Option<bool>,
|
|
220
|
+
padding: Option<bool>,
|
|
221
|
+
) -> Result<u32> {
|
|
222
|
+
crate::util::run_packed_into(&input, &mut output, move |inp, out| {
|
|
223
|
+
base64_encode_into_slice(inp, out, url_safe.unwrap_or(false), padding.unwrap_or(true))
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// Pure core: base64-decode `input` into `out` (zero-alloc `decode_slice`).
|
|
228
|
+
/// Returns bytes written; errors on invalid input or a too-small output
|
|
229
|
+
/// buffer. Shared by the `_into` napi path and the pure-core unit tests.
|
|
230
|
+
pub fn base64_decode_into_slice(
|
|
231
|
+
input: &[u8],
|
|
232
|
+
out: &mut [u8],
|
|
233
|
+
url_safe: bool,
|
|
234
|
+
padding: bool,
|
|
235
|
+
) -> Result<usize> {
|
|
236
|
+
engine(url_safe, padding)
|
|
237
|
+
.decode_slice(input, out)
|
|
238
|
+
.map_err(|e| Error::from_reason(format!("base64 decode: {e}")))
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/// Base64-decode into a caller-provided output buffer (zero-alloc via
|
|
242
|
+
/// `Engine::decode_slice`). Returns bytes written; errors on invalid input or
|
|
243
|
+
/// an output buffer that is too small.
|
|
244
|
+
#[napi]
|
|
245
|
+
pub fn base64_decode_into(
|
|
246
|
+
input: Uint8Array,
|
|
247
|
+
mut output: Uint8Array,
|
|
248
|
+
url_safe: Option<bool>,
|
|
249
|
+
padding: Option<bool>,
|
|
250
|
+
) -> Result<u32> {
|
|
251
|
+
crate::util::run_packed_into(&input, &mut output, move |inp, out| {
|
|
252
|
+
base64_decode_into_slice(inp, out, url_safe.unwrap_or(false), padding.unwrap_or(true))
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/// Higher-order instance: the alphabet/padding engine is selected ONCE at
|
|
257
|
+
/// construction and stored concretely, so `encode`/`decode` never re-run the
|
|
258
|
+
/// 4-way engine match on the per-call path.
|
|
259
|
+
#[napi]
|
|
260
|
+
pub struct Base64Codec {
|
|
261
|
+
engine: base64::engine::general_purpose::GeneralPurpose,
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#[napi]
|
|
265
|
+
impl Base64Codec {
|
|
266
|
+
#[napi(constructor)]
|
|
267
|
+
pub fn new(url_safe: Option<bool>, padding: Option<bool>) -> Self {
|
|
268
|
+
Self {
|
|
269
|
+
engine: engine(url_safe.unwrap_or(false), padding.unwrap_or(true)),
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#[napi]
|
|
274
|
+
pub fn encode(&self, input: Uint8Array) -> Buffer {
|
|
275
|
+
Buffer::from(self.engine.encode(input.as_ref()).into_bytes())
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
#[napi]
|
|
279
|
+
pub fn decode(&self, input: Uint8Array) -> Result<Buffer> {
|
|
280
|
+
self.engine
|
|
281
|
+
.decode(input.as_ref())
|
|
282
|
+
.map(Buffer::from)
|
|
283
|
+
.map_err(|e| Error::from_reason(e.to_string()))
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
#[cfg(test)]
|
|
288
|
+
mod tests {
|
|
289
|
+
use super::*;
|
|
290
|
+
|
|
291
|
+
#[test]
|
|
292
|
+
fn base64_roundtrip_standard() {
|
|
293
|
+
let data = b"hello world!";
|
|
294
|
+
let enc = base64::engine::general_purpose::STANDARD.encode(data);
|
|
295
|
+
assert_eq!(
|
|
296
|
+
base64_decode(Uint8Array::new(enc.into_bytes()), None, None)
|
|
297
|
+
.unwrap()
|
|
298
|
+
.as_ref(),
|
|
299
|
+
data
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
#[test]
|
|
304
|
+
fn base64url_no_padding() {
|
|
305
|
+
// 0xfb → 111110 11 → "-w" (url-safe, no padding)
|
|
306
|
+
let enc = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([0xfb]);
|
|
307
|
+
assert_eq!(enc, "-w");
|
|
308
|
+
let dec = base64url_decode(Uint8Array::new(enc.into_bytes())).unwrap();
|
|
309
|
+
assert_eq!(dec.as_ref(), [0xfb]);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
#[test]
|
|
313
|
+
fn base64_decode_rejects_invalid() {
|
|
314
|
+
assert!(base64_decode(Uint8Array::new(b"!!!".to_vec()), None, None).is_err());
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
#[test]
|
|
318
|
+
fn hex_roundtrip() {
|
|
319
|
+
let data = b"\x00\x01\xfe\xffAB";
|
|
320
|
+
let enc = hex_encode_bytes(data);
|
|
321
|
+
assert_eq!(enc, "0001feff4142");
|
|
322
|
+
assert_eq!(hex_decode_bytes(enc.as_bytes()).unwrap(), data);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
#[test]
|
|
326
|
+
fn hex_rejects_odd_and_bad() {
|
|
327
|
+
assert!(hex_decode_bytes(b"abc").is_err());
|
|
328
|
+
assert!(hex_decode_bytes(b"zz").is_err());
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
#[test]
|
|
332
|
+
fn codec_instance_encodes_urlsafe() {
|
|
333
|
+
let c = Base64Codec::new(Some(true), Some(false));
|
|
334
|
+
let enc = c.encode(Uint8Array::new(b"\xfb".to_vec()));
|
|
335
|
+
assert_eq!(enc.as_ref(), b"-w");
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ── reusable-output (_into) variants ──
|
|
339
|
+
|
|
340
|
+
#[test]
|
|
341
|
+
fn hex_encode_into_slice_matches_allocating() {
|
|
342
|
+
let data = b"\x00\x01\xfe\xffAB";
|
|
343
|
+
let expected = hex_encode_bytes(data);
|
|
344
|
+
let mut out = [0u8; 32];
|
|
345
|
+
let n = hex_encode_into_slice(data, &mut out).unwrap();
|
|
346
|
+
assert_eq!(n, expected.len());
|
|
347
|
+
assert_eq!(&out[..n], expected.as_bytes());
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
#[test]
|
|
351
|
+
fn hex_encode_into_reports_length() {
|
|
352
|
+
let data = b"hello";
|
|
353
|
+
let out = Uint8Array::new(vec![0u8; 16]);
|
|
354
|
+
let n = hex_encode_into(Uint8Array::new(data.to_vec()), out).unwrap();
|
|
355
|
+
assert_eq!(n as usize, data.len() * 2);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
#[test]
|
|
359
|
+
fn hex_encode_into_small_buffer_errors() {
|
|
360
|
+
let mut out = [0u8; 2];
|
|
361
|
+
assert!(hex_encode_into_slice(b"abc", &mut out).is_err());
|
|
362
|
+
let out2 = Uint8Array::new(vec![0u8; 4]);
|
|
363
|
+
assert!(hex_encode_into(Uint8Array::new(b"abcdef".to_vec()), out2).is_err());
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#[test]
|
|
367
|
+
fn hex_decode_into_roundtrips() {
|
|
368
|
+
let enc = hex_encode_bytes(b"\x00\x01\xfe\xffAB");
|
|
369
|
+
let mut out = [0u8; 16];
|
|
370
|
+
let n = hex_decode_into_slice(enc.as_bytes(), &mut out).unwrap();
|
|
371
|
+
assert_eq!(&out[..n], b"\x00\x01\xfe\xffAB");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
#[test]
|
|
375
|
+
fn hex_decode_into_rejects_bad_input() {
|
|
376
|
+
let mut out = [0u8; 4];
|
|
377
|
+
assert!(hex_decode_into_slice(b"abc", &mut out).is_err());
|
|
378
|
+
assert!(hex_decode_into_slice(b"zz", &mut out).is_err());
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
#[test]
|
|
382
|
+
fn hex_decode_into_small_buffer_errors() {
|
|
383
|
+
let mut out = [0u8; 2];
|
|
384
|
+
assert!(hex_decode_into_slice(b"0001020304", &mut out).is_err());
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
#[test]
|
|
388
|
+
fn base64_encode_into_reports_length() {
|
|
389
|
+
let data = b"hello world!";
|
|
390
|
+
let out = Uint8Array::new(vec![0u8; 64]);
|
|
391
|
+
let n = base64_encode_into(Uint8Array::new(data.to_vec()), out, None, None).unwrap();
|
|
392
|
+
assert_eq!(
|
|
393
|
+
n as usize,
|
|
394
|
+
base64::engine::general_purpose::STANDARD.encode(data).len()
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
#[test]
|
|
399
|
+
fn base64_encode_into_small_buffer_errors() {
|
|
400
|
+
let out = Uint8Array::new(vec![0u8; 4]);
|
|
401
|
+
assert!(
|
|
402
|
+
base64_encode_into(Uint8Array::new(b"hello world!".to_vec()), out, None, None).is_err()
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
#[test]
|
|
407
|
+
fn base64_decode_into_roundtrips() {
|
|
408
|
+
let data = b"hello world!";
|
|
409
|
+
let enc = base64::engine::general_purpose::STANDARD.encode(data);
|
|
410
|
+
let out = Uint8Array::new(vec![0u8; 32]);
|
|
411
|
+
let n = base64_decode_into(Uint8Array::new(enc.into_bytes()), out, None, None).unwrap();
|
|
412
|
+
assert_eq!(n as usize, data.len());
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
#[test]
|
|
416
|
+
fn base64_decode_into_rejects_invalid() {
|
|
417
|
+
let out = Uint8Array::new(vec![0u8; 32]);
|
|
418
|
+
assert!(base64_decode_into(Uint8Array::new(b"!!!".to_vec()), out, None, None).is_err());
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// rust/crypto/bcrypt.rs — bcrypt password hashing.
|
|
2
|
+
//
|
|
3
|
+
// Pure-Rust (bcrypt crate, Blowfish-based). Unlike argon2id (memory-hard), a
|
|
4
|
+
// bcrypt cost of `2^cost` is CPU-bound. We expose the standard `$2b$` PHC
|
|
5
|
+
// string format so the output is interchangeable with Bun.password / bcryptjs.
|
|
6
|
+
//
|
|
7
|
+
// Pure-Rust core stays unit-testable; only the entry points use napi types.
|
|
8
|
+
|
|
9
|
+
use napi::bindgen_prelude::*;
|
|
10
|
+
use napi_derive::napi;
|
|
11
|
+
|
|
12
|
+
/// bcrypt cost lower bound (bcrypt crate enforces 4..=31).
|
|
13
|
+
pub const BCRYPT_MIN_COST: u32 = 4;
|
|
14
|
+
/// bcrypt cost upper bound.
|
|
15
|
+
pub const BCRYPT_MAX_COST: u32 = 31;
|
|
16
|
+
|
|
17
|
+
/// Clamp a caller cost into the bcrypt-legal range (4..=31).
|
|
18
|
+
#[inline]
|
|
19
|
+
pub fn clamp_cost(cost: u32) -> u32 {
|
|
20
|
+
cost.clamp(BCRYPT_MIN_COST, BCRYPT_MAX_COST)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Hash a password with bcrypt at `cost`, returning a `$2b$` PHC string.
|
|
24
|
+
#[napi]
|
|
25
|
+
pub fn password_hash_bcrypt(password: Uint8Array, cost: u32) -> Result<String> {
|
|
26
|
+
let cost = clamp_cost(cost);
|
|
27
|
+
bcrypt::hash(password.as_ref(), cost).map_err(|e| Error::from_reason(e.to_string()))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// Verify a password against a bcrypt `$2b$` PHC string.
|
|
31
|
+
/// Returns false on ANY malformed hash (parity with argon2 `password_verify`).
|
|
32
|
+
#[napi]
|
|
33
|
+
pub fn password_verify_bcrypt(password: Uint8Array, hash: String) -> bool {
|
|
34
|
+
bcrypt::verify(password.as_ref(), &hash).unwrap_or(false)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
#[cfg(test)]
|
|
38
|
+
mod tests {
|
|
39
|
+
use super::*;
|
|
40
|
+
|
|
41
|
+
#[test]
|
|
42
|
+
fn clamp_cost_bounds() {
|
|
43
|
+
assert_eq!(clamp_cost(0), BCRYPT_MIN_COST);
|
|
44
|
+
assert_eq!(clamp_cost(3), BCRYPT_MIN_COST);
|
|
45
|
+
assert_eq!(clamp_cost(4), 4);
|
|
46
|
+
assert_eq!(clamp_cost(10), 10);
|
|
47
|
+
assert_eq!(clamp_cost(31), 31);
|
|
48
|
+
assert_eq!(clamp_cost(99), BCRYPT_MAX_COST);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
#[test]
|
|
52
|
+
fn bcrypt_roundtrip_and_wrong_password() {
|
|
53
|
+
let h = bcrypt::hash(b"password".as_slice(), 4).unwrap();
|
|
54
|
+
assert!(h.starts_with("$2b$04$"));
|
|
55
|
+
assert_eq!(h.len(), 60); // PHC bcrypt strings are exactly 60 chars
|
|
56
|
+
assert!(bcrypt::verify(b"password".as_slice(), &h).unwrap());
|
|
57
|
+
assert!(!bcrypt::verify(b"wrong".as_slice(), &h).unwrap());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[test]
|
|
61
|
+
fn bcrypt_is_cost_sensitive_and_self_contained() {
|
|
62
|
+
// Same password, two hashes differ (random salt) but both verify.
|
|
63
|
+
let a = bcrypt::hash(b"pw".as_slice(), 4).unwrap();
|
|
64
|
+
let b = bcrypt::hash(b"pw".as_slice(), 4).unwrap();
|
|
65
|
+
assert_ne!(a, b);
|
|
66
|
+
assert!(bcrypt::verify(b"pw".as_slice(), &a).unwrap());
|
|
67
|
+
assert!(bcrypt::verify(b"pw".as_slice(), &b).unwrap());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#[test]
|
|
71
|
+
fn napi_wrapper_clamps_and_roundtrips() {
|
|
72
|
+
// cost 3 is clamped up to 4 (cheap) — exercises the napi boundary.
|
|
73
|
+
let h = password_hash_bcrypt(Uint8Array::new(b"pw".to_vec()), 3).unwrap();
|
|
74
|
+
assert!(h.starts_with("$2b$04$"));
|
|
75
|
+
assert!(password_verify_bcrypt(
|
|
76
|
+
Uint8Array::new(b"pw".to_vec()),
|
|
77
|
+
h.clone()
|
|
78
|
+
));
|
|
79
|
+
assert!(!password_verify_bcrypt(
|
|
80
|
+
Uint8Array::new(b"nope".to_vec()),
|
|
81
|
+
h
|
|
82
|
+
));
|
|
83
|
+
// Malformed hash -> false (not a throw), matching argon2 semantics.
|
|
84
|
+
assert!(!password_verify_bcrypt(
|
|
85
|
+
Uint8Array::new(b"pw".to_vec()),
|
|
86
|
+
"garbage".to_string()
|
|
87
|
+
));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
// rust/crypto/cookie_sign.rs — signed cookies (`value.signature`, HMAC-SHA256).
|
|
2
|
+
//
|
|
3
|
+
// Composes the existing HMAC core + hex tables. The `CookieSigner`
|
|
4
|
+
// higher-order instance precomputes the HMAC key once in its constructor and
|
|
5
|
+
// reuses it across sign/verify calls.
|
|
6
|
+
|
|
7
|
+
use aws_lc_rs::hmac;
|
|
8
|
+
use napi::bindgen_prelude::*;
|
|
9
|
+
use napi_derive::napi;
|
|
10
|
+
|
|
11
|
+
use crate::util::bytes::{hex_decode_32, hex_encode_32};
|
|
12
|
+
|
|
13
|
+
/// Zero-alloc core: `value` ++ "." ++ 64-hex into `out`. Returns the bytes
|
|
14
|
+
/// written, or `None` on a too-small buffer. Used by the C-ABI (`bun:ffi`)
|
|
15
|
+
/// path to avoid the signed-cookie Vec + copy.
|
|
16
|
+
pub fn sign_cookie_into(value: &[u8], key: &hmac::Key, out: &mut [u8]) -> Option<usize> {
|
|
17
|
+
let need = value.len() + 1 + 64;
|
|
18
|
+
if out.len() < need {
|
|
19
|
+
return None;
|
|
20
|
+
}
|
|
21
|
+
let tag = hmac::sign(key, value);
|
|
22
|
+
let mut sig = [0u8; 64];
|
|
23
|
+
hex_encode_32(tag.as_ref(), &mut sig);
|
|
24
|
+
out[..value.len()].copy_from_slice(value);
|
|
25
|
+
out[value.len()] = b'.';
|
|
26
|
+
out[value.len() + 1..need].copy_from_slice(&sig);
|
|
27
|
+
Some(need)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// `value` ++ "." ++ lowercase-hex(HMAC-SHA256(secret, value)).
|
|
31
|
+
pub fn sign_cookie_bytes(value: &[u8], key: &hmac::Key) -> Vec<u8> {
|
|
32
|
+
let tag = hmac::sign(key, value);
|
|
33
|
+
let mut sig = [0u8; 64];
|
|
34
|
+
hex_encode_32(tag.as_ref(), &mut sig);
|
|
35
|
+
let mut out = Vec::with_capacity(value.len() + 1 + 64);
|
|
36
|
+
out.extend_from_slice(value);
|
|
37
|
+
out.push(b'.');
|
|
38
|
+
out.extend_from_slice(&sig);
|
|
39
|
+
out
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Zero-alloc core: constant-time verify into `out` (the value without its
|
|
43
|
+
/// signature). Returns the value length, or `None` on invalid signature /
|
|
44
|
+
/// too-small buffer. Used by the C-ABI (`bun:ffi`) path to avoid the Vec.
|
|
45
|
+
pub fn verify_cookie_into(signed: &[u8], key: &hmac::Key, out: &mut [u8]) -> Option<usize> {
|
|
46
|
+
let dot = signed.iter().rposition(|&b| b == b'.')?;
|
|
47
|
+
let (value, sig) = signed.split_at(dot);
|
|
48
|
+
let sig = &sig[1..];
|
|
49
|
+
if sig.len() != 64 {
|
|
50
|
+
return None;
|
|
51
|
+
}
|
|
52
|
+
let sig_bytes = hex_decode_32(sig)?;
|
|
53
|
+
if hmac::verify(key, value, &sig_bytes).is_err() {
|
|
54
|
+
return None;
|
|
55
|
+
}
|
|
56
|
+
if out.len() < value.len() {
|
|
57
|
+
return None;
|
|
58
|
+
}
|
|
59
|
+
out[..value.len()].copy_from_slice(value);
|
|
60
|
+
Some(value.len())
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Constant-time verify; returns the signed value without its signature.
|
|
64
|
+
pub fn verify_cookie_bytes(signed: &[u8], key: &hmac::Key) -> Option<Vec<u8>> {
|
|
65
|
+
let dot = signed.iter().rposition(|&b| b == b'.')?;
|
|
66
|
+
let (value, sig) = signed.split_at(dot);
|
|
67
|
+
let sig = &sig[1..];
|
|
68
|
+
if sig.len() != 64 {
|
|
69
|
+
return None;
|
|
70
|
+
}
|
|
71
|
+
let sig_bytes = hex_decode_32(sig)?;
|
|
72
|
+
hmac::verify(key, value, &sig_bytes)
|
|
73
|
+
.ok()
|
|
74
|
+
.map(|()| value.to_vec())
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Constant-time verify; returns whether the signature matches, WITHOUT
|
|
78
|
+
/// materializing the signed value. Same checks as [`verify_cookie_bytes`] but
|
|
79
|
+
/// skips the `value.to_vec()` — used by the packed batch to avoid a discarded
|
|
80
|
+
/// allocation per item.
|
|
81
|
+
pub fn verify_cookie_bytes_bool(signed: &[u8], key: &hmac::Key) -> bool {
|
|
82
|
+
let Some(dot) = signed.iter().rposition(|&b| b == b'.') else {
|
|
83
|
+
return false;
|
|
84
|
+
};
|
|
85
|
+
let (value, sig) = signed.split_at(dot);
|
|
86
|
+
let sig = &sig[1..];
|
|
87
|
+
if sig.len() != 64 {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
let Some(sig_bytes) = hex_decode_32(sig) else {
|
|
91
|
+
return false;
|
|
92
|
+
};
|
|
93
|
+
hmac::verify(key, value, &sig_bytes).is_ok()
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#[napi]
|
|
97
|
+
pub fn sign_cookie(value: Uint8Array, secret: Uint8Array) -> Buffer {
|
|
98
|
+
let key = hmac::Key::new(hmac::HMAC_SHA256, secret.as_ref());
|
|
99
|
+
Buffer::from(sign_cookie_bytes(value.as_ref(), &key))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#[napi]
|
|
103
|
+
pub fn verify_cookie(signed: Uint8Array, secret: Uint8Array) -> Option<Buffer> {
|
|
104
|
+
let key = hmac::Key::new(hmac::HMAC_SHA256, secret.as_ref());
|
|
105
|
+
verify_cookie_bytes(signed.as_ref(), &key).map(Buffer::from)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Higher-order instance: HMAC key compiled once, reused across calls.
|
|
109
|
+
#[napi]
|
|
110
|
+
pub struct CookieSigner {
|
|
111
|
+
key: hmac::Key,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[napi]
|
|
115
|
+
impl CookieSigner {
|
|
116
|
+
#[napi(constructor)]
|
|
117
|
+
pub fn new(secret: Uint8Array) -> Self {
|
|
118
|
+
Self {
|
|
119
|
+
key: hmac::Key::new(hmac::HMAC_SHA256, secret.as_ref()),
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
#[napi]
|
|
124
|
+
pub fn sign(&self, value: Uint8Array) -> Buffer {
|
|
125
|
+
Buffer::from(sign_cookie_bytes(value.as_ref(), &self.key))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
#[napi]
|
|
129
|
+
pub fn verify(&self, signed: Uint8Array) -> Option<Buffer> {
|
|
130
|
+
verify_cookie_bytes(signed.as_ref(), &self.key).map(Buffer::from)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[cfg(test)]
|
|
135
|
+
mod tests {
|
|
136
|
+
use super::*;
|
|
137
|
+
|
|
138
|
+
fn key(secret: &[u8]) -> hmac::Key {
|
|
139
|
+
hmac::Key::new(hmac::HMAC_SHA256, secret)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
#[test]
|
|
143
|
+
fn sign_then_verify_roundtrip() {
|
|
144
|
+
let k = key(b"super-secret");
|
|
145
|
+
let value = b"session=abc123";
|
|
146
|
+
let signed = sign_cookie_bytes(value, &k);
|
|
147
|
+
assert!(signed.starts_with(value));
|
|
148
|
+
assert_eq!(signed[value.len()], b'.');
|
|
149
|
+
assert_eq!(verify_cookie_bytes(&signed, &k).unwrap(), value);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#[test]
|
|
153
|
+
fn verify_rejects_tampered() {
|
|
154
|
+
let k = key(b"super-secret");
|
|
155
|
+
let value = b"session=abc123";
|
|
156
|
+
let mut signed = sign_cookie_bytes(value, &k);
|
|
157
|
+
let last = signed.len() - 1;
|
|
158
|
+
signed[last] = if signed[last] == b'0' { b'1' } else { b'0' };
|
|
159
|
+
assert!(verify_cookie_bytes(&signed, &k).is_none());
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
#[test]
|
|
163
|
+
fn verify_rejects_wrong_secret_and_malformed() {
|
|
164
|
+
let signed = sign_cookie_bytes(b"x", &key(b"secret-a"));
|
|
165
|
+
assert!(verify_cookie_bytes(&signed, &key(b"secret-b")).is_none());
|
|
166
|
+
assert!(verify_cookie_bytes(b"no-dot", &key(b"s")).is_none());
|
|
167
|
+
assert!(verify_cookie_bytes(b"x.bad", &key(b"s")).is_none());
|
|
168
|
+
}
|
|
169
|
+
}
|