Skip to main content

native_ossl/
cipher.rs

1//! `CipherAlg` — `EVP_CIPHER` algorithm descriptor.
2//!
3//! Phase 3.2 delivers `CipherAlg`; Phase 4.2 extends this module with
4//! `CipherCtx<Dir>`, `AeadEncryptCtx`, and `AeadDecryptCtx`.
5
6use crate::error::ErrorStack;
7use native_ossl_sys as sys;
8use std::ffi::CStr;
9use std::sync::Arc;
10
11// ── CipherAlg — algorithm descriptor ─────────────────────────────────────────
12
13/// An OpenSSL symmetric cipher algorithm descriptor (`EVP_CIPHER*`).
14///
15/// Fetched once and reused.  Implements `Clone` via `EVP_CIPHER_up_ref`.
16pub struct CipherAlg {
17    ptr: *mut sys::EVP_CIPHER,
18    /// Keeps the library context alive while this descriptor is in use.
19    lib_ctx: Option<Arc<crate::lib_ctx::LibCtx>>,
20}
21
22impl CipherAlg {
23    /// Fetch a cipher algorithm from the global default library context.
24    ///
25    /// # Errors
26    ///
27    /// Returns `Err` if the algorithm is not available.
28    pub fn fetch(name: &CStr, props: Option<&CStr>) -> Result<Self, ErrorStack> {
29        let props_ptr = props.map_or(std::ptr::null(), CStr::as_ptr);
30        let ptr = unsafe { sys::EVP_CIPHER_fetch(std::ptr::null_mut(), name.as_ptr(), props_ptr) };
31        if ptr.is_null() {
32            return Err(ErrorStack::drain());
33        }
34        Ok(CipherAlg { ptr, lib_ctx: None })
35    }
36
37    /// Fetch a cipher algorithm from an explicit library context.
38    ///
39    /// # Errors
40    pub fn fetch_in(
41        ctx: &Arc<crate::lib_ctx::LibCtx>,
42        name: &CStr,
43        props: Option<&CStr>,
44    ) -> Result<Self, ErrorStack> {
45        let props_ptr = props.map_or(std::ptr::null(), CStr::as_ptr);
46        let ptr = unsafe { sys::EVP_CIPHER_fetch(ctx.as_ptr(), name.as_ptr(), props_ptr) };
47        if ptr.is_null() {
48            return Err(ErrorStack::drain());
49        }
50        Ok(CipherAlg {
51            ptr,
52            lib_ctx: Some(Arc::clone(ctx)),
53        })
54    }
55
56    /// Expected key length in bytes (e.g. 32 for AES-256).
57    #[must_use]
58    pub fn key_len(&self) -> usize {
59        usize::try_from(unsafe { sys::EVP_CIPHER_get_key_length(self.ptr) }).unwrap_or(0)
60    }
61
62    /// Expected IV length in bytes (0 for ECB mode).
63    #[must_use]
64    pub fn iv_len(&self) -> usize {
65        usize::try_from(unsafe { sys::EVP_CIPHER_get_iv_length(self.ptr) }).unwrap_or(0)
66    }
67
68    /// Block size in bytes (1 for stream ciphers; 16 for AES).
69    #[must_use]
70    pub fn block_size(&self) -> usize {
71        usize::try_from(unsafe { sys::EVP_CIPHER_get_block_size(self.ptr) }).unwrap_or(0)
72    }
73
74    /// OpenSSL flags for this cipher (e.g. `EVP_CIPH_FLAG_AEAD_CIPHER`).
75    #[must_use]
76    pub fn flags(&self) -> u64 {
77        unsafe { sys::EVP_CIPHER_get_flags(self.ptr) }
78    }
79
80    /// Return `true` if this is an AEAD cipher (GCM, CCM, ChaCha20-Poly1305).
81    #[must_use]
82    pub fn is_aead(&self) -> bool {
83        // EVP_CIPH_FLAG_AEAD_CIPHER = 0x0020_0000
84        (self.flags() & 0x0020_0000) != 0
85    }
86
87    /// Return the raw `EVP_CIPHER*` pointer.  Valid for the lifetime of `self`.
88    #[must_use]
89    pub fn as_ptr(&self) -> *const sys::EVP_CIPHER {
90        self.ptr
91    }
92}
93
94impl Clone for CipherAlg {
95    fn clone(&self) -> Self {
96        unsafe { sys::EVP_CIPHER_up_ref(self.ptr) };
97        CipherAlg {
98            ptr: self.ptr,
99            lib_ctx: self.lib_ctx.clone(),
100        }
101    }
102}
103
104impl Drop for CipherAlg {
105    fn drop(&mut self) {
106        unsafe { sys::EVP_CIPHER_free(self.ptr) };
107    }
108}
109
110// SAFETY: `EVP_CIPHER*` is reference-counted and immutable after fetch.
111unsafe impl Send for CipherAlg {}
112unsafe impl Sync for CipherAlg {}
113
114// ── Direction markers (Phase 4.2) ─────────────────────────────────────────────
115
116/// Marker type for encrypt direction.
117pub struct Encrypt;
118/// Marker type for decrypt direction.
119pub struct Decrypt;
120
121mod sealed {
122    pub trait Direction {}
123    impl Direction for super::Encrypt {}
124    impl Direction for super::Decrypt {}
125}
126
127// ── CipherCtx<Dir> — stateful context ────────────────────────────────────────
128
129/// Stateful symmetric cipher context.
130///
131/// `Dir` is either [`Encrypt`] or [`Decrypt`]; the type parameter prevents
132/// accidentally calling encrypt operations on a decrypt context and vice versa.
133///
134/// `!Clone` — cipher contexts have no `up_ref`; use a fresh context per message.
135pub struct CipherCtx<Dir> {
136    ptr: *mut sys::EVP_CIPHER_CTX,
137    _dir: std::marker::PhantomData<Dir>,
138}
139
140impl<Dir: sealed::Direction> CipherCtx<Dir> {
141    /// Feed `input` into the cipher; write plaintext/ciphertext to `output`.
142    ///
143    /// `output` must be at least `input.len() + block_size - 1` bytes.
144    /// Returns the number of bytes written.
145    ///
146    /// # Errors
147    pub fn update(&mut self, input: &[u8], output: &mut [u8]) -> Result<usize, ErrorStack>
148    where
149        Dir: IsEncrypt,
150    {
151        // SAFETY: self.ptr is a valid EVP_CIPHER_CTX initialised in CipherAlg::encrypt/decrypt.
152        unsafe { Dir::do_update(self.ptr, input, output) }
153    }
154
155    /// Feed `input` through the cipher and return the output as a `Vec<u8>`.
156    ///
157    /// Allocates `input.len() + block_size` bytes, calls [`Self::update`], then
158    /// truncates to the actual number of bytes written.  Use this when the caller
159    /// cannot easily compute the output size in advance.
160    ///
161    /// # Errors
162    pub fn update_to_vec(&mut self, input: &[u8]) -> Result<Vec<u8>, ErrorStack>
163    where
164        Dir: IsEncrypt,
165    {
166        let block_size =
167            usize::try_from(unsafe { sys::EVP_CIPHER_CTX_get_block_size(self.ptr) }).unwrap_or(0);
168        let max = input.len() + block_size;
169        let mut out = vec![0u8; max];
170        let n = self.update(input, &mut out)?;
171        out.truncate(n);
172        Ok(out)
173    }
174
175    /// Finalise the operation (flush padding / verify auth tag).
176    ///
177    /// # Errors
178    pub fn finalize(&mut self, output: &mut [u8]) -> Result<usize, ErrorStack>
179    where
180        Dir: IsEncrypt,
181    {
182        // SAFETY: same as update.
183        unsafe { Dir::do_finalize(self.ptr, output) }
184    }
185
186    /// Set dynamic parameters on the context (e.g. GCM tag length).
187    ///
188    /// # Errors
189    pub fn set_params(&mut self, params: &crate::params::Params<'_>) -> Result<(), ErrorStack> {
190        crate::ossl_call!(sys::EVP_CIPHER_CTX_set_params(self.ptr, params.as_ptr()))
191    }
192
193    /// Query arbitrary parameters from the context via a pre-built getter array.
194    ///
195    /// Build a `Params` array with placeholder values using [`ParamBuilder`], then
196    /// call this method to have OpenSSL fill in the real values.  Use the typed
197    /// getters on [`Params`] (e.g. `get_size_t`) to read the results.
198    ///
199    /// This is the low-level interface.  For common queries prefer the typed
200    /// helpers [`aead_tag_len`](Self::aead_tag_len), [`key_len`](Self::key_len),
201    /// and [`iv_len`](Self::iv_len).
202    ///
203    /// [`ParamBuilder`]: crate::params::ParamBuilder
204    /// [`Params`]: crate::params::Params
205    ///
206    /// # Errors
207    ///
208    /// Returns `Err` if `EVP_CIPHER_CTX_get_params` fails.
209    pub fn get_params(&self, params: &mut crate::params::Params<'_>) -> Result<(), ErrorStack> {
210        // SAFETY:
211        // - self.ptr is non-null (constructor invariant)
212        // - params.as_mut_ptr() is valid for the duration of this call
213        // - &self ensures no concurrent mutable access to self.ptr
214        crate::ossl_call!(sys::EVP_CIPHER_CTX_get_params(
215            self.ptr,
216            params.as_mut_ptr()
217        ))
218    }
219
220    /// Return the authentication tag length for this AEAD cipher context.
221    ///
222    /// Available after `EVP_EncryptInit_ex2` / `EVP_DecryptInit_ex2` with an AEAD
223    /// algorithm (AES-GCM, AES-CCM, ChaCha20-Poly1305, etc.).
224    ///
225    /// The getter pattern used here builds a `Params` array with a placeholder
226    /// value (`0`) for `OSSL_CIPHER_PARAM_AEAD_TAG_LEN` (`"taglen"`); OpenSSL
227    /// overwrites it with the actual tag length during `EVP_CIPHER_CTX_get_params`.
228    ///
229    /// # Errors
230    ///
231    /// Returns `Err` if the context is not initialised with an AEAD algorithm,
232    /// or if `EVP_CIPHER_CTX_get_params` fails.
233    pub fn aead_tag_len(&self) -> Result<usize, ErrorStack> {
234        let mut params = crate::params::ParamBuilder::new()?
235            .push_size(c"taglen", 0)?
236            .build()?;
237        // SAFETY:
238        // - self.ptr is non-null (constructor invariant)
239        // - params.as_mut_ptr() is valid for the duration of this call
240        // - &self ensures no concurrent mutable access to self.ptr
241        crate::ossl_call!(sys::EVP_CIPHER_CTX_get_params(
242            self.ptr,
243            params.as_mut_ptr()
244        ))?;
245        params
246            .get_size_t(c"taglen")
247            .map_err(|_| crate::error::ErrorStack::drain())
248    }
249
250    /// Return the key length in bytes for this cipher context.
251    ///
252    /// Reads `OSSL_CIPHER_PARAM_KEYLEN` (`"keylen"`) from the context.
253    /// Useful when the key length is variable (e.g. for some stream ciphers)
254    /// or to confirm the value that was set during initialisation.
255    ///
256    /// # Errors
257    ///
258    /// Returns `Err` if `EVP_CIPHER_CTX_get_params` fails.
259    pub fn key_len(&self) -> Result<usize, ErrorStack> {
260        let mut params = crate::params::ParamBuilder::new()?
261            .push_size(c"keylen", 0)?
262            .build()?;
263        // SAFETY: same as aead_tag_len.
264        crate::ossl_call!(sys::EVP_CIPHER_CTX_get_params(
265            self.ptr,
266            params.as_mut_ptr()
267        ))?;
268        params
269            .get_size_t(c"keylen")
270            .map_err(|_| crate::error::ErrorStack::drain())
271    }
272
273    /// Return the IV length in bytes for this cipher context.
274    ///
275    /// Reads `OSSL_CIPHER_PARAM_IVLEN` (`"ivlen"`) from the context.
276    /// Useful for allocating IV buffers when the algorithm is determined
277    /// at runtime.
278    ///
279    /// # Errors
280    ///
281    /// Returns `Err` if `EVP_CIPHER_CTX_get_params` fails.
282    pub fn iv_len(&self) -> Result<usize, ErrorStack> {
283        let mut params = crate::params::ParamBuilder::new()?
284            .push_size(c"ivlen", 0)?
285            .build()?;
286        // SAFETY: same as aead_tag_len.
287        crate::ossl_call!(sys::EVP_CIPHER_CTX_get_params(
288            self.ptr,
289            params.as_mut_ptr()
290        ))?;
291        params
292            .get_size_t(c"ivlen")
293            .map_err(|_| crate::error::ErrorStack::drain())
294    }
295
296    /// Return the raw `EVP_CIPHER_CTX*` pointer.  Returns a mutable pointer
297    /// because most OpenSSL EVP functions require `EVP_CIPHER_CTX*` even for
298    /// logically read-only operations.
299    #[must_use]
300    pub fn as_ptr(&self) -> *mut sys::EVP_CIPHER_CTX {
301        self.ptr
302    }
303}
304
305impl<Dir> Drop for CipherCtx<Dir> {
306    fn drop(&mut self) {
307        unsafe { sys::EVP_CIPHER_CTX_free(self.ptr) };
308    }
309}
310
311unsafe impl<Dir: sealed::Direction> Send for CipherCtx<Dir> {}
312
313/// Helper trait that routes to the correct `EVP_Encrypt*` or `EVP_Decrypt*` functions.
314///
315/// Sealed: only `Encrypt` and `Decrypt` implement this.
316pub trait IsEncrypt: sealed::Direction {
317    /// Feed data into the cipher and write output.
318    ///
319    /// # Errors
320    ///
321    /// Returns `Err` if the underlying EVP update call fails.
322    ///
323    /// # Safety
324    ///
325    /// `ctx` must be a valid, initialised `EVP_CIPHER_CTX*`.
326    unsafe fn do_update(
327        ctx: *mut sys::EVP_CIPHER_CTX,
328        input: &[u8],
329        output: &mut [u8],
330    ) -> Result<usize, ErrorStack>;
331
332    /// Flush final block and write output.
333    ///
334    /// # Errors
335    ///
336    /// Returns `Err` if finalisation fails (e.g. auth tag mismatch for AEAD).
337    ///
338    /// # Safety
339    ///
340    /// `ctx` must be a valid, initialised `EVP_CIPHER_CTX*`.
341    unsafe fn do_finalize(
342        ctx: *mut sys::EVP_CIPHER_CTX,
343        output: &mut [u8],
344    ) -> Result<usize, ErrorStack>;
345}
346
347impl IsEncrypt for Encrypt {
348    unsafe fn do_update(
349        ctx: *mut sys::EVP_CIPHER_CTX,
350        input: &[u8],
351        output: &mut [u8],
352    ) -> Result<usize, ErrorStack> {
353        let inl = i32::try_from(input.len()).map_err(|_| ErrorStack::drain())?;
354        let mut outl: i32 = 0;
355        crate::ossl_call!(sys::EVP_EncryptUpdate(
356            ctx,
357            output.as_mut_ptr(),
358            std::ptr::addr_of_mut!(outl),
359            input.as_ptr(),
360            inl
361        ))?;
362        Ok(usize::try_from(outl).unwrap_or(0))
363    }
364
365    unsafe fn do_finalize(
366        ctx: *mut sys::EVP_CIPHER_CTX,
367        output: &mut [u8],
368    ) -> Result<usize, ErrorStack> {
369        let mut outl: i32 = 0;
370        crate::ossl_call!(sys::EVP_EncryptFinal_ex(
371            ctx,
372            output.as_mut_ptr(),
373            std::ptr::addr_of_mut!(outl)
374        ))?;
375        Ok(usize::try_from(outl).unwrap_or(0))
376    }
377}
378
379impl IsEncrypt for Decrypt {
380    unsafe fn do_update(
381        ctx: *mut sys::EVP_CIPHER_CTX,
382        input: &[u8],
383        output: &mut [u8],
384    ) -> Result<usize, ErrorStack> {
385        let inl = i32::try_from(input.len()).map_err(|_| ErrorStack::drain())?;
386        let mut outl: i32 = 0;
387        crate::ossl_call!(sys::EVP_DecryptUpdate(
388            ctx,
389            output.as_mut_ptr(),
390            std::ptr::addr_of_mut!(outl),
391            input.as_ptr(),
392            inl
393        ))?;
394        Ok(usize::try_from(outl).unwrap_or(0))
395    }
396
397    unsafe fn do_finalize(
398        ctx: *mut sys::EVP_CIPHER_CTX,
399        output: &mut [u8],
400    ) -> Result<usize, ErrorStack> {
401        let mut outl: i32 = 0;
402        crate::ossl_call!(sys::EVP_DecryptFinal_ex(
403            ctx,
404            output.as_mut_ptr(),
405            std::ptr::addr_of_mut!(outl)
406        ))?;
407        Ok(usize::try_from(outl).unwrap_or(0))
408    }
409}
410
411impl CipherAlg {
412    /// Create an encryption context.
413    ///
414    /// **`key` and `iv` are copied** into OpenSSL's internal state by
415    /// `EVP_EncryptInit_ex2` (key scheduling + zeroization on free).
416    ///
417    /// # Errors
418    pub fn encrypt(
419        &self,
420        key: &[u8],
421        iv: &[u8],
422        params: Option<&crate::params::Params<'_>>,
423    ) -> Result<CipherCtx<Encrypt>, ErrorStack> {
424        let ctx_ptr = unsafe { sys::EVP_CIPHER_CTX_new() };
425        if ctx_ptr.is_null() {
426            return Err(ErrorStack::drain());
427        }
428        let params_ptr = params.map_or(crate::params::null_params(), crate::params::Params::as_ptr);
429        crate::ossl_call!(sys::EVP_EncryptInit_ex2(
430            ctx_ptr,
431            self.ptr,
432            key.as_ptr(),
433            iv.as_ptr(),
434            params_ptr
435        ))
436        .map_err(|e| {
437            unsafe { sys::EVP_CIPHER_CTX_free(ctx_ptr) };
438            e
439        })?;
440        Ok(CipherCtx {
441            ptr: ctx_ptr,
442            _dir: std::marker::PhantomData,
443        })
444    }
445
446    /// Create a decryption context.
447    ///
448    /// Same key/IV copy semantics as `encrypt`.
449    ///
450    /// # Errors
451    pub fn decrypt(
452        &self,
453        key: &[u8],
454        iv: &[u8],
455        params: Option<&crate::params::Params<'_>>,
456    ) -> Result<CipherCtx<Decrypt>, ErrorStack> {
457        let ctx_ptr = unsafe { sys::EVP_CIPHER_CTX_new() };
458        if ctx_ptr.is_null() {
459            return Err(ErrorStack::drain());
460        }
461        let params_ptr = params.map_or(crate::params::null_params(), crate::params::Params::as_ptr);
462        crate::ossl_call!(sys::EVP_DecryptInit_ex2(
463            ctx_ptr,
464            self.ptr,
465            key.as_ptr(),
466            iv.as_ptr(),
467            params_ptr
468        ))
469        .map_err(|e| {
470            unsafe { sys::EVP_CIPHER_CTX_free(ctx_ptr) };
471            e
472        })?;
473        Ok(CipherCtx {
474            ptr: ctx_ptr,
475            _dir: std::marker::PhantomData,
476        })
477    }
478}
479
480// ── AEAD types (Phase 4.2) ────────────────────────────────────────────────────
481
482/// AEAD encryption context (GCM, CCM, ChaCha20-Poly1305).
483pub struct AeadEncryptCtx(CipherCtx<Encrypt>);
484
485impl AeadEncryptCtx {
486    /// Create an AEAD encryption context.
487    ///
488    /// # Panics
489    ///
490    /// Panics if `alg` is not an AEAD cipher (`EVP_CIPH_FLAG_AEAD_CIPHER` not set).
491    ///
492    /// # Errors
493    pub fn new(
494        alg: &CipherAlg,
495        key: &[u8],
496        iv: &[u8],
497        params: Option<&crate::params::Params<'_>>,
498    ) -> Result<Self, ErrorStack> {
499        assert!(alg.is_aead(), "CipherAlg is not an AEAD algorithm");
500        Ok(AeadEncryptCtx(alg.encrypt(key, iv, params)?))
501    }
502
503    /// Set additional authenticated data (AAD).  Call before first `update`.
504    ///
505    /// # Panics
506    ///
507    /// Panics if `aad.len() > i32::MAX` (effectively impossible in practice).
508    ///
509    /// # Errors
510    pub fn set_aad(&mut self, aad: &[u8]) -> Result<(), ErrorStack> {
511        // AAD is fed via EVP_EncryptUpdate with output = NULL.
512        let alen = i32::try_from(aad.len()).expect("AAD too large for EVP_EncryptUpdate");
513        let mut outl: i32 = 0;
514        crate::ossl_call!(sys::EVP_EncryptUpdate(
515            self.0.ptr,
516            std::ptr::null_mut(),
517            std::ptr::addr_of_mut!(outl),
518            aad.as_ptr(),
519            alen
520        ))
521    }
522
523    /// Feed `input` into the AEAD cipher; write to `output`.
524    ///
525    /// # Errors
526    pub fn update(&mut self, input: &[u8], output: &mut [u8]) -> Result<usize, ErrorStack> {
527        self.0.update(input, output)
528    }
529
530    /// Flush any remaining bytes.  Must be called before `tag`.
531    ///
532    /// # Errors
533    pub fn finalize(&mut self, output: &mut [u8]) -> Result<usize, ErrorStack> {
534        self.0.finalize(output)
535    }
536
537    /// Retrieve the authentication tag after `finalize`.
538    ///
539    /// `tag` must be 16 bytes for GCM (`EVP_CTRL_GCM_GET_TAG = 16`).
540    ///
541    /// # Panics
542    ///
543    /// Panics if `tag.len() > i32::MAX`.
544    ///
545    /// # Errors
546    pub fn tag(&self, tag: &mut [u8]) -> Result<(), ErrorStack> {
547        // EVP_CTRL_GCM_GET_TAG = 16
548        let tlen = i32::try_from(tag.len()).expect("tag slice too large");
549        let rc = unsafe {
550            sys::EVP_CIPHER_CTX_ctrl(
551                self.0.ptr,
552                16, // EVP_CTRL_GCM_GET_TAG
553                tlen,
554                tag.as_mut_ptr().cast(),
555            )
556        };
557        if rc != 1 {
558            return Err(ErrorStack::drain());
559        }
560        Ok(())
561    }
562}
563
564/// AEAD decryption context.
565pub struct AeadDecryptCtx(CipherCtx<Decrypt>);
566
567impl AeadDecryptCtx {
568    /// Create an AEAD decryption context.
569    ///
570    /// # Panics
571    ///
572    /// Panics if `alg` is not an AEAD cipher.
573    ///
574    /// # Errors
575    pub fn new(
576        alg: &CipherAlg,
577        key: &[u8],
578        iv: &[u8],
579        params: Option<&crate::params::Params<'_>>,
580    ) -> Result<Self, ErrorStack> {
581        assert!(alg.is_aead(), "CipherAlg is not an AEAD algorithm");
582        Ok(AeadDecryptCtx(alg.decrypt(key, iv, params)?))
583    }
584
585    /// Set AAD before first `update`.
586    ///
587    /// # Panics
588    ///
589    /// Panics if `aad.len() > i32::MAX`.
590    ///
591    /// # Errors
592    pub fn set_aad(&mut self, aad: &[u8]) -> Result<(), ErrorStack> {
593        let alen = i32::try_from(aad.len()).expect("AAD too large for EVP_DecryptUpdate");
594        let mut outl: i32 = 0;
595        crate::ossl_call!(sys::EVP_DecryptUpdate(
596            self.0.ptr,
597            std::ptr::null_mut(),
598            std::ptr::addr_of_mut!(outl),
599            aad.as_ptr(),
600            alen
601        ))
602    }
603
604    /// Feed `input` into the cipher; write to `output`.
605    ///
606    /// # Errors
607    pub fn update(&mut self, input: &[u8], output: &mut [u8]) -> Result<usize, ErrorStack> {
608        self.0.update(input, output)
609    }
610
611    /// Set the expected authentication tag before `finalize`.
612    ///
613    /// # Panics
614    ///
615    /// Panics if `tag.len() > i32::MAX`.
616    ///
617    /// # Errors
618    pub fn set_tag(&mut self, tag: &[u8]) -> Result<(), ErrorStack> {
619        // EVP_CTRL_GCM_SET_TAG = 17
620        let tlen = i32::try_from(tag.len()).expect("tag slice too large");
621        let rc = unsafe {
622            sys::EVP_CIPHER_CTX_ctrl(
623                self.0.ptr,
624                17, // EVP_CTRL_GCM_SET_TAG
625                tlen,
626                // OpenSSL API is not const-correct here; cast away const.
627                tag.as_ptr().cast_mut().cast(),
628            )
629        };
630        if rc != 1 {
631            return Err(ErrorStack::drain());
632        }
633        Ok(())
634    }
635
636    /// Finalise — returns `Err` if authentication tag verification fails.
637    ///
638    /// # Errors
639    pub fn finalize(&mut self, output: &mut [u8]) -> Result<usize, ErrorStack> {
640        self.0.finalize(output)
641    }
642}
643
644// ── Tests ─────────────────────────────────────────────────────────────────────
645
646#[cfg(test)]
647mod tests {
648    use super::*;
649
650    #[test]
651    fn fetch_aes_256_gcm_properties() {
652        let alg = CipherAlg::fetch(c"AES-256-GCM", None).unwrap();
653        assert_eq!(alg.key_len(), 32);
654        assert_eq!(alg.iv_len(), 12);
655        assert_eq!(alg.block_size(), 1);
656        assert!(alg.is_aead());
657    }
658
659    #[test]
660    fn fetch_aes_256_cbc_properties() {
661        let alg = CipherAlg::fetch(c"AES-256-CBC", None).unwrap();
662        assert_eq!(alg.key_len(), 32);
663        assert_eq!(alg.iv_len(), 16);
664        assert_eq!(alg.block_size(), 16);
665        assert!(!alg.is_aead());
666    }
667
668    #[test]
669    fn fetch_nonexistent_fails() {
670        assert!(CipherAlg::fetch(c"NONEXISTENT_CIPHER_XYZ", None).is_err());
671    }
672
673    #[test]
674    fn clone_then_drop_both() {
675        let alg = CipherAlg::fetch(c"AES-256-GCM", None).unwrap();
676        let alg2 = alg.clone();
677        drop(alg);
678        drop(alg2);
679    }
680
681    /// AES-256-CBC round-trip encrypt + decrypt.
682    #[test]
683    fn aes_256_cbc_round_trip() {
684        let alg = CipherAlg::fetch(c"AES-256-CBC", None).unwrap();
685        let key = [0x42u8; 32];
686        let iv = [0x24u8; 16];
687        let plaintext = b"Hello, cipher world!";
688
689        // Encrypt.
690        let mut enc = alg.encrypt(&key, &iv, None).unwrap();
691        let mut ciphertext = vec![0u8; plaintext.len() + alg.block_size()];
692        let n = enc.update(plaintext, &mut ciphertext).unwrap();
693        let m = enc.finalize(&mut ciphertext[n..]).unwrap();
694        ciphertext.truncate(n + m);
695
696        // Decrypt.
697        let mut dec = alg.decrypt(&key, &iv, None).unwrap();
698        let mut recovered = vec![0u8; ciphertext.len() + alg.block_size()];
699        let n2 = dec.update(&ciphertext, &mut recovered).unwrap();
700        let m2 = dec.finalize(&mut recovered[n2..]).unwrap();
701        recovered.truncate(n2 + m2);
702
703        assert_eq!(recovered, plaintext);
704    }
705
706    /// AES-256-GCM AEAD: encrypt → tag → decrypt → verify; tag corruption → Err.
707    #[test]
708    fn aes_256_gcm_round_trip_and_tag_failure() {
709        let alg = CipherAlg::fetch(c"AES-256-GCM", None).unwrap();
710        let key = [0x11u8; 32];
711        let iv = [0x22u8; 12];
712        let aad = b"additional data";
713        let plaintext = b"secret message!";
714
715        // Encrypt.
716        let mut enc = AeadEncryptCtx::new(&alg, &key, &iv, None).unwrap();
717        enc.set_aad(aad).unwrap();
718        let mut ciphertext = vec![0u8; plaintext.len()];
719        let n = enc.update(plaintext, &mut ciphertext).unwrap();
720        enc.finalize(&mut ciphertext[n..]).unwrap();
721        let mut tag = [0u8; 16];
722        enc.tag(&mut tag).unwrap();
723
724        // Decrypt with correct tag — must succeed.
725        let mut dec = AeadDecryptCtx::new(&alg, &key, &iv, None).unwrap();
726        dec.set_aad(aad).unwrap();
727        let mut recovered = vec![0u8; ciphertext.len()];
728        let n2 = dec.update(&ciphertext, &mut recovered).unwrap();
729        dec.set_tag(&tag).unwrap();
730        dec.finalize(&mut recovered[n2..]).unwrap();
731        assert_eq!(&recovered[..n2], plaintext);
732
733        // Decrypt with corrupted tag — must fail.
734        let mut bad_tag = tag;
735        bad_tag[0] ^= 0xff;
736        let mut dec2 = AeadDecryptCtx::new(&alg, &key, &iv, None).unwrap();
737        dec2.set_aad(aad).unwrap();
738        let mut dummy = vec![0u8; ciphertext.len()];
739        dec2.update(&ciphertext, &mut dummy).unwrap();
740        dec2.set_tag(&bad_tag).unwrap();
741        assert!(dec2.finalize(&mut dummy).is_err());
742    }
743
744    /// AES-256-GCM: `aead_tag_len()` returns 16 (default GCM tag length).
745    #[test]
746    fn cipher_ctx_aead_tag_len_aes_gcm() {
747        let alg = CipherAlg::fetch(c"AES-256-GCM", None).unwrap();
748        let key = [0x11u8; 32];
749        let iv = [0x22u8; 12];
750        let ctx = alg.encrypt(&key, &iv, None).unwrap();
751        assert_eq!(ctx.aead_tag_len().unwrap(), 16);
752    }
753
754    /// AES-256-GCM: `key_len()` on a `CipherCtx` returns 32.
755    #[test]
756    fn cipher_ctx_key_len_aes256() {
757        let alg = CipherAlg::fetch(c"AES-256-GCM", None).unwrap();
758        let key = [0x11u8; 32];
759        let iv = [0x22u8; 12];
760        let ctx = alg.encrypt(&key, &iv, None).unwrap();
761        assert_eq!(ctx.key_len().unwrap(), 32);
762    }
763
764    /// AES-256-GCM: `iv_len()` on a `CipherCtx` returns 12.
765    #[test]
766    fn cipher_ctx_iv_len_aes256_gcm() {
767        let alg = CipherAlg::fetch(c"AES-256-GCM", None).unwrap();
768        let key = [0x11u8; 32];
769        let iv = [0x22u8; 12];
770        let ctx = alg.encrypt(&key, &iv, None).unwrap();
771        assert_eq!(ctx.iv_len().unwrap(), 12);
772    }
773
774    /// `get_params` with a manually-built getter array works correctly.
775    #[test]
776    fn cipher_ctx_get_params_generic() {
777        let alg = CipherAlg::fetch(c"AES-256-GCM", None).unwrap();
778        let key = [0x11u8; 32];
779        let iv = [0x22u8; 12];
780        let ctx = alg.encrypt(&key, &iv, None).unwrap();
781
782        let mut params = crate::params::ParamBuilder::new()
783            .unwrap()
784            .push_size(c"keylen", 0)
785            .unwrap()
786            .push_size(c"ivlen", 0)
787            .unwrap()
788            .build()
789            .unwrap();
790        ctx.get_params(&mut params).unwrap();
791        assert_eq!(params.get_size_t(c"keylen").unwrap(), 32);
792        assert_eq!(params.get_size_t(c"ivlen").unwrap(), 12);
793    }
794}