castrum 0.9.10 → 0.9.11
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 +132 -91
- package/Cargo.toml +14 -4
- package/README.md +5 -2
- 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/docs/FFI_BUN_GUIDE.md +8 -4
- package/docs/HOW_TO_ADD_AN_OP.md +12 -9
- package/docs/REPO_MAP.md +10 -6
- package/docs/ai/TREE.md +3 -1
- package/package.json +3 -2
- package/rust/ffi/crypto.rs +224 -197
- package/rust/ffi/hashing.rs +68 -51
- package/rust/ffi/http.rs +33 -24
- package/rust/ffi/ingress.rs +13 -12
- package/rust/ffi/metrics.rs +8 -7
- package/rust/ffi/payload.rs +26 -23
- package/rust/ffi/probe.rs +18 -11
- package/rust/ffi/route.rs +6 -5
- package/rust/ffi/task.rs +14 -11
- package/rust/ffi/tests.rs +95 -0
- package/rust/ffi/util.rs +13 -0
- package/rust/ffi/validators.rs +36 -29
- package/rust/http/http_date.rs +4 -0
- package/rust/panic_safety.rs +191 -0
- package/rust/proptest_suite.rs +131 -1
- package/rust/util/bytes.rs +32 -0
package/rust/ffi/crypto.rs
CHANGED
|
@@ -9,8 +9,7 @@ use pbkdf2::pbkdf2_hmac;
|
|
|
9
9
|
use sha2::Sha256;
|
|
10
10
|
use std::slice;
|
|
11
11
|
|
|
12
|
-
use super::util::{aead_alg, cstring_return, panic_guard, with_hmac_key_cached};
|
|
13
|
-
|
|
12
|
+
use super::util::{aead_alg, c_abi, cstring_return, panic_guard, with_hmac_key_cached};
|
|
14
13
|
/// HMAC-SHA256 verify → 1/0 (constant-time, hex sig compared after
|
|
15
14
|
/// whitespace-trim — mirrors the napi scalar path).
|
|
16
15
|
///
|
|
@@ -25,15 +24,17 @@ pub unsafe extern "C" fn castrum_hmac_sha256_verify(
|
|
|
25
24
|
sig: *const u8,
|
|
26
25
|
slen: usize,
|
|
27
26
|
) -> u8 {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
27
|
+
c_abi!(0, {
|
|
28
|
+
if key.is_null() || data.is_null() || sig.is_null() {
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
let sig_t = crate::util::bytes::trim_ascii_whitespace(slice::from_raw_parts(sig, slen));
|
|
32
|
+
let Some(sig_bytes) = crate::util::bytes::hex_decode_32(sig_t) else {
|
|
33
|
+
return 0;
|
|
34
|
+
};
|
|
35
|
+
with_hmac_key_cached(slice::from_raw_parts(key, klen), |k| {
|
|
36
|
+
u8::from(hmac::verify(k, slice::from_raw_parts(data, dlen), &sig_bytes).is_ok())
|
|
37
|
+
})
|
|
37
38
|
})
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -48,14 +49,16 @@ pub unsafe extern "C" fn castrum_csrf_verify(
|
|
|
48
49
|
secret: *const u8,
|
|
49
50
|
slen: usize,
|
|
50
51
|
) -> u8 {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
52
|
+
c_abi!(0, {
|
|
53
|
+
if token.is_null() || secret.is_null() {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
with_hmac_key_cached(slice::from_raw_parts(secret, slen), |k| {
|
|
57
|
+
u8::from(crate::crypto::csrf::csrf_verify_with_key(
|
|
58
|
+
slice::from_raw_parts(token, tlen),
|
|
59
|
+
k,
|
|
60
|
+
))
|
|
61
|
+
})
|
|
59
62
|
})
|
|
60
63
|
}
|
|
61
64
|
|
|
@@ -128,16 +131,18 @@ pub unsafe extern "C" fn castrum_hmac_sha256(
|
|
|
128
131
|
out: *mut u8,
|
|
129
132
|
out_cap: usize,
|
|
130
133
|
) -> usize {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
134
|
+
c_abi!(0, {
|
|
135
|
+
if key.is_null() || data.is_null() || out.is_null() || out_cap < 64 {
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
let tag = with_hmac_key_cached(slice::from_raw_parts(key, klen), |k| {
|
|
139
|
+
hmac::sign(k, slice::from_raw_parts(data, dlen))
|
|
140
|
+
});
|
|
141
|
+
let mut hex = [0u8; 64];
|
|
142
|
+
crate::util::bytes::hex_encode_32(tag.as_ref(), &mut hex);
|
|
143
|
+
slice::from_raw_parts_mut(out, 64).copy_from_slice(&hex);
|
|
144
|
+
64
|
|
145
|
+
})
|
|
141
146
|
}
|
|
142
147
|
|
|
143
148
|
/// Sign a cookie `value` as `value.<64-hex sig>`, returned as a null-terminated
|
|
@@ -152,13 +157,15 @@ pub unsafe extern "C" fn castrum_sign_cookie(
|
|
|
152
157
|
secret: *const u8,
|
|
153
158
|
slen: usize,
|
|
154
159
|
) -> *const std::os::raw::c_char {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
160
|
+
c_abi!(std::ptr::null(), {
|
|
161
|
+
if value.is_null() || secret.is_null() {
|
|
162
|
+
return std::ptr::null();
|
|
163
|
+
}
|
|
164
|
+
let v = slice::from_raw_parts(value, vlen);
|
|
165
|
+
cstring_return(vlen + 65, |out| {
|
|
166
|
+
with_hmac_key_cached(slice::from_raw_parts(secret, slen), |key| {
|
|
167
|
+
crate::crypto::cookie_sign::sign_cookie_into(v, key, out)
|
|
168
|
+
})
|
|
162
169
|
})
|
|
163
170
|
})
|
|
164
171
|
}
|
|
@@ -181,22 +188,24 @@ pub unsafe extern "C" fn castrum_sign_cookie_into(
|
|
|
181
188
|
out: *mut u8,
|
|
182
189
|
out_cap: usize,
|
|
183
190
|
) -> usize {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
191
|
+
c_abi!(0, {
|
|
192
|
+
if value.is_null() || secret.is_null() || out.is_null() {
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
let needed = vlen + 65;
|
|
196
|
+
if out_cap < needed {
|
|
197
|
+
return needed;
|
|
198
|
+
}
|
|
199
|
+
let v = slice::from_raw_parts(value, vlen);
|
|
200
|
+
with_hmac_key_cached(slice::from_raw_parts(secret, slen), |key| {
|
|
201
|
+
crate::crypto::cookie_sign::sign_cookie_into(
|
|
202
|
+
v,
|
|
203
|
+
key,
|
|
204
|
+
slice::from_raw_parts_mut(out, out_cap),
|
|
205
|
+
)
|
|
206
|
+
})
|
|
207
|
+
.unwrap_or_default()
|
|
198
208
|
})
|
|
199
|
-
.unwrap_or_default()
|
|
200
209
|
}
|
|
201
210
|
|
|
202
211
|
/// Verify a signed cookie → the value without its signature, returned as a
|
|
@@ -212,13 +221,15 @@ pub unsafe extern "C" fn castrum_verify_cookie(
|
|
|
212
221
|
secret: *const u8,
|
|
213
222
|
klen: usize,
|
|
214
223
|
) -> *const std::os::raw::c_char {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
224
|
+
c_abi!(std::ptr::null(), {
|
|
225
|
+
if signed.is_null() || secret.is_null() {
|
|
226
|
+
return std::ptr::null();
|
|
227
|
+
}
|
|
228
|
+
let s = slice::from_raw_parts(signed, slen);
|
|
229
|
+
cstring_return(slen, |out| {
|
|
230
|
+
with_hmac_key_cached(slice::from_raw_parts(secret, klen), |key| {
|
|
231
|
+
crate::crypto::cookie_sign::verify_cookie_into(s, key, out)
|
|
232
|
+
})
|
|
222
233
|
})
|
|
223
234
|
})
|
|
224
235
|
}
|
|
@@ -242,30 +253,32 @@ pub unsafe extern "C" fn castrum_verify_cookie_into(
|
|
|
242
253
|
out: *mut u8,
|
|
243
254
|
out_cap: usize,
|
|
244
255
|
) -> usize {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
let s = slice::from_raw_parts(signed, slen);
|
|
249
|
-
// The value is a prefix of `signed` (everything before the last `.`), so
|
|
250
|
-
// `slen` is always a sufficient bound — needed-size only differs when the
|
|
251
|
-
// caller passed less.
|
|
252
|
-
if out_cap < slen {
|
|
253
|
-
// Compute the actual needed length without writing (a full verify is
|
|
254
|
-
// wasted if we're just sizing; the caller retries at most once).
|
|
255
|
-
let dot = s.iter().rposition(|&b| b == b'.').unwrap_or(0);
|
|
256
|
-
if dot != 0 {
|
|
257
|
-
return dot;
|
|
256
|
+
c_abi!(0, {
|
|
257
|
+
if signed.is_null() || secret.is_null() || out.is_null() {
|
|
258
|
+
return 0;
|
|
258
259
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
260
|
+
let s = slice::from_raw_parts(signed, slen);
|
|
261
|
+
// The value is a prefix of `signed` (everything before the last `.`), so
|
|
262
|
+
// `slen` is always a sufficient bound — needed-size only differs when the
|
|
263
|
+
// caller passed less.
|
|
264
|
+
if out_cap < slen {
|
|
265
|
+
// Compute the actual needed length without writing (a full verify is
|
|
266
|
+
// wasted if we're just sizing; the caller retries at most once).
|
|
267
|
+
let dot = s.iter().rposition(|&b| b == b'.').unwrap_or(0);
|
|
268
|
+
if dot != 0 {
|
|
269
|
+
return dot;
|
|
270
|
+
}
|
|
271
|
+
return slen;
|
|
272
|
+
}
|
|
273
|
+
with_hmac_key_cached(slice::from_raw_parts(secret, klen), |key| {
|
|
274
|
+
crate::crypto::cookie_sign::verify_cookie_into(
|
|
275
|
+
s,
|
|
276
|
+
key,
|
|
277
|
+
slice::from_raw_parts_mut(out, out_cap),
|
|
278
|
+
)
|
|
279
|
+
})
|
|
280
|
+
.unwrap_or_default()
|
|
267
281
|
})
|
|
268
|
-
.unwrap_or_default()
|
|
269
282
|
}
|
|
270
283
|
|
|
271
284
|
/// CSRF token (`<64-hex(rnd)>.<64-hex(sig)>`, 129 bytes) returned as a
|
|
@@ -278,25 +291,27 @@ pub unsafe extern "C" fn castrum_csrf_token(
|
|
|
278
291
|
secret: *const u8,
|
|
279
292
|
slen: usize,
|
|
280
293
|
) -> *const std::os::raw::c_char {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
out
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
294
|
+
c_abi!(std::ptr::null(), {
|
|
295
|
+
if secret.is_null() {
|
|
296
|
+
return std::ptr::null();
|
|
297
|
+
}
|
|
298
|
+
let mut rnd = [0u8; 32];
|
|
299
|
+
if getrandom::fill(&mut rnd).is_err() {
|
|
300
|
+
return std::ptr::null();
|
|
301
|
+
}
|
|
302
|
+
let mut rnd_hex = [0u8; 64];
|
|
303
|
+
crate::util::bytes::hex_encode(&rnd, &mut rnd_hex);
|
|
304
|
+
let mut sig_hex = [0u8; 64];
|
|
305
|
+
let signed = with_hmac_key_cached(slice::from_raw_parts(secret, slen), |k| {
|
|
306
|
+
hmac::sign(k, &rnd_hex)
|
|
307
|
+
});
|
|
308
|
+
crate::util::bytes::hex_encode(signed.as_ref(), &mut sig_hex);
|
|
309
|
+
cstring_return(129, |out| {
|
|
310
|
+
out[..64].copy_from_slice(&rnd_hex);
|
|
311
|
+
out[64] = b'.';
|
|
312
|
+
out[65..129].copy_from_slice(&sig_hex);
|
|
313
|
+
Some(129)
|
|
314
|
+
})
|
|
300
315
|
})
|
|
301
316
|
}
|
|
302
317
|
|
|
@@ -314,28 +329,30 @@ pub unsafe extern "C" fn castrum_csrf_token_into(
|
|
|
314
329
|
out: *mut u8,
|
|
315
330
|
out_cap: usize,
|
|
316
331
|
) -> usize {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
332
|
+
c_abi!(0, {
|
|
333
|
+
if secret.is_null() || out.is_null() {
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
|
336
|
+
if out_cap < 129 {
|
|
337
|
+
return 129;
|
|
338
|
+
}
|
|
339
|
+
let mut rnd = [0u8; 32];
|
|
340
|
+
if getrandom::fill(&mut rnd).is_err() {
|
|
341
|
+
return 0;
|
|
342
|
+
}
|
|
343
|
+
let mut rnd_hex = [0u8; 64];
|
|
344
|
+
crate::util::bytes::hex_encode(&rnd, &mut rnd_hex);
|
|
345
|
+
let mut sig_hex = [0u8; 64];
|
|
346
|
+
let signed = with_hmac_key_cached(slice::from_raw_parts(secret, slen), |k| {
|
|
347
|
+
hmac::sign(k, &rnd_hex)
|
|
348
|
+
});
|
|
349
|
+
crate::util::bytes::hex_encode(signed.as_ref(), &mut sig_hex);
|
|
350
|
+
let o = slice::from_raw_parts_mut(out, 129);
|
|
351
|
+
o[..64].copy_from_slice(&rnd_hex);
|
|
352
|
+
o[64] = b'.';
|
|
353
|
+
o[65..129].copy_from_slice(&sig_hex);
|
|
354
|
+
129
|
|
355
|
+
})
|
|
339
356
|
}
|
|
340
357
|
|
|
341
358
|
/// Argon2id password hash → PHC string bytes into `out` (m/t/p/out_len params).
|
|
@@ -434,23 +451,25 @@ pub unsafe extern "C" fn castrum_pbkdf2_sha256(
|
|
|
434
451
|
out: *mut u8,
|
|
435
452
|
out_cap: usize,
|
|
436
453
|
) -> usize {
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
+
c_abi!(0, {
|
|
455
|
+
if password.is_null() || salt.is_null() || out.is_null() {
|
|
456
|
+
return 0;
|
|
457
|
+
}
|
|
458
|
+
let dk_len = dk_len.clamp(
|
|
459
|
+
crate::crypto::pbkdf2::PBKDF2_MIN_LEN,
|
|
460
|
+
crate::crypto::pbkdf2::PBKDF2_MAX_LEN,
|
|
461
|
+
) as usize;
|
|
462
|
+
if out_cap < dk_len {
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
pbkdf2_hmac::<Sha256>(
|
|
466
|
+
slice::from_raw_parts(password, plen),
|
|
467
|
+
slice::from_raw_parts(salt, slen),
|
|
468
|
+
rounds.max(1),
|
|
469
|
+
slice::from_raw_parts_mut(out, dk_len),
|
|
470
|
+
);
|
|
471
|
+
dk_len
|
|
472
|
+
})
|
|
454
473
|
}
|
|
455
474
|
|
|
456
475
|
/// AEAD encrypt → ciphertext+tag into `out`; `alg` 0 = AES-256-GCM, 1 = ChaCha20.
|
|
@@ -552,19 +571,21 @@ pub unsafe extern "C" fn castrum_base64_encode(
|
|
|
552
571
|
url_safe: u8,
|
|
553
572
|
padding: u8,
|
|
554
573
|
) -> usize {
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
574
|
+
c_abi!(0, {
|
|
575
|
+
if data.is_null() || out.is_null() {
|
|
576
|
+
return 0;
|
|
577
|
+
}
|
|
578
|
+
// Zero-alloc core (`Engine::encode_slice`) — no intermediate Vec. A
|
|
579
|
+
// too-small buffer yields 0 (the JS wrappers size exactly, so a miss is a
|
|
580
|
+
// caller bug / real error).
|
|
581
|
+
crate::crypto::base64::base64_encode_into_slice(
|
|
582
|
+
slice::from_raw_parts(data, len),
|
|
583
|
+
slice::from_raw_parts_mut(out, out_cap),
|
|
584
|
+
url_safe != 0,
|
|
585
|
+
padding != 0,
|
|
586
|
+
)
|
|
587
|
+
.unwrap_or_default()
|
|
588
|
+
})
|
|
568
589
|
}
|
|
569
590
|
|
|
570
591
|
/// base64 decode into `out`; returns 0 on invalid input / too-small buffer.
|
|
@@ -580,18 +601,20 @@ pub unsafe extern "C" fn castrum_base64_decode(
|
|
|
580
601
|
url_safe: u8,
|
|
581
602
|
padding: u8,
|
|
582
603
|
) -> usize {
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
604
|
+
c_abi!(0, {
|
|
605
|
+
if data.is_null() || out.is_null() {
|
|
606
|
+
return 0;
|
|
607
|
+
}
|
|
608
|
+
// Zero-alloc core (`Engine::decode_slice`) — no intermediate Vec.
|
|
609
|
+
// Invalid input OR too-small buffer → 0 (napi parity).
|
|
610
|
+
crate::crypto::base64::base64_decode_into_slice(
|
|
611
|
+
slice::from_raw_parts(data, len),
|
|
612
|
+
slice::from_raw_parts_mut(out, out_cap),
|
|
613
|
+
url_safe != 0,
|
|
614
|
+
padding != 0,
|
|
615
|
+
)
|
|
616
|
+
.unwrap_or_default()
|
|
617
|
+
})
|
|
595
618
|
}
|
|
596
619
|
|
|
597
620
|
/// Random hex token: `byte_len` random bytes → `byte_len*2` hex chars, returned
|
|
@@ -601,21 +624,23 @@ pub unsafe extern "C" fn castrum_base64_decode(
|
|
|
601
624
|
/// `byte_len` must be ≤ 16 MiB (else `null`).
|
|
602
625
|
#[no_mangle]
|
|
603
626
|
pub unsafe extern "C" fn castrum_random_token(byte_len: u32) -> *const std::os::raw::c_char {
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
627
|
+
c_abi!(std::ptr::null(), {
|
|
628
|
+
const MAX: usize = 16 * 1024 * 1024;
|
|
629
|
+
let len = byte_len as usize;
|
|
630
|
+
let Some(out_len) = len.checked_mul(2) else {
|
|
631
|
+
return std::ptr::null();
|
|
632
|
+
};
|
|
633
|
+
if len > MAX {
|
|
634
|
+
return std::ptr::null();
|
|
635
|
+
}
|
|
636
|
+
let mut bytes = vec![0u8; len];
|
|
637
|
+
if getrandom::fill(&mut bytes).is_err() {
|
|
638
|
+
return std::ptr::null();
|
|
639
|
+
}
|
|
640
|
+
cstring_return(out_len, |out| {
|
|
641
|
+
crate::util::bytes::hex_encode(&bytes, out);
|
|
642
|
+
Some(out_len)
|
|
643
|
+
})
|
|
619
644
|
})
|
|
620
645
|
}
|
|
621
646
|
|
|
@@ -633,25 +658,27 @@ pub unsafe extern "C" fn castrum_random_token_into(
|
|
|
633
658
|
out: *mut u8,
|
|
634
659
|
out_cap: usize,
|
|
635
660
|
) -> usize {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
661
|
+
c_abi!(0, {
|
|
662
|
+
const MAX: usize = 16 * 1024 * 1024;
|
|
663
|
+
let len = byte_len as usize;
|
|
664
|
+
let Some(out_len) = len.checked_mul(2) else {
|
|
665
|
+
return 0;
|
|
666
|
+
};
|
|
667
|
+
if len > MAX || out.is_null() {
|
|
668
|
+
return 0;
|
|
669
|
+
}
|
|
670
|
+
if out_cap < out_len {
|
|
671
|
+
// Needed-size convention: report the exact required size.
|
|
672
|
+
return out_len;
|
|
673
|
+
}
|
|
674
|
+
let mut bytes = vec![0u8; len];
|
|
675
|
+
if getrandom::fill(&mut bytes).is_err() {
|
|
676
|
+
return 0;
|
|
677
|
+
}
|
|
678
|
+
let o = slice::from_raw_parts_mut(out, out_len);
|
|
679
|
+
crate::util::bytes::hex_encode(&bytes, o);
|
|
680
|
+
out_len
|
|
681
|
+
})
|
|
655
682
|
}
|
|
656
683
|
|
|
657
684
|
// ── Session envelope (fused JSON + HMAC) ────────────────────────────
|