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 }