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,708 @@
|
|
|
1
|
+
// rust/ffi/crypto.rs — crypto C-ABI exports (hmac / cookies / csrf / passwords /
|
|
2
|
+
// aead / base64 / random token).
|
|
3
|
+
//
|
|
4
|
+
// Compiled keys go through `hmac_key_cached` (per-thread LRU — see `util.rs`);
|
|
5
|
+
// fallible / allocating cores route through `panic_guard`.
|
|
6
|
+
|
|
7
|
+
use aws_lc_rs::hmac;
|
|
8
|
+
use pbkdf2::pbkdf2_hmac;
|
|
9
|
+
use sha2::Sha256;
|
|
10
|
+
use std::slice;
|
|
11
|
+
|
|
12
|
+
use super::util::{aead_alg, cstring_return, hmac_key_cached, panic_guard};
|
|
13
|
+
|
|
14
|
+
/// HMAC-SHA256 verify → 1/0 (constant-time, hex sig compared after
|
|
15
|
+
/// whitespace-trim — mirrors the napi scalar path).
|
|
16
|
+
///
|
|
17
|
+
/// # Safety
|
|
18
|
+
/// `key`/`data`/`sig` must be valid for reads of their lengths.
|
|
19
|
+
#[no_mangle]
|
|
20
|
+
pub unsafe extern "C" fn castrum_hmac_sha256_verify(
|
|
21
|
+
key: *const u8,
|
|
22
|
+
klen: usize,
|
|
23
|
+
data: *const u8,
|
|
24
|
+
dlen: usize,
|
|
25
|
+
sig: *const u8,
|
|
26
|
+
slen: usize,
|
|
27
|
+
) -> u8 {
|
|
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
|
+
let k = hmac_key_cached(slice::from_raw_parts(key, klen));
|
|
36
|
+
u8::from(hmac::verify(&k, slice::from_raw_parts(data, dlen), &sig_bytes).is_ok())
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// CSRF constant-time verify → 1/0 (token format `<64-hex>.<64-hex>`).
|
|
40
|
+
///
|
|
41
|
+
/// # Safety
|
|
42
|
+
/// `token`/`secret` must be valid for reads of their lengths.
|
|
43
|
+
#[no_mangle]
|
|
44
|
+
pub unsafe extern "C" fn castrum_csrf_verify(
|
|
45
|
+
token: *const u8,
|
|
46
|
+
tlen: usize,
|
|
47
|
+
secret: *const u8,
|
|
48
|
+
slen: usize,
|
|
49
|
+
) -> u8 {
|
|
50
|
+
if token.is_null() || secret.is_null() {
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
let k = hmac_key_cached(slice::from_raw_parts(secret, slen));
|
|
54
|
+
u8::from(crate::crypto::csrf::csrf_verify_with_key(
|
|
55
|
+
slice::from_raw_parts(token, tlen),
|
|
56
|
+
&k,
|
|
57
|
+
))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/// Argon2id password verify → 1/0 (constant-time internally).
|
|
61
|
+
///
|
|
62
|
+
/// # Safety
|
|
63
|
+
/// `password`/`phc` must be valid for reads of their lengths.
|
|
64
|
+
#[no_mangle]
|
|
65
|
+
pub unsafe extern "C" fn castrum_password_verify(
|
|
66
|
+
password: *const u8,
|
|
67
|
+
plen: usize,
|
|
68
|
+
phc: *const u8,
|
|
69
|
+
phclen: usize,
|
|
70
|
+
) -> u8 {
|
|
71
|
+
if password.is_null() || phc.is_null() {
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
u8::from(crate::crypto::argon2::verify_password(
|
|
75
|
+
slice::from_raw_parts(password, plen),
|
|
76
|
+
slice::from_raw_parts(phc, phclen),
|
|
77
|
+
))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// bcrypt password verify → 1/0 (PHC `$2b$` string).
|
|
81
|
+
///
|
|
82
|
+
/// `hash` is a `bun:ffi` `cstring` ARG (the engine transcodes the JS PHC string
|
|
83
|
+
/// in-engine; the callee borrows via `CStr::from_ptr`). The password stays a
|
|
84
|
+
/// `(ptr,len)` byte slice — it may contain arbitrary bytes (including NUL).
|
|
85
|
+
///
|
|
86
|
+
/// # Safety
|
|
87
|
+
/// `password` must be valid for reads of `plen` bytes; `hash` must be a valid
|
|
88
|
+
/// NUL-terminated C string.
|
|
89
|
+
#[no_mangle]
|
|
90
|
+
pub unsafe extern "C" fn castrum_password_verify_bcrypt(
|
|
91
|
+
password: *const u8,
|
|
92
|
+
plen: usize,
|
|
93
|
+
hash: *const std::os::raw::c_char,
|
|
94
|
+
) -> u8 {
|
|
95
|
+
if password.is_null() || hash.is_null() {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
let Ok(h) = std::ffi::CStr::from_ptr(hash).to_str() else {
|
|
99
|
+
return 0;
|
|
100
|
+
};
|
|
101
|
+
u8::from(bcrypt::verify(slice::from_raw_parts(password, plen), h).unwrap_or(false))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// HMAC-SHA256 hex (64 chars) into `out`.
|
|
105
|
+
///
|
|
106
|
+
/// # Safety
|
|
107
|
+
/// `key`/`data` must be valid for reads of their lengths; `out` for `out_cap`.
|
|
108
|
+
#[no_mangle]
|
|
109
|
+
pub unsafe extern "C" fn castrum_hmac_sha256(
|
|
110
|
+
key: *const u8,
|
|
111
|
+
klen: usize,
|
|
112
|
+
data: *const u8,
|
|
113
|
+
dlen: usize,
|
|
114
|
+
out: *mut u8,
|
|
115
|
+
out_cap: usize,
|
|
116
|
+
) -> usize {
|
|
117
|
+
if key.is_null() || data.is_null() || out.is_null() || out_cap < 64 {
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
let k = hmac_key_cached(slice::from_raw_parts(key, klen));
|
|
121
|
+
let tag = hmac::sign(&k, slice::from_raw_parts(data, dlen));
|
|
122
|
+
let mut hex = [0u8; 64];
|
|
123
|
+
crate::util::bytes::hex_encode_32(tag.as_ref(), &mut hex);
|
|
124
|
+
slice::from_raw_parts_mut(out, 64).copy_from_slice(&hex);
|
|
125
|
+
64
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Sign a cookie `value` as `value.<64-hex sig>`, returned as a null-terminated
|
|
129
|
+
/// C string into the per-thread reused buffer (`cstring` return).
|
|
130
|
+
///
|
|
131
|
+
/// # Safety
|
|
132
|
+
/// `value`/`secret` must be valid for reads of their lengths.
|
|
133
|
+
#[no_mangle]
|
|
134
|
+
pub unsafe extern "C" fn castrum_sign_cookie(
|
|
135
|
+
value: *const u8,
|
|
136
|
+
vlen: usize,
|
|
137
|
+
secret: *const u8,
|
|
138
|
+
slen: usize,
|
|
139
|
+
) -> *const std::os::raw::c_char {
|
|
140
|
+
if value.is_null() || secret.is_null() {
|
|
141
|
+
return std::ptr::null();
|
|
142
|
+
}
|
|
143
|
+
let v = slice::from_raw_parts(value, vlen);
|
|
144
|
+
let key = hmac_key_cached(slice::from_raw_parts(secret, slen));
|
|
145
|
+
cstring_return(vlen + 65, |out| {
|
|
146
|
+
crate::crypto::cookie_sign::sign_cookie_into(v, &key, out)
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Sign a cookie `value` as `value.<64-hex sig>` written directly into a caller
|
|
151
|
+
/// buffer — the pooled sibling of `castrum_sign_cookie` (no cstring round-trip;
|
|
152
|
+
/// this is the path `signCookieInto` must use). Writes `vlen + 65` bytes;
|
|
153
|
+
/// returns bytes written, the exact required size when `out_cap` is too small,
|
|
154
|
+
/// or 0 on invalid input.
|
|
155
|
+
///
|
|
156
|
+
/// # Safety
|
|
157
|
+
/// `value`/`secret` must be valid for reads of their lengths; `out` for writes
|
|
158
|
+
/// up to `out_cap`.
|
|
159
|
+
#[no_mangle]
|
|
160
|
+
pub unsafe extern "C" fn castrum_sign_cookie_into(
|
|
161
|
+
value: *const u8,
|
|
162
|
+
vlen: usize,
|
|
163
|
+
secret: *const u8,
|
|
164
|
+
slen: usize,
|
|
165
|
+
out: *mut u8,
|
|
166
|
+
out_cap: usize,
|
|
167
|
+
) -> usize {
|
|
168
|
+
if value.is_null() || secret.is_null() || out.is_null() {
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
let needed = vlen + 65;
|
|
172
|
+
if out_cap < needed {
|
|
173
|
+
return needed;
|
|
174
|
+
}
|
|
175
|
+
let v = slice::from_raw_parts(value, vlen);
|
|
176
|
+
let key = hmac_key_cached(slice::from_raw_parts(secret, slen));
|
|
177
|
+
crate::crypto::cookie_sign::sign_cookie_into(v, &key, slice::from_raw_parts_mut(out, out_cap))
|
|
178
|
+
.unwrap_or_default()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Verify a signed cookie → the value without its signature, returned as a
|
|
182
|
+
/// null-terminated C string into the per-thread reused buffer. `null` on an
|
|
183
|
+
/// invalid signature / malformed input (the JS side maps to verify-fail).
|
|
184
|
+
///
|
|
185
|
+
/// # Safety
|
|
186
|
+
/// `signed`/`secret` must be valid for reads of their lengths.
|
|
187
|
+
#[no_mangle]
|
|
188
|
+
pub unsafe extern "C" fn castrum_verify_cookie(
|
|
189
|
+
signed: *const u8,
|
|
190
|
+
slen: usize,
|
|
191
|
+
secret: *const u8,
|
|
192
|
+
klen: usize,
|
|
193
|
+
) -> *const std::os::raw::c_char {
|
|
194
|
+
if signed.is_null() || secret.is_null() {
|
|
195
|
+
return std::ptr::null();
|
|
196
|
+
}
|
|
197
|
+
let s = slice::from_raw_parts(signed, slen);
|
|
198
|
+
let key = hmac_key_cached(slice::from_raw_parts(secret, klen));
|
|
199
|
+
cstring_return(slen, |out| {
|
|
200
|
+
crate::crypto::cookie_sign::verify_cookie_into(s, &key, out)
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// Verify a signed cookie → its value written directly into a caller buffer —
|
|
205
|
+
/// the pooled sibling of `castrum_verify_cookie` (no cstring round-trip; the
|
|
206
|
+
/// value length is unknown a priori, so the caller sizes to `slen` — the value
|
|
207
|
+
/// is always a prefix of `signed`). Returns the value length, the exact
|
|
208
|
+
/// required size when `out_cap` is too small, or 0 on invalid signature /
|
|
209
|
+
/// malformed input.
|
|
210
|
+
///
|
|
211
|
+
/// # Safety
|
|
212
|
+
/// `signed`/`secret` must be valid for reads of their lengths; `out` for writes
|
|
213
|
+
/// up to `out_cap`.
|
|
214
|
+
#[no_mangle]
|
|
215
|
+
pub unsafe extern "C" fn castrum_verify_cookie_into(
|
|
216
|
+
signed: *const u8,
|
|
217
|
+
slen: usize,
|
|
218
|
+
secret: *const u8,
|
|
219
|
+
klen: usize,
|
|
220
|
+
out: *mut u8,
|
|
221
|
+
out_cap: usize,
|
|
222
|
+
) -> usize {
|
|
223
|
+
if signed.is_null() || secret.is_null() || out.is_null() {
|
|
224
|
+
return 0;
|
|
225
|
+
}
|
|
226
|
+
let s = slice::from_raw_parts(signed, slen);
|
|
227
|
+
let key = hmac_key_cached(slice::from_raw_parts(secret, klen));
|
|
228
|
+
// The value is a prefix of `signed` (everything before the last `.`), so
|
|
229
|
+
// `slen` is always a sufficient bound — needed-size only differs when the
|
|
230
|
+
// caller passed less.
|
|
231
|
+
if out_cap < slen {
|
|
232
|
+
// Compute the actual needed length without writing (a full verify is
|
|
233
|
+
// wasted if we're just sizing; the caller retries at most once).
|
|
234
|
+
let dot = s.iter().rposition(|&b| b == b'.').unwrap_or(0);
|
|
235
|
+
if dot != 0 {
|
|
236
|
+
return dot;
|
|
237
|
+
}
|
|
238
|
+
return slen;
|
|
239
|
+
}
|
|
240
|
+
crate::crypto::cookie_sign::verify_cookie_into(s, &key, slice::from_raw_parts_mut(out, out_cap))
|
|
241
|
+
.unwrap_or_default()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// CSRF token (`<64-hex(rnd)>.<64-hex(sig)>`, 129 bytes) returned as a
|
|
245
|
+
/// null-terminated C string into the per-thread reused buffer.
|
|
246
|
+
///
|
|
247
|
+
/// # Safety
|
|
248
|
+
/// `secret` must be valid for reads of `slen` bytes.
|
|
249
|
+
#[no_mangle]
|
|
250
|
+
pub unsafe extern "C" fn castrum_csrf_token(
|
|
251
|
+
secret: *const u8,
|
|
252
|
+
slen: usize,
|
|
253
|
+
) -> *const std::os::raw::c_char {
|
|
254
|
+
if secret.is_null() {
|
|
255
|
+
return std::ptr::null();
|
|
256
|
+
}
|
|
257
|
+
let k = hmac_key_cached(slice::from_raw_parts(secret, slen));
|
|
258
|
+
let mut rnd = [0u8; 32];
|
|
259
|
+
if getrandom::fill(&mut rnd).is_err() {
|
|
260
|
+
return std::ptr::null();
|
|
261
|
+
}
|
|
262
|
+
let mut rnd_hex = [0u8; 64];
|
|
263
|
+
crate::util::bytes::hex_encode(&rnd, &mut rnd_hex);
|
|
264
|
+
let mut sig_hex = [0u8; 64];
|
|
265
|
+
crate::util::bytes::hex_encode(hmac::sign(&k, &rnd_hex).as_ref(), &mut sig_hex);
|
|
266
|
+
cstring_return(129, |out| {
|
|
267
|
+
out[..64].copy_from_slice(&rnd_hex);
|
|
268
|
+
out[64] = b'.';
|
|
269
|
+
out[65..129].copy_from_slice(&sig_hex);
|
|
270
|
+
Some(129)
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// CSRF token (`<64-hex(rnd)>.<64-hex(sig)>`, 129 bytes) written directly into
|
|
275
|
+
/// a caller buffer — the pooled sibling of `castrum_csrf_token` (no cstring
|
|
276
|
+
/// round-trip). Returns 129, the exact required size when `out_cap` is too
|
|
277
|
+
/// small, or 0 on RNG failure.
|
|
278
|
+
///
|
|
279
|
+
/// # Safety
|
|
280
|
+
/// `secret` must be valid for reads of `slen` bytes; `out` for writes up to `out_cap`.
|
|
281
|
+
#[no_mangle]
|
|
282
|
+
pub unsafe extern "C" fn castrum_csrf_token_into(
|
|
283
|
+
secret: *const u8,
|
|
284
|
+
slen: usize,
|
|
285
|
+
out: *mut u8,
|
|
286
|
+
out_cap: usize,
|
|
287
|
+
) -> usize {
|
|
288
|
+
if secret.is_null() || out.is_null() {
|
|
289
|
+
return 0;
|
|
290
|
+
}
|
|
291
|
+
if out_cap < 129 {
|
|
292
|
+
return 129;
|
|
293
|
+
}
|
|
294
|
+
let k = hmac_key_cached(slice::from_raw_parts(secret, slen));
|
|
295
|
+
let mut rnd = [0u8; 32];
|
|
296
|
+
if getrandom::fill(&mut rnd).is_err() {
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
let mut rnd_hex = [0u8; 64];
|
|
300
|
+
crate::util::bytes::hex_encode(&rnd, &mut rnd_hex);
|
|
301
|
+
let mut sig_hex = [0u8; 64];
|
|
302
|
+
crate::util::bytes::hex_encode(hmac::sign(&k, &rnd_hex).as_ref(), &mut sig_hex);
|
|
303
|
+
let o = slice::from_raw_parts_mut(out, 129);
|
|
304
|
+
o[..64].copy_from_slice(&rnd_hex);
|
|
305
|
+
o[64] = b'.';
|
|
306
|
+
o[65..129].copy_from_slice(&sig_hex);
|
|
307
|
+
129
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// Argon2id password hash → PHC string bytes into `out` (m/t/p/out_len params).
|
|
311
|
+
///
|
|
312
|
+
/// # Safety
|
|
313
|
+
/// `password`/`salt` must be valid for reads of their lengths; `out` for `out_cap`.
|
|
314
|
+
#[no_mangle]
|
|
315
|
+
pub unsafe extern "C" fn castrum_password_hash(
|
|
316
|
+
password: *const u8,
|
|
317
|
+
plen: usize,
|
|
318
|
+
salt: *const u8,
|
|
319
|
+
slen: usize,
|
|
320
|
+
m_cost: u32,
|
|
321
|
+
t_cost: u32,
|
|
322
|
+
p_cost: u32,
|
|
323
|
+
out_len: u32,
|
|
324
|
+
out: *mut u8,
|
|
325
|
+
out_cap: usize,
|
|
326
|
+
) -> usize {
|
|
327
|
+
if password.is_null() || salt.is_null() || out.is_null() {
|
|
328
|
+
return 0;
|
|
329
|
+
}
|
|
330
|
+
let Some(phc) = panic_guard(
|
|
331
|
+
|| {
|
|
332
|
+
crate::crypto::argon2::hash_password(
|
|
333
|
+
slice::from_raw_parts(password, plen),
|
|
334
|
+
slice::from_raw_parts(salt, slen),
|
|
335
|
+
m_cost,
|
|
336
|
+
t_cost,
|
|
337
|
+
p_cost,
|
|
338
|
+
out_len,
|
|
339
|
+
)
|
|
340
|
+
.ok()
|
|
341
|
+
},
|
|
342
|
+
None,
|
|
343
|
+
) else {
|
|
344
|
+
return 0;
|
|
345
|
+
};
|
|
346
|
+
let bytes = phc.as_bytes();
|
|
347
|
+
if bytes.len() > out_cap {
|
|
348
|
+
// Needed-size convention: report the exact PHC length so the JS side
|
|
349
|
+
// can pre-size (or retry once) instead of re-running the hash.
|
|
350
|
+
return bytes.len();
|
|
351
|
+
}
|
|
352
|
+
slice::from_raw_parts_mut(out, bytes.len()).copy_from_slice(bytes);
|
|
353
|
+
bytes.len()
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/// bcrypt password hash → `$2b$` PHC string into `out` (cost clamped 4..=31).
|
|
357
|
+
///
|
|
358
|
+
/// # Safety
|
|
359
|
+
/// `password` must be valid for reads of `plen` bytes; `out` for `out_cap`.
|
|
360
|
+
#[no_mangle]
|
|
361
|
+
pub unsafe extern "C" fn castrum_password_hash_bcrypt(
|
|
362
|
+
password: *const u8,
|
|
363
|
+
plen: usize,
|
|
364
|
+
cost: u32,
|
|
365
|
+
out: *mut u8,
|
|
366
|
+
out_cap: usize,
|
|
367
|
+
) -> usize {
|
|
368
|
+
if password.is_null() || out.is_null() {
|
|
369
|
+
return 0;
|
|
370
|
+
}
|
|
371
|
+
let Some(h) = panic_guard(
|
|
372
|
+
|| {
|
|
373
|
+
bcrypt::hash(
|
|
374
|
+
slice::from_raw_parts(password, plen),
|
|
375
|
+
crate::crypto::bcrypt::clamp_cost(cost),
|
|
376
|
+
)
|
|
377
|
+
.ok()
|
|
378
|
+
},
|
|
379
|
+
None,
|
|
380
|
+
) else {
|
|
381
|
+
return 0;
|
|
382
|
+
};
|
|
383
|
+
let bytes = h.as_bytes();
|
|
384
|
+
if bytes.len() > out_cap {
|
|
385
|
+
return 0;
|
|
386
|
+
}
|
|
387
|
+
slice::from_raw_parts_mut(out, bytes.len()).copy_from_slice(bytes);
|
|
388
|
+
bytes.len()
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/// PBKDF2-HMAC-SHA256 → `dk_len` bytes into `out` (dk_len clamped 1..=1MiB).
|
|
392
|
+
///
|
|
393
|
+
/// # Safety
|
|
394
|
+
/// `password`/`salt` must be valid for reads of their lengths; `out` for `out_cap`.
|
|
395
|
+
#[no_mangle]
|
|
396
|
+
pub unsafe extern "C" fn castrum_pbkdf2_sha256(
|
|
397
|
+
password: *const u8,
|
|
398
|
+
plen: usize,
|
|
399
|
+
salt: *const u8,
|
|
400
|
+
slen: usize,
|
|
401
|
+
rounds: u32,
|
|
402
|
+
dk_len: u32,
|
|
403
|
+
out: *mut u8,
|
|
404
|
+
out_cap: usize,
|
|
405
|
+
) -> usize {
|
|
406
|
+
if password.is_null() || salt.is_null() || out.is_null() {
|
|
407
|
+
return 0;
|
|
408
|
+
}
|
|
409
|
+
let dk_len = dk_len.clamp(
|
|
410
|
+
crate::crypto::pbkdf2::PBKDF2_MIN_LEN,
|
|
411
|
+
crate::crypto::pbkdf2::PBKDF2_MAX_LEN,
|
|
412
|
+
) as usize;
|
|
413
|
+
if out_cap < dk_len {
|
|
414
|
+
return 0;
|
|
415
|
+
}
|
|
416
|
+
pbkdf2_hmac::<Sha256>(
|
|
417
|
+
slice::from_raw_parts(password, plen),
|
|
418
|
+
slice::from_raw_parts(salt, slen),
|
|
419
|
+
rounds.max(1),
|
|
420
|
+
slice::from_raw_parts_mut(out, dk_len),
|
|
421
|
+
);
|
|
422
|
+
dk_len
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/// AEAD encrypt → ciphertext+tag into `out`; `alg` 0 = AES-256-GCM, 1 = ChaCha20.
|
|
426
|
+
///
|
|
427
|
+
/// # Safety
|
|
428
|
+
/// `key`/`nonce`/`plaintext` must be valid for reads of their lengths; `out` for `out_cap`.
|
|
429
|
+
#[no_mangle]
|
|
430
|
+
pub unsafe extern "C" fn castrum_aead_encrypt(
|
|
431
|
+
key: *const u8,
|
|
432
|
+
klen: usize,
|
|
433
|
+
nonce: *const u8,
|
|
434
|
+
nlen: usize,
|
|
435
|
+
plaintext: *const u8,
|
|
436
|
+
plen: usize,
|
|
437
|
+
alg: u8,
|
|
438
|
+
out: *mut u8,
|
|
439
|
+
out_cap: usize,
|
|
440
|
+
) -> usize {
|
|
441
|
+
let Some(alg) = aead_alg(alg) else {
|
|
442
|
+
return 0;
|
|
443
|
+
};
|
|
444
|
+
if key.is_null() || nonce.is_null() || plaintext.is_null() || out.is_null() {
|
|
445
|
+
return 0;
|
|
446
|
+
}
|
|
447
|
+
let Some(ct) = panic_guard(
|
|
448
|
+
|| {
|
|
449
|
+
crate::crypto::aead::encrypt(
|
|
450
|
+
alg,
|
|
451
|
+
slice::from_raw_parts(key, klen),
|
|
452
|
+
slice::from_raw_parts(nonce, nlen),
|
|
453
|
+
slice::from_raw_parts(plaintext, plen),
|
|
454
|
+
)
|
|
455
|
+
.ok()
|
|
456
|
+
},
|
|
457
|
+
None,
|
|
458
|
+
) else {
|
|
459
|
+
return 0;
|
|
460
|
+
};
|
|
461
|
+
if ct.len() > out_cap {
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
|
464
|
+
slice::from_raw_parts_mut(out, ct.len()).copy_from_slice(&ct);
|
|
465
|
+
ct.len()
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/// AEAD decrypt → plaintext into `out`; returns 0 on auth failure / malformed.
|
|
469
|
+
///
|
|
470
|
+
/// # Safety
|
|
471
|
+
/// `key`/`nonce`/`ciphertext` must be valid for reads of their lengths; `out` for `out_cap`.
|
|
472
|
+
#[no_mangle]
|
|
473
|
+
pub unsafe extern "C" fn castrum_aead_decrypt(
|
|
474
|
+
key: *const u8,
|
|
475
|
+
klen: usize,
|
|
476
|
+
nonce: *const u8,
|
|
477
|
+
nlen: usize,
|
|
478
|
+
ciphertext: *const u8,
|
|
479
|
+
clen: usize,
|
|
480
|
+
alg: u8,
|
|
481
|
+
out: *mut u8,
|
|
482
|
+
out_cap: usize,
|
|
483
|
+
) -> usize {
|
|
484
|
+
let Some(alg) = aead_alg(alg) else {
|
|
485
|
+
return 0;
|
|
486
|
+
};
|
|
487
|
+
if key.is_null() || nonce.is_null() || ciphertext.is_null() || out.is_null() {
|
|
488
|
+
return 0;
|
|
489
|
+
}
|
|
490
|
+
let Some(pt) = panic_guard(
|
|
491
|
+
|| {
|
|
492
|
+
crate::crypto::aead::decrypt(
|
|
493
|
+
alg,
|
|
494
|
+
slice::from_raw_parts(key, klen),
|
|
495
|
+
slice::from_raw_parts(nonce, nlen),
|
|
496
|
+
slice::from_raw_parts(ciphertext, clen),
|
|
497
|
+
)
|
|
498
|
+
.ok()
|
|
499
|
+
},
|
|
500
|
+
None,
|
|
501
|
+
) else {
|
|
502
|
+
return 0;
|
|
503
|
+
};
|
|
504
|
+
if pt.len() > out_cap {
|
|
505
|
+
return 0;
|
|
506
|
+
}
|
|
507
|
+
slice::from_raw_parts_mut(out, pt.len()).copy_from_slice(&pt);
|
|
508
|
+
pt.len()
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/// base64 encode into `out` with `url_safe`/`padding` u8 flags.
|
|
512
|
+
///
|
|
513
|
+
/// # Safety
|
|
514
|
+
/// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
|
|
515
|
+
#[no_mangle]
|
|
516
|
+
pub unsafe extern "C" fn castrum_base64_encode(
|
|
517
|
+
data: *const u8,
|
|
518
|
+
len: usize,
|
|
519
|
+
out: *mut u8,
|
|
520
|
+
out_cap: usize,
|
|
521
|
+
url_safe: u8,
|
|
522
|
+
padding: u8,
|
|
523
|
+
) -> usize {
|
|
524
|
+
if data.is_null() || out.is_null() {
|
|
525
|
+
return 0;
|
|
526
|
+
}
|
|
527
|
+
// Zero-alloc core (`Engine::encode_slice`) — no intermediate Vec. A
|
|
528
|
+
// too-small buffer yields 0 (the JS wrappers size exactly, so a miss is a
|
|
529
|
+
// caller bug / real error).
|
|
530
|
+
crate::crypto::base64::base64_encode_into_slice(
|
|
531
|
+
slice::from_raw_parts(data, len),
|
|
532
|
+
slice::from_raw_parts_mut(out, out_cap),
|
|
533
|
+
url_safe != 0,
|
|
534
|
+
padding != 0,
|
|
535
|
+
)
|
|
536
|
+
.unwrap_or_default()
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/// base64 decode into `out`; returns 0 on invalid input / too-small buffer.
|
|
540
|
+
///
|
|
541
|
+
/// # Safety
|
|
542
|
+
/// `data` must be valid for reads of `len` bytes; `out` for writes up to `out_cap`.
|
|
543
|
+
#[no_mangle]
|
|
544
|
+
pub unsafe extern "C" fn castrum_base64_decode(
|
|
545
|
+
data: *const u8,
|
|
546
|
+
len: usize,
|
|
547
|
+
out: *mut u8,
|
|
548
|
+
out_cap: usize,
|
|
549
|
+
url_safe: u8,
|
|
550
|
+
padding: u8,
|
|
551
|
+
) -> usize {
|
|
552
|
+
if data.is_null() || out.is_null() {
|
|
553
|
+
return 0;
|
|
554
|
+
}
|
|
555
|
+
// Zero-alloc core (`Engine::decode_slice`) — no intermediate Vec.
|
|
556
|
+
// Invalid input OR too-small buffer → 0 (napi parity).
|
|
557
|
+
crate::crypto::base64::base64_decode_into_slice(
|
|
558
|
+
slice::from_raw_parts(data, len),
|
|
559
|
+
slice::from_raw_parts_mut(out, out_cap),
|
|
560
|
+
url_safe != 0,
|
|
561
|
+
padding != 0,
|
|
562
|
+
)
|
|
563
|
+
.unwrap_or_default()
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/// Random hex token: `byte_len` random bytes → `byte_len*2` hex chars, returned
|
|
567
|
+
/// as a null-terminated C string into the per-thread reused buffer.
|
|
568
|
+
///
|
|
569
|
+
/// # Safety
|
|
570
|
+
/// `byte_len` must be ≤ 16 MiB (else `null`).
|
|
571
|
+
#[no_mangle]
|
|
572
|
+
pub unsafe extern "C" fn castrum_random_token(byte_len: u32) -> *const std::os::raw::c_char {
|
|
573
|
+
const MAX: usize = 16 * 1024 * 1024;
|
|
574
|
+
let len = byte_len as usize;
|
|
575
|
+
let Some(out_len) = len.checked_mul(2) else {
|
|
576
|
+
return std::ptr::null();
|
|
577
|
+
};
|
|
578
|
+
if len > MAX {
|
|
579
|
+
return std::ptr::null();
|
|
580
|
+
}
|
|
581
|
+
let mut bytes = vec![0u8; len];
|
|
582
|
+
if getrandom::fill(&mut bytes).is_err() {
|
|
583
|
+
return std::ptr::null();
|
|
584
|
+
}
|
|
585
|
+
cstring_return(out_len, |out| {
|
|
586
|
+
crate::util::bytes::hex_encode(&bytes, out);
|
|
587
|
+
Some(out_len)
|
|
588
|
+
})
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/// Random hex token written directly into a caller buffer — the pooled sibling
|
|
592
|
+
/// of `castrum_random_token` (no cstring round-trip on the Bun side). Writes
|
|
593
|
+
/// `byte_len*2` hex chars for `byte_len` random bytes. Returns bytes written
|
|
594
|
+
/// (`byte_len*2`), the exact required size when `out_cap` is too small, or 0 on
|
|
595
|
+
/// a real error (over the 16 MiB cap or the random source failing).
|
|
596
|
+
///
|
|
597
|
+
/// # Safety
|
|
598
|
+
/// `out` must be valid for writes up to `out_cap` (and `out` non-null).
|
|
599
|
+
#[no_mangle]
|
|
600
|
+
pub unsafe extern "C" fn castrum_random_token_into(
|
|
601
|
+
byte_len: u32,
|
|
602
|
+
out: *mut u8,
|
|
603
|
+
out_cap: usize,
|
|
604
|
+
) -> usize {
|
|
605
|
+
const MAX: usize = 16 * 1024 * 1024;
|
|
606
|
+
let len = byte_len as usize;
|
|
607
|
+
let Some(out_len) = len.checked_mul(2) else {
|
|
608
|
+
return 0;
|
|
609
|
+
};
|
|
610
|
+
if len > MAX || out.is_null() {
|
|
611
|
+
return 0;
|
|
612
|
+
}
|
|
613
|
+
if out_cap < out_len {
|
|
614
|
+
// Needed-size convention: report the exact required size.
|
|
615
|
+
return out_len;
|
|
616
|
+
}
|
|
617
|
+
let mut bytes = vec![0u8; len];
|
|
618
|
+
if getrandom::fill(&mut bytes).is_err() {
|
|
619
|
+
return 0;
|
|
620
|
+
}
|
|
621
|
+
let o = slice::from_raw_parts_mut(out, out_len);
|
|
622
|
+
crate::util::bytes::hex_encode(&bytes, o);
|
|
623
|
+
out_len
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// ── Session envelope (fused JSON + HMAC) ────────────────────────────
|
|
627
|
+
|
|
628
|
+
/// Seal a session envelope NATIVELY: build
|
|
629
|
+
/// `{"id":"…","data":<data_json>,"exp":exp}` and HMAC-sign it into the
|
|
630
|
+
/// `payload.<64-hex>` cookie token — ONE crossing replaces the JS
|
|
631
|
+
/// `signCookie(JSON.stringify(envelope))` pair. `null` = empty id/secret /
|
|
632
|
+
/// panic. `data_json` is embedded verbatim (caller passes valid JSON).
|
|
633
|
+
///
|
|
634
|
+
/// # Safety
|
|
635
|
+
/// All four args must be valid NUL-terminated C strings.
|
|
636
|
+
#[no_mangle]
|
|
637
|
+
pub unsafe extern "C" fn castrum_session_seal(
|
|
638
|
+
id: *const std::os::raw::c_char,
|
|
639
|
+
data_json: *const std::os::raw::c_char,
|
|
640
|
+
exp_secs: i64,
|
|
641
|
+
secret: *const std::os::raw::c_char,
|
|
642
|
+
) -> *const std::os::raw::c_char {
|
|
643
|
+
if id.is_null() || data_json.is_null() || secret.is_null() {
|
|
644
|
+
return std::ptr::null();
|
|
645
|
+
}
|
|
646
|
+
let id_b = std::ffi::CStr::from_ptr(id).to_bytes();
|
|
647
|
+
let data_b = std::ffi::CStr::from_ptr(data_json).to_bytes();
|
|
648
|
+
let sec_b = std::ffi::CStr::from_ptr(secret).to_bytes();
|
|
649
|
+
let sealed = super::util::panic_guard(
|
|
650
|
+
|| crate::crypto::session::seal_core(id_b, data_b, exp_secs, sec_b),
|
|
651
|
+
None,
|
|
652
|
+
);
|
|
653
|
+
let Some(tok) = sealed else {
|
|
654
|
+
return std::ptr::null();
|
|
655
|
+
};
|
|
656
|
+
super::util::cstring_return(tok.len(), move |buf| {
|
|
657
|
+
if buf.len() < tok.len() {
|
|
658
|
+
return None;
|
|
659
|
+
}
|
|
660
|
+
buf[..tok.len()].copy_from_slice(&tok);
|
|
661
|
+
Some(tok.len())
|
|
662
|
+
})
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/// Open a sealed session token: verify the HMAC and extract the envelope in
|
|
666
|
+
/// ONE crossing. Packed output: `[u8 ok=1][i64 exp][u32 idLen][id][u32
|
|
667
|
+
/// dataLen][dataJson]`. Needed-size convention (`w > out_cap` = exact size);
|
|
668
|
+
/// `0` = bad signature / malformed / too-small-with-error semantics per the
|
|
669
|
+
/// caller contract below.
|
|
670
|
+
///
|
|
671
|
+
/// # Safety
|
|
672
|
+
/// `token`/`secret` valid NUL-terminated C strings; `out` for writes up to
|
|
673
|
+
/// `out_cap`.
|
|
674
|
+
#[no_mangle]
|
|
675
|
+
pub unsafe extern "C" fn castrum_session_open(
|
|
676
|
+
token: *const std::os::raw::c_char,
|
|
677
|
+
secret: *const std::os::raw::c_char,
|
|
678
|
+
out: *mut u8,
|
|
679
|
+
out_cap: usize,
|
|
680
|
+
) -> usize {
|
|
681
|
+
if token.is_null() || secret.is_null() || out.is_null() {
|
|
682
|
+
return 0;
|
|
683
|
+
}
|
|
684
|
+
let tok = std::ffi::CStr::from_ptr(token).to_bytes();
|
|
685
|
+
let sec = std::ffi::CStr::from_ptr(secret).to_bytes();
|
|
686
|
+
let opened = panic_guard(|| crate::crypto::session::open_core(tok, sec), None);
|
|
687
|
+
let Some((exp, id, data)) = opened else {
|
|
688
|
+
return 0;
|
|
689
|
+
};
|
|
690
|
+
// Layout size: 1 + 8 + 4+id + 4+data
|
|
691
|
+
let need = 1 + 8 + 4 + id.len() + 4 + data.len();
|
|
692
|
+
if need > out_cap {
|
|
693
|
+
return need;
|
|
694
|
+
}
|
|
695
|
+
let o = slice::from_raw_parts_mut(out, need);
|
|
696
|
+
o[0] = 1;
|
|
697
|
+
o[1..9].copy_from_slice(&exp.to_le_bytes());
|
|
698
|
+
let id_len = id.len() as u32;
|
|
699
|
+
o[9..13].copy_from_slice(&id_len.to_le_bytes());
|
|
700
|
+
let mut p = 13usize;
|
|
701
|
+
o[p..p + id.len()].copy_from_slice(&id);
|
|
702
|
+
p += id.len();
|
|
703
|
+
let d_len = data.len() as u32;
|
|
704
|
+
o[p..p + 4].copy_from_slice(&d_len.to_le_bytes());
|
|
705
|
+
p += 4;
|
|
706
|
+
o[p..p + data.len()].copy_from_slice(&data);
|
|
707
|
+
need
|
|
708
|
+
}
|