@@ -19,7 +19,7 @@ use crate::{
1919 secstatus_to_res,
2020} ;
2121
22- #[ cfg( not( feature = "disable-encryption" ) ) ]
22+ #[ cfg( all ( not( feature = "disable-encryption" ) , not ( feature = "blapi" ) ) ) ]
2323mod recprot {
2424 use std:: {
2525 fmt,
@@ -238,6 +238,328 @@ mod recprot {
238238 }
239239}
240240
241+ #[ cfg( all( not( feature = "disable-encryption" ) , feature = "blapi" ) ) ]
242+ mod recprot {
243+ use std:: {
244+ fmt,
245+ os:: raw:: { c_char, c_int, c_uint, c_ulong} ,
246+ ptr:: { null, null_mut} ,
247+ } ;
248+
249+ use crate :: {
250+ Cipher , Error , Res , SymKey , Version ,
251+ constants:: { TLS_AES_128_GCM_SHA256 , TLS_AES_256_GCM_SHA384 , TLS_CHACHA20_POLY1305_SHA256 } ,
252+ err:: { sec:: SEC_ERROR_BAD_DATA , secstatus_to_res} ,
253+ freebl:: { self , AesCtx , ChaCha20Ctx } ,
254+ hp:: SSL_HkdfExpandLabelWithMech ,
255+ p11:: { CK_MECHANISM_TYPE , CKM_HKDF_DATA , PK11SymKey } ,
256+ } ;
257+
258+ // Compile-time conversions of module constants to C types used in every
259+ // freebl call — avoids repeated infallible try_from at each call site.
260+ #[ expect( clippy:: cast_possible_truncation, reason = "NONCE_LEN = 12 and TAG_LEN = 16 both fit in u32" ) ]
261+ const NONCE_LEN_C : c_uint = super :: NONCE_LEN as c_uint ;
262+ #[ expect( clippy:: cast_possible_truncation, reason = "NONCE_LEN = 12 and TAG_LEN = 16 both fit in u32" ) ]
263+ const TAG_LEN_C : c_uint = super :: TAG_LEN as c_uint ;
264+ const NONCE_LEN_UL : c_ulong = super :: NONCE_LEN as c_ulong ;
265+ const TAG_BITS_UL : c_ulong = ( super :: TAG_LEN * 8 ) as c_ulong ;
266+ #[ expect(
267+ clippy:: cast_possible_truncation,
268+ reason = "CK_GCM_MESSAGE_PARAMS is a small fixed struct"
269+ ) ]
270+ const GCM_PARAMS_LEN_C : c_uint = size_of :: < freebl:: CK_GCM_MESSAGE_PARAMS > ( ) as c_uint ;
271+
272+ enum CipherSpec {
273+ Aes ( c_uint ) , // AES-GCM with this key length
274+ ChaCha ( c_uint ) , // ChaCha20-Poly1305 with this key length (always 32)
275+ }
276+
277+ impl CipherSpec {
278+ const fn key_len ( & self ) -> c_uint {
279+ match self {
280+ Self :: Aes ( n) | Self :: ChaCha ( n) => * n,
281+ }
282+ }
283+ }
284+
285+ const fn cipher_spec ( cipher : Cipher ) -> Res < CipherSpec > {
286+ match cipher {
287+ TLS_AES_128_GCM_SHA256 => Ok ( CipherSpec :: Aes ( 16 ) ) ,
288+ TLS_AES_256_GCM_SHA384 => Ok ( CipherSpec :: Aes ( 32 ) ) ,
289+ TLS_CHACHA20_POLY1305_SHA256 => Ok ( CipherSpec :: ChaCha ( 32 ) ) ,
290+ _ => Err ( Error :: UnsupportedCipher ) ,
291+ }
292+ }
293+
294+ fn expand_label (
295+ version : Version ,
296+ cipher : Cipher ,
297+ secret : & SymKey ,
298+ label : & str ,
299+ key_len : c_uint ,
300+ ) -> Res < SymKey > {
301+ let mut ptr: * mut PK11SymKey = null_mut ( ) ;
302+ unsafe {
303+ SSL_HkdfExpandLabelWithMech (
304+ version,
305+ cipher,
306+ * * secret,
307+ null ( ) ,
308+ 0 ,
309+ label. as_ptr ( ) . cast :: < c_char > ( ) ,
310+ c_uint:: try_from ( label. len ( ) ) ?,
311+ CK_MECHANISM_TYPE :: from ( CKM_HKDF_DATA ) ,
312+ key_len,
313+ & raw mut ptr,
314+ )
315+ } ?;
316+ SymKey :: from_ptr ( ptr)
317+ }
318+
319+ enum RecordCipher {
320+ Aes {
321+ ctx_encrypt : AesCtx ,
322+ ctx_decrypt : AesCtx ,
323+ } ,
324+ ChaCha ( ChaCha20Ctx ) ,
325+ }
326+
327+ /// Dispatch an AEAD operation to the appropriate freebl primitive.
328+ ///
329+ /// # Safety
330+ ///
331+ /// `output`, `tag`, and `input` must be valid for `output_max`, `TAG_LEN`,
332+ /// and `input_len` bytes respectively. `output` and `input` may overlap
333+ /// (in-place); `tag` must not overlap the `output` region.
334+ #[ expect( clippy:: too_many_arguments, reason = "Thin wrapper over two 10-argument C functions." ) ]
335+ unsafe fn aead_op (
336+ cipher : & RecordCipher ,
337+ encrypt : bool ,
338+ nonce : & mut [ u8 ; super :: NONCE_LEN ] ,
339+ aad : & [ u8 ] ,
340+ output : * mut u8 ,
341+ output_max : c_uint ,
342+ tag : * mut u8 ,
343+ input : * const u8 ,
344+ input_len : c_uint ,
345+ ) -> Res < usize > {
346+ let mut out_len: c_uint = 0 ;
347+ let aad_len = c_uint:: try_from ( aad. len ( ) ) ?;
348+ match cipher {
349+ RecordCipher :: Aes { ctx_encrypt, ctx_decrypt } => {
350+ let ctx = if encrypt { ctx_encrypt } else { ctx_decrypt } ;
351+ let mut params = freebl:: CK_GCM_MESSAGE_PARAMS {
352+ pIv : nonce. as_mut_ptr ( ) ,
353+ ulIvLen : NONCE_LEN_UL ,
354+ ulIvFixedBits : 0 ,
355+ ivGenerator : 0 ,
356+ pTag : tag,
357+ ulTagBits : TAG_BITS_UL ,
358+ } ;
359+ secstatus_to_res ( unsafe {
360+ freebl:: AES_AEAD (
361+ * * ctx,
362+ output,
363+ & raw mut out_len,
364+ output_max,
365+ input,
366+ input_len,
367+ std:: ptr:: from_mut ( & mut params) . cast ( ) ,
368+ GCM_PARAMS_LEN_C ,
369+ aad. as_ptr ( ) ,
370+ aad_len,
371+ )
372+ } ) ?;
373+ }
374+ RecordCipher :: ChaCha ( ctx) => {
375+ let f = if encrypt {
376+ freebl:: ChaCha20Poly1305_Encrypt
377+ } else {
378+ freebl:: ChaCha20Poly1305_Decrypt
379+ } ;
380+ secstatus_to_res ( unsafe {
381+ f (
382+ * * ctx, output, & raw mut out_len, output_max,
383+ input, input_len, nonce. as_ptr ( ) , NONCE_LEN_C ,
384+ aad. as_ptr ( ) , aad_len, tag,
385+ )
386+ } ) ?;
387+ }
388+ }
389+ Ok ( usize:: try_from ( out_len) ?)
390+ }
391+
392+ pub struct RecordProtection {
393+ cipher : RecordCipher ,
394+ nonce_base : [ u8 ; super :: NONCE_LEN ] ,
395+ }
396+
397+ impl RecordProtection {
398+ /// Create a new AEAD instance.
399+ ///
400+ /// # Errors
401+ ///
402+ /// Returns `Error` when the underlying crypto operations fail.
403+ pub fn new ( version : Version , cipher : Cipher , secret : & SymKey , prefix : & str ) -> Res < Self > {
404+ let spec = cipher_spec ( cipher) ?;
405+ // Closure captures version/cipher/secret; binds result so key_bytes borrow stays live.
406+ let derive = |suffix, len| {
407+ expand_label ( version, cipher, secret, & format ! ( "{prefix}{suffix}" ) , len)
408+ } ;
409+ let key_sym = derive ( "key" , spec. key_len ( ) ) ?;
410+ let key_bytes = key_sym. key_data ( ) ?;
411+
412+ let nonce_base: [ u8 ; super :: NONCE_LEN ] = derive ( "iv" , NONCE_LEN_C ) ?
413+ . key_data ( ) ?
414+ . try_into ( )
415+ . map_err ( |_| Error :: Internal ) ?;
416+
417+ let record_cipher = match spec {
418+ CipherSpec :: ChaCha ( key_len) => RecordCipher :: ChaCha ( ChaCha20Ctx :: from_ptr ( unsafe {
419+ freebl:: ChaCha20Poly1305_CreateContext ( key_bytes. as_ptr ( ) , key_len, TAG_LEN_C )
420+ } ) ?) ,
421+ CipherSpec :: Aes ( key_len) => {
422+ let make_aes_ctx = |encrypt : c_int | unsafe {
423+ freebl:: AES_CreateContext ( key_bytes. as_ptr ( ) , null ( ) , freebl:: NSS_AES_GCM , encrypt, key_len, 16 )
424+ } ;
425+ RecordCipher :: Aes {
426+ ctx_encrypt : AesCtx :: from_ptr ( make_aes_ctx ( 1 ) ) ?,
427+ ctx_decrypt : AesCtx :: from_ptr ( make_aes_ctx ( 0 ) ) ?,
428+ }
429+ }
430+ } ;
431+
432+ Ok ( Self { cipher : record_cipher, nonce_base } )
433+ }
434+
435+ /// Get the expansion size (authentication tag length) for this AEAD.
436+ #[ must_use]
437+ #[ expect( clippy:: unused_self) ]
438+ pub const fn expansion ( & self ) -> usize {
439+ super :: TAG_LEN
440+ }
441+
442+ /// Encrypt plaintext with associated data.
443+ ///
444+ /// # Errors
445+ ///
446+ /// Returns `Error` when encryption fails.
447+ pub fn encrypt < ' a > (
448+ & self ,
449+ count : u64 ,
450+ aad : & [ u8 ] ,
451+ input : & [ u8 ] ,
452+ output : & ' a mut [ u8 ] ,
453+ ) -> Res < & ' a [ u8 ] > {
454+ if output. len ( ) < input. len ( ) . checked_add ( super :: TAG_LEN ) . ok_or ( Error :: IntegerOverflow ) ? {
455+ return Err ( Error :: from ( SEC_ERROR_BAD_DATA ) ) ;
456+ }
457+ let mut nonce = super :: xor_nonce ( & self . nonce_base , count) ;
458+ let input_len = c_uint:: try_from ( input. len ( ) ) ?;
459+ let out_len = unsafe {
460+ aead_op (
461+ & self . cipher , true , & mut nonce, aad,
462+ output. as_mut_ptr ( ) , input_len,
463+ output[ input. len ( ) ..] . as_mut_ptr ( ) ,
464+ input. as_ptr ( ) , input_len,
465+ )
466+ } ?;
467+ debug_assert_eq ! ( out_len, input. len( ) ) ;
468+ Ok ( & output[ ..out_len + super :: TAG_LEN ] )
469+ }
470+
471+ /// Encrypt plaintext in place with associated data.
472+ ///
473+ /// # Errors
474+ ///
475+ /// Returns `Error` when encryption fails.
476+ pub fn encrypt_in_place ( & self , count : u64 , aad : & [ u8 ] , data : & mut [ u8 ] ) -> Res < usize > {
477+ if data. len ( ) < self . expansion ( ) {
478+ return Err ( Error :: from ( SEC_ERROR_BAD_DATA ) ) ;
479+ }
480+ let pt_len = data. len ( ) - self . expansion ( ) ;
481+ let mut nonce = super :: xor_nonce ( & self . nonce_base , count) ;
482+ let data_ptr = data. as_mut_ptr ( ) ;
483+ let pt_len_c = c_uint:: try_from ( pt_len) ?;
484+ let out_len = unsafe {
485+ aead_op (
486+ & self . cipher , true , & mut nonce, aad,
487+ data_ptr, pt_len_c,
488+ data_ptr. add ( pt_len) ,
489+ data_ptr. cast_const ( ) , pt_len_c,
490+ )
491+ } ?;
492+ debug_assert_eq ! ( out_len, pt_len) ;
493+ Ok ( data. len ( ) )
494+ }
495+
496+ /// Decrypt ciphertext with associated data.
497+ ///
498+ /// # Errors
499+ ///
500+ /// Returns `Error` when decryption or authentication fails.
501+ pub fn decrypt < ' a > (
502+ & self ,
503+ count : u64 ,
504+ aad : & [ u8 ] ,
505+ input : & [ u8 ] ,
506+ output : & ' a mut [ u8 ] ,
507+ ) -> Res < & ' a [ u8 ] > {
508+ let ct_len = input
509+ . len ( )
510+ . checked_sub ( super :: TAG_LEN )
511+ . ok_or_else ( || Error :: from ( SEC_ERROR_BAD_DATA ) ) ?;
512+ if output. len ( ) < ct_len {
513+ return Err ( Error :: from ( SEC_ERROR_BAD_DATA ) ) ;
514+ }
515+ let mut tag = [ 0u8 ; super :: TAG_LEN ] ;
516+ tag. copy_from_slice ( & input[ ct_len..ct_len + super :: TAG_LEN ] ) ;
517+ let mut nonce = super :: xor_nonce ( & self . nonce_base , count) ;
518+ let out_len = unsafe {
519+ aead_op (
520+ & self . cipher , false , & mut nonce, aad,
521+ output. as_mut_ptr ( ) , c_uint:: try_from ( output. len ( ) ) ?,
522+ tag. as_mut_ptr ( ) ,
523+ input. as_ptr ( ) , c_uint:: try_from ( ct_len) ?,
524+ )
525+ } ?;
526+ Ok ( & output[ ..out_len] )
527+ }
528+
529+ /// Decrypt ciphertext in place with associated data.
530+ ///
531+ /// # Errors
532+ ///
533+ /// Returns `Error` when decryption or authentication fails.
534+ pub fn decrypt_in_place ( & self , count : u64 , aad : & [ u8 ] , data : & mut [ u8 ] ) -> Res < usize > {
535+ let ct_len = data
536+ . len ( )
537+ . checked_sub ( super :: TAG_LEN )
538+ . ok_or_else ( || Error :: from ( SEC_ERROR_BAD_DATA ) ) ?;
539+ let mut tag = [ 0u8 ; super :: TAG_LEN ] ;
540+ tag. copy_from_slice ( & data[ ct_len..ct_len + super :: TAG_LEN ] ) ;
541+ let mut nonce = super :: xor_nonce ( & self . nonce_base , count) ;
542+ let data_ptr = data. as_mut_ptr ( ) ;
543+ let out_len = unsafe {
544+ aead_op (
545+ & self . cipher , false , & mut nonce, aad,
546+ data_ptr, c_uint:: try_from ( data. len ( ) ) ?,
547+ tag. as_mut_ptr ( ) ,
548+ data_ptr. cast_const ( ) , c_uint:: try_from ( ct_len) ?,
549+ )
550+ } ?;
551+ debug_assert_eq ! ( out_len, ct_len) ;
552+ Ok ( out_len)
553+ }
554+ }
555+
556+ impl fmt:: Debug for RecordProtection {
557+ fn fmt ( & self , f : & mut fmt:: Formatter ) -> fmt:: Result {
558+ write ! ( f, "[AEAD Context]" )
559+ }
560+ }
561+ }
562+
241563#[ cfg( feature = "disable-encryption" ) ]
242564mod recprot {
243565 use std:: fmt;
@@ -464,6 +786,17 @@ const TAG_LEN: usize = 16;
464786
465787pub type SequenceNumber = u64 ;
466788
789+ fn xor_nonce ( base : & [ u8 ; NONCE_LEN ] , count : SequenceNumber ) -> [ u8 ; NONCE_LEN ] {
790+ let mut nonce = * base;
791+ for ( n, & s) in nonce[ NONCE_LEN - COUNTER_LEN ..]
792+ . iter_mut ( )
793+ . zip ( & count. to_be_bytes ( ) )
794+ {
795+ * n ^= s;
796+ }
797+ nonce
798+ }
799+
467800/// All the lengths used by `PK11_AEADOp` are signed. This converts to that.
468801fn c_int_len < T > ( l : T ) -> Res < c_int >
469802where
@@ -513,12 +846,7 @@ impl Aead {
513846 }
514847
515848 fn make_nonce ( nonce : & mut [ u8 ; NONCE_LEN ] , seq : SequenceNumber ) {
516- for ( n, & s) in nonce[ NONCE_LEN - COUNTER_LEN ..]
517- . iter_mut ( )
518- . zip ( & seq. to_be_bytes ( ) )
519- {
520- * n ^= s;
521- }
849+ * nonce = xor_nonce ( nonce, seq) ;
522850 }
523851
524852 pub fn import_key ( algorithm : AeadAlgorithms , key : & [ u8 ] ) -> Result < SymKey , Error > {
0 commit comments