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,265 @@
|
|
|
1
|
+
// rust/crypto/argon2.rs — argon2id password hashing.
|
|
2
|
+
//
|
|
3
|
+
// Backend-framework feature: password storage hashing. argon2id is the OWASP
|
|
4
|
+
// recommendation for new hashes; it is deliberately CPU/memory-heavy, which
|
|
5
|
+
// makes it an ideal candidate to offload to Rust (and parallelize with rayon).
|
|
6
|
+
// The `argon2` crate (RustCrypto) is pure Rust; `SaltString::encode_b64` lets us
|
|
7
|
+
// feed a caller-supplied salt so outputs are deterministic (unit-testable).
|
|
8
|
+
//
|
|
9
|
+
// Pure-Rust core (no napi types) stays unit-testable; only the entry points
|
|
10
|
+
// use napi types.
|
|
11
|
+
|
|
12
|
+
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
|
|
13
|
+
use argon2::{Algorithm, Argon2, Params, Version};
|
|
14
|
+
use napi::bindgen_prelude::*;
|
|
15
|
+
use napi_derive::napi;
|
|
16
|
+
|
|
17
|
+
// ── Options ────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/// argon2id parameters (defaults follow OWASP: 19 MiB, 2 iterations, 1 lane,
|
|
20
|
+
/// 32-byte output). Callers may lower the memory cost for fast benchmarks.
|
|
21
|
+
#[napi(object)]
|
|
22
|
+
pub struct PasswordHashOptions {
|
|
23
|
+
/// Memory cost in KiB (m). Default: 19_456 (19 MiB).
|
|
24
|
+
pub m_cost: Option<u32>,
|
|
25
|
+
/// Iterations (t). Default: 2.
|
|
26
|
+
pub t_cost: Option<u32>,
|
|
27
|
+
/// Parallelism (p). Default: 1.
|
|
28
|
+
pub p_cost: Option<u32>,
|
|
29
|
+
/// Output length in bytes. Default: 32.
|
|
30
|
+
pub out_len: Option<u32>,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fn resolve_opts(o: Option<&PasswordHashOptions>) -> (u32, u32, u32, u32) {
|
|
34
|
+
match o {
|
|
35
|
+
Some(o) => (
|
|
36
|
+
o.m_cost.unwrap_or(19_456),
|
|
37
|
+
o.t_cost.unwrap_or(2),
|
|
38
|
+
o.p_cost.unwrap_or(1),
|
|
39
|
+
o.out_len.unwrap_or(32),
|
|
40
|
+
),
|
|
41
|
+
None => (19_456, 2, 1, 32),
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Pure-Rust core ─────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/// Hash `password` with argon2id using the given raw salt bytes. Returns the
|
|
48
|
+
/// PHC string (`$argon2id$v=19$m=...,t=...,p=...$<b64 salt>$<b64 hash>`).
|
|
49
|
+
pub fn hash_password(
|
|
50
|
+
password: &[u8],
|
|
51
|
+
salt: &[u8],
|
|
52
|
+
m_cost: u32,
|
|
53
|
+
t_cost: u32,
|
|
54
|
+
p_cost: u32,
|
|
55
|
+
out_len: u32,
|
|
56
|
+
) -> std::result::Result<String, argon2::password_hash::Error> {
|
|
57
|
+
let params = Params::new(m_cost, t_cost, p_cost, Some(out_len as usize))?;
|
|
58
|
+
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
|
|
59
|
+
let salt = SaltString::encode_b64(salt)?;
|
|
60
|
+
Ok(argon2.hash_password(password, &salt)?.to_string())
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/// Verify a password against a PHC string (constant-time internally).
|
|
64
|
+
pub fn verify_password(password: &[u8], phc: &[u8]) -> bool {
|
|
65
|
+
let Ok(phc_str) = std::str::from_utf8(phc) else {
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
68
|
+
let Ok(parsed) = PasswordHash::new(phc_str) else {
|
|
69
|
+
return false;
|
|
70
|
+
};
|
|
71
|
+
Argon2::default().verify_password(password, &parsed).is_ok()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── NAPI entry points ──────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/// Hash a password with argon2id. `salt` must be at least 8 bytes; output is a
|
|
77
|
+
/// PHC string. Deterministic given the same inputs.
|
|
78
|
+
#[napi]
|
|
79
|
+
pub fn password_hash(
|
|
80
|
+
password: Uint8Array,
|
|
81
|
+
salt: Uint8Array,
|
|
82
|
+
options: Option<PasswordHashOptions>,
|
|
83
|
+
) -> Result<Buffer> {
|
|
84
|
+
let (m, t, p, out_len) = resolve_opts(options.as_ref());
|
|
85
|
+
let phc = hash_password(password.as_ref(), salt.as_ref(), m, t, p, out_len)
|
|
86
|
+
.map_err(|e| Error::from_reason(format!("argon2 hash failed: {e}")))?;
|
|
87
|
+
Ok(Buffer::from(phc.into_bytes()))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Verify a password against a PHC string. Returns false on any parse failure.
|
|
91
|
+
#[napi]
|
|
92
|
+
pub fn password_verify(password: Uint8Array, phc: Buffer) -> bool {
|
|
93
|
+
verify_password(password.as_ref(), phc.as_ref())
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Packed password-verify batch (two packed lists: passwords + PHC strings,
|
|
97
|
+
/// zipped) → bitset of valid. Items with malformed PHC strings yield `false`.
|
|
98
|
+
#[napi]
|
|
99
|
+
pub fn password_verify_batch_packed(passwords: Uint8Array, phcs: Uint8Array) -> Result<Buffer> {
|
|
100
|
+
use crate::util::packed::PackedIter;
|
|
101
|
+
let pws = passwords.as_ref();
|
|
102
|
+
let ph = phcs.as_ref();
|
|
103
|
+
let count = PackedIter::new(pws)?.len().min(PackedIter::new(ph)?.len());
|
|
104
|
+
let mut out = Vec::with_capacity(4 + count.div_ceil(8));
|
|
105
|
+
out.extend_from_slice(&(count as u32).to_le_bytes());
|
|
106
|
+
out.resize(4 + count.div_ceil(8), 0);
|
|
107
|
+
for (i, (pw, phc)) in PackedIter::new(pws)?.zip(PackedIter::new(ph)?).enumerate() {
|
|
108
|
+
if verify_password(pw, phc) {
|
|
109
|
+
out[4 + (i >> 3)] |= 1 << (i & 7);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
Ok(Buffer::from(out))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// Higher-order instance: precompiles the argon2id `Params`/`Argon2` once at
|
|
116
|
+
/// construction (fixed m/t/p/out_len), so every `hash` skips the per-call
|
|
117
|
+
/// parameter construction — the CPU/memory cost of the hash itself is
|
|
118
|
+
/// unchanged, only the setup is amortized.
|
|
119
|
+
#[napi]
|
|
120
|
+
pub struct Argon2Hasher {
|
|
121
|
+
argon2: Argon2<'static>,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#[napi]
|
|
125
|
+
impl Argon2Hasher {
|
|
126
|
+
#[napi(constructor)]
|
|
127
|
+
pub fn new(options: Option<PasswordHashOptions>) -> Result<Self> {
|
|
128
|
+
let (m, t, p, out_len) = resolve_opts(options.as_ref());
|
|
129
|
+
let params = Params::new(m, t, p, Some(out_len as usize))
|
|
130
|
+
.map_err(|e| Error::from_reason(format!("invalid argon2 params: {e}")))?;
|
|
131
|
+
Ok(Self {
|
|
132
|
+
argon2: Argon2::new(Algorithm::Argon2id, Version::V0x13, params),
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// Hash a password with the precompiled params → PHC string bytes.
|
|
137
|
+
#[napi]
|
|
138
|
+
pub fn hash(&self, password: Uint8Array, salt: Uint8Array) -> Result<Buffer> {
|
|
139
|
+
let salt = SaltString::encode_b64(salt.as_ref())
|
|
140
|
+
.map_err(|e| Error::from_reason(format!("invalid salt: {e}")))?;
|
|
141
|
+
let phc = self
|
|
142
|
+
.argon2
|
|
143
|
+
.hash_password(password.as_ref(), &salt)
|
|
144
|
+
.map_err(|e| Error::from_reason(format!("argon2 hash failed: {e}")))?;
|
|
145
|
+
Ok(Buffer::from(phc.to_string().into_bytes()))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Verify a password against a PHC string. Returns false on any failure.
|
|
149
|
+
#[napi]
|
|
150
|
+
pub fn verify(&self, password: Uint8Array, phc: Buffer) -> bool {
|
|
151
|
+
verify_password(password.as_ref(), phc.as_ref())
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/// Parallel argon2id batch: packed `[u32 count]{[u32 len][password]}` in →
|
|
156
|
+
/// packed `[u32 count]{[u32 len][phc]}` out, all hashed with the SAME salt.
|
|
157
|
+
#[napi]
|
|
158
|
+
pub fn password_hash_batch_packed(
|
|
159
|
+
data: Uint8Array,
|
|
160
|
+
salt: Uint8Array,
|
|
161
|
+
options: Option<PasswordHashOptions>,
|
|
162
|
+
) -> Result<Buffer> {
|
|
163
|
+
let (m, t, p, out_len) = resolve_opts(options.as_ref());
|
|
164
|
+
|
|
165
|
+
let phc_for = |password: &[u8]| -> Vec<u8> {
|
|
166
|
+
hash_password(password, salt.as_ref(), m, t, p, out_len)
|
|
167
|
+
.map(|s| s.into_bytes())
|
|
168
|
+
.unwrap_or_default()
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
crate::util::run_packed_batch(data.as_ref(), phc_for).map(Buffer::from)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#[cfg(test)]
|
|
175
|
+
mod tests {
|
|
176
|
+
use super::*;
|
|
177
|
+
|
|
178
|
+
const PASSWORD: &[u8] = b"correct horse battery staple";
|
|
179
|
+
const SALT: &[u8] = b"0123456789abcdef";
|
|
180
|
+
|
|
181
|
+
#[test]
|
|
182
|
+
fn hash_then_verify_roundtrip() {
|
|
183
|
+
let phc = hash_password(PASSWORD, SALT, 4096, 2, 1, 32).unwrap();
|
|
184
|
+
assert!(phc.starts_with("$argon2id$v=19$"));
|
|
185
|
+
assert!(verify_password(PASSWORD, phc.as_bytes()));
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
#[test]
|
|
189
|
+
fn argon2_hasher_instance_roundtrips() {
|
|
190
|
+
// Precompiled-params instance: hash/verify with no per-call Params
|
|
191
|
+
// construction. Output must match the scalar core for the same params.
|
|
192
|
+
let hasher = Argon2Hasher::new(Some(PasswordHashOptions {
|
|
193
|
+
m_cost: Some(4096),
|
|
194
|
+
t_cost: Some(2),
|
|
195
|
+
p_cost: Some(1),
|
|
196
|
+
out_len: Some(32),
|
|
197
|
+
}))
|
|
198
|
+
.unwrap();
|
|
199
|
+
|
|
200
|
+
let phc = hasher
|
|
201
|
+
.hash(
|
|
202
|
+
Uint8Array::new(PASSWORD.to_vec()),
|
|
203
|
+
Uint8Array::new(SALT.to_vec()),
|
|
204
|
+
)
|
|
205
|
+
.unwrap();
|
|
206
|
+
let phc_str = String::from_utf8(phc.to_vec()).unwrap();
|
|
207
|
+
assert!(phc_str.starts_with("$argon2id$v=19$"));
|
|
208
|
+
|
|
209
|
+
// The instance output matches the scalar core byte-for-byte.
|
|
210
|
+
let scalar = hash_password(PASSWORD, SALT, 4096, 2, 1, 32).unwrap();
|
|
211
|
+
assert_eq!(phc_str, scalar);
|
|
212
|
+
|
|
213
|
+
// Verify: correct password true, wrong password false.
|
|
214
|
+
assert!(hasher.verify(
|
|
215
|
+
Uint8Array::new(PASSWORD.to_vec()),
|
|
216
|
+
Buffer::from(phc_str.clone().into_bytes())
|
|
217
|
+
));
|
|
218
|
+
assert!(!hasher.verify(
|
|
219
|
+
Uint8Array::new(b"wrong-password".to_vec()),
|
|
220
|
+
Buffer::from(phc_str.into_bytes())
|
|
221
|
+
));
|
|
222
|
+
|
|
223
|
+
// Invalid options → construction error.
|
|
224
|
+
assert!(Argon2Hasher::new(Some(PasswordHashOptions {
|
|
225
|
+
m_cost: Some(0),
|
|
226
|
+
t_cost: Some(2),
|
|
227
|
+
p_cost: Some(1),
|
|
228
|
+
out_len: Some(32),
|
|
229
|
+
}))
|
|
230
|
+
.is_err());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
#[test]
|
|
234
|
+
fn verify_rejects_wrong_password() {
|
|
235
|
+
let phc = hash_password(PASSWORD, SALT, 4096, 2, 1, 32).unwrap();
|
|
236
|
+
assert!(!verify_password(b"wrong password", phc.as_bytes()));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#[test]
|
|
240
|
+
fn verify_is_deterministic_given_salt() {
|
|
241
|
+
let a = hash_password(PASSWORD, SALT, 4096, 2, 1, 32).unwrap();
|
|
242
|
+
let b = hash_password(PASSWORD, SALT, 4096, 2, 1, 32).unwrap();
|
|
243
|
+
assert_eq!(a, b);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
#[test]
|
|
247
|
+
fn different_salt_different_hash() {
|
|
248
|
+
let a = hash_password(PASSWORD, b"aaaaaaaaaaaaaaaa", 4096, 2, 1, 32).unwrap();
|
|
249
|
+
let b = hash_password(PASSWORD, b"bbbbbbbbbbbbbbbb", 4096, 2, 1, 32).unwrap();
|
|
250
|
+
assert_ne!(a, b);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#[test]
|
|
254
|
+
fn verify_rejects_garbage() {
|
|
255
|
+
assert!(!verify_password(PASSWORD, b"not-a-phc-string"));
|
|
256
|
+
assert!(!verify_password(PASSWORD, b""));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#[test]
|
|
260
|
+
fn params_validation() {
|
|
261
|
+
assert!(hash_password(PASSWORD, SALT, 0, 2, 1, 32).is_err());
|
|
262
|
+
// Salt too short (argon2 requires >= 8 bytes).
|
|
263
|
+
assert!(hash_password(PASSWORD, b"123", 4096, 2, 1, 32).is_err());
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -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
|
+
}
|