lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

wrapping.rs (8573B)


      1 //! Data-key wrapping contracts.
      2 
      3 use crate::SecretRef;
      4 use crate::context::EnvelopeContext;
      5 use crate::envelope::LegacyV1ResealAuthority;
      6 use crate::error::Error;
      7 use alloc::boxed::Box;
      8 use alloc::vec::Vec;
      9 use core::fmt;
     10 use core::future::Future;
     11 use core::pin::Pin;
     12 use zeroize::Zeroizing;
     13 
     14 /// Maximum plaintext accepted by the generic wrapping boundary.
     15 pub const SECRET_MATERIAL_MAX_BYTES: usize = 64 * 1024;
     16 /// Maximum protected value accepted by the generic wrapping boundary.
     17 pub const WRAPPED_SECRET_MAX_BYTES: usize = 128 * 1024;
     18 
     19 /// A sendable provider future that does not prescribe an executor.
     20 pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
     21 
     22 /// Opaque, single-owner plaintext material that zeroizes on drop.
     23 ///
     24 /// This type never implements `Clone` or `Serialize`, and its diagnostics are
     25 /// always redacted. Callers must opt in to the narrow [`Self::expose_secret`]
     26 /// scope when invoking cryptographic code.
     27 ///
     28 /// ```compile_fail
     29 /// use radroots_secrets::wrapping::SecretMaterial;
     30 ///
     31 /// let material = SecretMaterial::from_slice(b"secret")?;
     32 /// let _duplicate = material.clone();
     33 /// # Ok::<(), radroots_secrets::Error>(())
     34 /// ```
     35 ///
     36 /// ```compile_fail
     37 /// use radroots_secrets::wrapping::SecretMaterial;
     38 ///
     39 /// let material = SecretMaterial::from_slice(b"secret")?;
     40 /// let _json = serde_json::to_string(&material)?;
     41 /// # Ok::<(), Box<dyn std::error::Error>>(())
     42 /// ```
     43 pub struct SecretMaterial(Zeroizing<Box<[u8]>>);
     44 
     45 impl SecretMaterial {
     46     /// Copies caller-supplied material into a zeroizing owner.
     47     pub fn from_slice(bytes: &[u8]) -> Result<Self, Error> {
     48         if bytes.is_empty() || bytes.len() > SECRET_MATERIAL_MAX_BYTES {
     49             return Err(Error::InvalidSecretLength {
     50                 actual_bytes: bytes.len(),
     51                 max_bytes: SECRET_MATERIAL_MAX_BYTES,
     52             });
     53         }
     54         Ok(Self(Zeroizing::new(Box::from(bytes))))
     55     }
     56 
     57     pub(crate) fn from_owned(bytes: Vec<u8>) -> Result<Self, Error> {
     58         let bytes = Zeroizing::new(bytes);
     59         Self::from_slice(bytes.as_slice())
     60     }
     61 
     62     /// Exposes plaintext only for the lifetime of an explicit closure call.
     63     pub fn expose_secret<T>(&self, use_secret: impl FnOnce(&[u8]) -> T) -> T {
     64         use_secret(&self.0[..])
     65     }
     66 
     67     /// Returns the plaintext length without exposing its contents.
     68     #[must_use]
     69     pub fn len(&self) -> usize {
     70         self.0.len()
     71     }
     72 
     73     /// Returns whether the value is empty.
     74     #[must_use]
     75     pub fn is_empty(&self) -> bool {
     76         self.0.is_empty()
     77     }
     78 }
     79 
     80 impl fmt::Debug for SecretMaterial {
     81     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     82         formatter.write_str("SecretMaterial(<redacted>)")
     83     }
     84 }
     85 
     86 /// Opaque provider-wrapped material safe for persistence but not diagnostics.
     87 #[derive(Clone, PartialEq, Eq)]
     88 pub struct WrappedSecret(Vec<u8>);
     89 
     90 impl WrappedSecret {
     91     /// Validates and owns provider-wrapped material.
     92     pub fn from_bytes(bytes: impl Into<Vec<u8>>) -> Result<Self, Error> {
     93         let bytes = bytes.into();
     94         if bytes.is_empty() || bytes.len() > WRAPPED_SECRET_MAX_BYTES {
     95             return Err(Error::InvalidWrappedLength {
     96                 actual_bytes: bytes.len(),
     97                 max_bytes: WRAPPED_SECRET_MAX_BYTES,
     98             });
     99         }
    100         Ok(Self(bytes))
    101     }
    102 
    103     /// Returns the wrapped representation for envelope persistence.
    104     #[must_use]
    105     pub fn as_bytes(&self) -> &[u8] {
    106         self.0.as_slice()
    107     }
    108 }
    109 
    110 impl fmt::Debug for WrappedSecret {
    111     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    112         formatter.write_str("WrappedSecret(<redacted>)")
    113     }
    114 }
    115 
    116 /// Borrowed input for one key-wrapping operation.
    117 #[derive(Debug, Clone, Copy)]
    118 pub struct WrapRequest<'a> {
    119     reference: &'a SecretRef,
    120     context: &'a EnvelopeContext,
    121     plaintext: &'a SecretMaterial,
    122 }
    123 
    124 impl<'a> WrapRequest<'a> {
    125     /// Creates an explicit wrapping request.
    126     #[must_use]
    127     pub const fn new(
    128         reference: &'a SecretRef,
    129         context: &'a EnvelopeContext,
    130         plaintext: &'a SecretMaterial,
    131     ) -> Self {
    132         Self {
    133             reference,
    134             context,
    135             plaintext,
    136         }
    137     }
    138 
    139     /// Returns the provider capability reference.
    140     #[must_use]
    141     pub const fn reference(&self) -> &'a SecretRef {
    142         self.reference
    143     }
    144 
    145     /// Returns the independently validated semantic wrapping authority.
    146     #[must_use]
    147     pub const fn context(&self) -> &'a EnvelopeContext {
    148         self.context
    149     }
    150 
    151     /// Returns the single-owner plaintext wrapper.
    152     #[must_use]
    153     pub const fn plaintext(&self) -> &'a SecretMaterial {
    154         self.plaintext
    155     }
    156 }
    157 
    158 /// Borrowed input for one key-unwrapping operation.
    159 #[derive(Debug, Clone, Copy)]
    160 pub struct UnwrapRequest<'a> {
    161     reference: &'a SecretRef,
    162     context: &'a EnvelopeContext,
    163     wrapped: &'a WrappedSecret,
    164 }
    165 
    166 /// Capability-gated input for migration-only v1 key unwrapping.
    167 pub struct LegacyV1UnwrapRequest<'a> {
    168     reference: &'a SecretRef,
    169     wrapped: &'a WrappedSecret,
    170     _authority: &'a LegacyV1ResealAuthority,
    171 }
    172 
    173 impl<'a> LegacyV1UnwrapRequest<'a> {
    174     pub(crate) const fn new(
    175         reference: &'a SecretRef,
    176         wrapped: &'a WrappedSecret,
    177         authority: &'a LegacyV1ResealAuthority,
    178     ) -> Self {
    179         Self {
    180             reference,
    181             wrapped,
    182             _authority: authority,
    183         }
    184     }
    185 
    186     /// Returns the exact legacy provider capability reference.
    187     #[must_use]
    188     pub const fn reference(&self) -> &'a SecretRef {
    189         self.reference
    190     }
    191 
    192     /// Returns the exact provider-wrapped v1 value.
    193     #[must_use]
    194     pub const fn wrapped(&self) -> &'a WrappedSecret {
    195         self.wrapped
    196     }
    197 }
    198 
    199 impl fmt::Debug for LegacyV1UnwrapRequest<'_> {
    200     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
    201         formatter.write_str("LegacyV1UnwrapRequest(<redacted>)")
    202     }
    203 }
    204 
    205 impl<'a> UnwrapRequest<'a> {
    206     /// Creates an explicit unwrapping request.
    207     #[must_use]
    208     pub const fn new(
    209         reference: &'a SecretRef,
    210         context: &'a EnvelopeContext,
    211         wrapped: &'a WrappedSecret,
    212     ) -> Self {
    213         Self {
    214             reference,
    215             context,
    216             wrapped,
    217         }
    218     }
    219 
    220     /// Returns the provider capability reference.
    221     #[must_use]
    222     pub const fn reference(&self) -> &'a SecretRef {
    223         self.reference
    224     }
    225 
    226     /// Returns the independently validated semantic unwrapping authority.
    227     #[must_use]
    228     pub const fn context(&self) -> &'a EnvelopeContext {
    229         self.context
    230     }
    231 
    232     /// Returns the provider-wrapped value.
    233     #[must_use]
    234     pub const fn wrapped(&self) -> &'a WrappedSecret {
    235         self.wrapped
    236     }
    237 }
    238 
    239 /// Executor-neutral, dyn-compatible data-key wrapping.
    240 pub trait KeyWrapping: Send + Sync {
    241     /// Wraps explicit caller-owned plaintext for the selected reference.
    242     fn wrap<'a>(&'a self, request: WrapRequest<'a>) -> BoxFuture<'a, Result<WrappedSecret, Error>>;
    243 
    244     /// Unwraps provider-owned protected material into a zeroizing owner.
    245     fn unwrap<'a>(
    246         &'a self,
    247         request: UnwrapRequest<'a>,
    248     ) -> BoxFuture<'a, Result<SecretMaterial, Error>>;
    249 
    250     /// Unwraps v1 material only when the envelope migration boundary grants authority.
    251     fn unwrap_legacy_v1<'a>(
    252         &'a self,
    253         _request: LegacyV1UnwrapRequest<'a>,
    254     ) -> BoxFuture<'a, Result<SecretMaterial, Error>> {
    255         Box::pin(async { Err(Error::LegacyEnvelopeDenied) })
    256     }
    257 }
    258 
    259 #[cfg(test)]
    260 mod tests {
    261     use super::{
    262         SECRET_MATERIAL_MAX_BYTES, SecretMaterial, WRAPPED_SECRET_MAX_BYTES, WrappedSecret,
    263     };
    264     use alloc::vec;
    265     use alloc::vec::Vec;
    266     use zeroize::Zeroize;
    267 
    268     #[test]
    269     fn owned_plaintext_buffer_zeroizes_in_place() {
    270         let mut material =
    271             SecretMaterial::from_slice(b"owned plaintext sentinel").expect("valid secret material");
    272         let original_len = material.len();
    273         material.0.zeroize();
    274         assert_eq!(material.len(), original_len);
    275         material.expose_secret(|bytes| assert!(bytes.iter().all(|byte| *byte == 0)));
    276     }
    277 
    278     #[test]
    279     fn rejected_owned_plaintext_is_wrapped_before_validation() {
    280         assert!(SecretMaterial::from_owned(Vec::new()).is_err());
    281         assert!(SecretMaterial::from_slice(&vec![0; SECRET_MATERIAL_MAX_BYTES + 1]).is_err());
    282         assert!(WrappedSecret::from_bytes(Vec::new()).is_err());
    283         assert!(WrappedSecret::from_bytes(vec![0; WRAPPED_SECRET_MAX_BYTES + 1]).is_err());
    284     }
    285 }