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.
@@ -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
- 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())
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
- if token.is_null() || secret.is_null() {
52
- return 0;
53
- }
54
- with_hmac_key_cached(slice::from_raw_parts(secret, slen), |k| {
55
- u8::from(crate::crypto::csrf::csrf_verify_with_key(
56
- slice::from_raw_parts(token, tlen),
57
- k,
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
- if key.is_null() || data.is_null() || out.is_null() || out_cap < 64 {
132
- return 0;
133
- }
134
- let tag = with_hmac_key_cached(slice::from_raw_parts(key, klen), |k| {
135
- hmac::sign(k, slice::from_raw_parts(data, dlen))
136
- });
137
- let mut hex = [0u8; 64];
138
- crate::util::bytes::hex_encode_32(tag.as_ref(), &mut hex);
139
- slice::from_raw_parts_mut(out, 64).copy_from_slice(&hex);
140
- 64
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
- if value.is_null() || secret.is_null() {
156
- return std::ptr::null();
157
- }
158
- let v = slice::from_raw_parts(value, vlen);
159
- cstring_return(vlen + 65, |out| {
160
- with_hmac_key_cached(slice::from_raw_parts(secret, slen), |key| {
161
- crate::crypto::cookie_sign::sign_cookie_into(v, key, out)
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
- if value.is_null() || secret.is_null() || out.is_null() {
185
- return 0;
186
- }
187
- let needed = vlen + 65;
188
- if out_cap < needed {
189
- return needed;
190
- }
191
- let v = slice::from_raw_parts(value, vlen);
192
- with_hmac_key_cached(slice::from_raw_parts(secret, slen), |key| {
193
- crate::crypto::cookie_sign::sign_cookie_into(
194
- v,
195
- key,
196
- slice::from_raw_parts_mut(out, out_cap),
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
- if signed.is_null() || secret.is_null() {
216
- return std::ptr::null();
217
- }
218
- let s = slice::from_raw_parts(signed, slen);
219
- cstring_return(slen, |out| {
220
- with_hmac_key_cached(slice::from_raw_parts(secret, klen), |key| {
221
- crate::crypto::cookie_sign::verify_cookie_into(s, key, out)
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
- if signed.is_null() || secret.is_null() || out.is_null() {
246
- return 0;
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
- return slen;
260
- }
261
- with_hmac_key_cached(slice::from_raw_parts(secret, klen), |key| {
262
- crate::crypto::cookie_sign::verify_cookie_into(
263
- s,
264
- key,
265
- slice::from_raw_parts_mut(out, out_cap),
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
- if secret.is_null() {
282
- return std::ptr::null();
283
- }
284
- let mut rnd = [0u8; 32];
285
- if getrandom::fill(&mut rnd).is_err() {
286
- return std::ptr::null();
287
- }
288
- let mut rnd_hex = [0u8; 64];
289
- crate::util::bytes::hex_encode(&rnd, &mut rnd_hex);
290
- let mut sig_hex = [0u8; 64];
291
- let signed = with_hmac_key_cached(slice::from_raw_parts(secret, slen), |k| {
292
- hmac::sign(k, &rnd_hex)
293
- });
294
- crate::util::bytes::hex_encode(signed.as_ref(), &mut sig_hex);
295
- cstring_return(129, |out| {
296
- out[..64].copy_from_slice(&rnd_hex);
297
- out[64] = b'.';
298
- out[65..129].copy_from_slice(&sig_hex);
299
- Some(129)
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
- if secret.is_null() || out.is_null() {
318
- return 0;
319
- }
320
- if out_cap < 129 {
321
- return 129;
322
- }
323
- let mut rnd = [0u8; 32];
324
- if getrandom::fill(&mut rnd).is_err() {
325
- return 0;
326
- }
327
- let mut rnd_hex = [0u8; 64];
328
- crate::util::bytes::hex_encode(&rnd, &mut rnd_hex);
329
- let mut sig_hex = [0u8; 64];
330
- let signed = with_hmac_key_cached(slice::from_raw_parts(secret, slen), |k| {
331
- hmac::sign(k, &rnd_hex)
332
- });
333
- crate::util::bytes::hex_encode(signed.as_ref(), &mut sig_hex);
334
- let o = slice::from_raw_parts_mut(out, 129);
335
- o[..64].copy_from_slice(&rnd_hex);
336
- o[64] = b'.';
337
- o[65..129].copy_from_slice(&sig_hex);
338
- 129
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
- if password.is_null() || salt.is_null() || out.is_null() {
438
- return 0;
439
- }
440
- let dk_len = dk_len.clamp(
441
- crate::crypto::pbkdf2::PBKDF2_MIN_LEN,
442
- crate::crypto::pbkdf2::PBKDF2_MAX_LEN,
443
- ) as usize;
444
- if out_cap < dk_len {
445
- return 0;
446
- }
447
- pbkdf2_hmac::<Sha256>(
448
- slice::from_raw_parts(password, plen),
449
- slice::from_raw_parts(salt, slen),
450
- rounds.max(1),
451
- slice::from_raw_parts_mut(out, dk_len),
452
- );
453
- dk_len
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
- if data.is_null() || out.is_null() {
556
- return 0;
557
- }
558
- // Zero-alloc core (`Engine::encode_slice`) — no intermediate Vec. A
559
- // too-small buffer yields 0 (the JS wrappers size exactly, so a miss is a
560
- // caller bug / real error).
561
- crate::crypto::base64::base64_encode_into_slice(
562
- slice::from_raw_parts(data, len),
563
- slice::from_raw_parts_mut(out, out_cap),
564
- url_safe != 0,
565
- padding != 0,
566
- )
567
- .unwrap_or_default()
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
- if data.is_null() || out.is_null() {
584
- return 0;
585
- }
586
- // Zero-alloc core (`Engine::decode_slice`) — no intermediate Vec.
587
- // Invalid input OR too-small buffer → 0 (napi parity).
588
- crate::crypto::base64::base64_decode_into_slice(
589
- slice::from_raw_parts(data, len),
590
- slice::from_raw_parts_mut(out, out_cap),
591
- url_safe != 0,
592
- padding != 0,
593
- )
594
- .unwrap_or_default()
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
- const MAX: usize = 16 * 1024 * 1024;
605
- let len = byte_len as usize;
606
- let Some(out_len) = len.checked_mul(2) else {
607
- return std::ptr::null();
608
- };
609
- if len > MAX {
610
- return std::ptr::null();
611
- }
612
- let mut bytes = vec![0u8; len];
613
- if getrandom::fill(&mut bytes).is_err() {
614
- return std::ptr::null();
615
- }
616
- cstring_return(out_len, |out| {
617
- crate::util::bytes::hex_encode(&bytes, out);
618
- Some(out_len)
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
- const MAX: usize = 16 * 1024 * 1024;
637
- let len = byte_len as usize;
638
- let Some(out_len) = len.checked_mul(2) else {
639
- return 0;
640
- };
641
- if len > MAX || out.is_null() {
642
- return 0;
643
- }
644
- if out_cap < out_len {
645
- // Needed-size convention: report the exact required size.
646
- return out_len;
647
- }
648
- let mut bytes = vec![0u8; len];
649
- if getrandom::fill(&mut bytes).is_err() {
650
- return 0;
651
- }
652
- let o = slice::from_raw_parts_mut(out, out_len);
653
- crate::util::bytes::hex_encode(&bytes, o);
654
- out_len
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) ────────────────────────────