error.rs (16836B)
1 //! Normalized secret-operation errors. 2 3 use crate::id::BackendKind; 4 use core::fmt; 5 6 /// Why a [`crate::SecretId`] failed validation. 7 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 8 #[non_exhaustive] 9 pub enum SecretIdError { 10 /// The identifier was empty. 11 Empty, 12 /// The identifier exceeded the package limit. 13 TooLong { 14 /// Observed UTF-8 byte length. 15 actual_bytes: usize, 16 /// Maximum accepted UTF-8 byte length. 17 max_bytes: usize, 18 }, 19 /// The identifier contained a character outside its portable alphabet. 20 InvalidCharacter { 21 /// UTF-8 byte offset of the invalid character. 22 byte_offset: usize, 23 }, 24 } 25 26 /// Authenticated context field rejected by validation. 27 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 28 #[non_exhaustive] 29 pub enum ContextField { 30 /// Envelope use-case identifier. 31 Purpose, 32 /// Subject type discriminator. 33 SubjectType, 34 /// Canonical subject identity. 35 SubjectValue, 36 /// Plaintext schema identifier. 37 PayloadSchema, 38 } 39 40 /// Secret-safe reason an authenticated context field was rejected. 41 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 42 #[non_exhaustive] 43 pub enum ContextValueError { 44 /// The field was empty. 45 Empty, 46 /// The field exceeded its explicit bound. 47 TooLong { 48 /// Observed UTF-8 byte length. 49 actual_bytes: usize, 50 /// Maximum accepted UTF-8 byte length. 51 max_bytes: usize, 52 }, 53 /// The field did not use its canonical portable representation. 54 NonCanonical, 55 } 56 57 /// A security property requested by a host but unsupported by a provider. 58 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 59 #[non_exhaustive] 60 pub enum PolicyRequirement { 61 /// The secret must remain device-local. 62 DeviceLocal, 63 /// The provider must require user presence. 64 UserPresence, 65 /// The provider must use hardware-backed protection. 66 HardwareBacked, 67 } 68 69 /// A normalized provider operation. 70 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 71 #[non_exhaustive] 72 pub enum Operation { 73 /// Create or open a provider root. 74 Open, 75 /// Provision caller-supplied material. 76 Provision, 77 /// Rotate caller-supplied material. 78 Rotate, 79 /// Remove provider-owned material. 80 Remove, 81 /// Read protected provider state. 82 Read, 83 /// Persist protected provider state. 84 Write, 85 /// Wrap plaintext key material. 86 Wrap, 87 /// Unwrap protected key material. 88 Unwrap, 89 } 90 91 /// A normalized, secret-safe package failure. 92 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 93 #[non_exhaustive] 94 pub enum Error { 95 /// A secret identifier failed validation. 96 InvalidSecretId(SecretIdError), 97 /// An authenticated envelope context field failed validation. 98 InvalidContextValue { 99 /// Rejected semantic field without its value. 100 field: ContextField, 101 /// Secret-safe validation class. 102 reason: ContextValueError, 103 }, 104 /// The independently expected context did not match authenticated metadata. 105 ContextMismatch, 106 /// Key versions start at one; zero is never a valid version. 107 InvalidKeyVersion, 108 /// Secret material was empty or exceeded the bounded input limit. 109 InvalidSecretLength { 110 /// Observed byte length. 111 actual_bytes: usize, 112 /// Maximum accepted byte length. 113 max_bytes: usize, 114 }, 115 /// Wrapped material was empty or exceeded the bounded input limit. 116 InvalidWrappedLength { 117 /// Observed byte length. 118 actual_bytes: usize, 119 /// Maximum accepted byte length. 120 max_bytes: usize, 121 }, 122 /// No explicitly selected provider was available. 123 BackendUnavailable { 124 /// Requested adapter family. 125 backend: BackendKind, 126 }, 127 /// A provider cannot satisfy a required security property. 128 PolicyUnsupported { 129 /// Provider that rejected the policy. 130 backend: BackendKind, 131 /// Unsupported property. 132 requirement: PolicyRequirement, 133 }, 134 /// A reference was sent to the wrong provider family. 135 BackendMismatch { 136 /// Provider selected by the host. 137 provider: BackendKind, 138 /// Provider recorded by the reference. 139 reference: BackendKind, 140 }, 141 /// A provider operation failed without exposing its native diagnostic. 142 BackendFailure { 143 /// Provider that failed. 144 backend: BackendKind, 145 /// Normalized operation that failed. 146 operation: Operation, 147 }, 148 /// The referenced provider-owned key was not found. 149 SecretNotFound { 150 /// Provider that did not contain the key. 151 backend: BackendKind, 152 /// Missing key revision. 153 key_version: u32, 154 }, 155 /// The referenced provider-owned key already exists. 156 SecretAlreadyExists { 157 /// Provider that already contains the key. 158 backend: BackendKind, 159 /// Existing key revision. 160 key_version: u32, 161 }, 162 /// A key rotation did not preserve identity or advance the version. 163 InvalidRotation, 164 /// A filesystem path was relative, traversing, symlinked, or not a file. 165 UnsafePath, 166 /// Filesystem permissions allowed access outside the current user. 167 InsecurePermissions, 168 /// An OS keyring service identifier failed portable validation. 169 InvalidServiceName, 170 /// Envelope data exceeded the package-wide bound. 171 EnvelopeTooLarge { 172 /// Observed byte length. 173 actual_bytes: usize, 174 /// Maximum accepted byte length. 175 max_bytes: usize, 176 }, 177 /// Envelope bytes were truncated or structurally invalid. 178 EnvelopeMalformed, 179 /// The encoded envelope version is not supported. 180 UnsupportedEnvelopeVersion { 181 /// Observed version number. 182 version: u16, 183 }, 184 /// The authenticated context encoding version is unsupported. 185 UnsupportedContextVersion { 186 /// Observed context encoding version. 187 version: u16, 188 }, 189 /// A v1 envelope was presented to the normal v2-only open API. 190 LegacyEnvelopeDenied, 191 /// The expected legacy provider reference did not match authenticated v1 metadata. 192 ProviderReferenceMismatch, 193 /// A legacy payload failed its owning schema validator. 194 LegacyPayloadValidationFailed, 195 /// Legacy key or nonce material was reused for a v2 reseal. 196 LegacyEntropyReuse, 197 /// The encoded cipher identifier is not supported. 198 UnsupportedCipher { 199 /// Observed cipher identifier. 200 cipher: u8, 201 }, 202 /// The encoded key-source identifier is not supported. 203 UnsupportedKeySource { 204 /// Observed key-source identifier. 205 key_source: u8, 206 }, 207 /// The encoded backend identifier is not supported. 208 UnsupportedBackend { 209 /// Observed backend identifier. 210 backend: u8, 211 }, 212 /// A data-encryption key had an invalid length. 213 InvalidDataKeyLength { 214 /// Observed byte length. 215 actual_bytes: usize, 216 }, 217 /// Authenticated encryption failed. 218 EncryptFailed, 219 /// Authentication or decryption failed. 220 DecryptFailed, 221 } 222 223 impl fmt::Display for SecretIdError { 224 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 225 match self { 226 Self::Empty => formatter.write_str("secret identifier is empty"), 227 Self::TooLong { 228 actual_bytes, 229 max_bytes, 230 } => write!( 231 formatter, 232 "secret identifier is too long: {actual_bytes} bytes; maximum is {max_bytes}" 233 ), 234 Self::InvalidCharacter { byte_offset } => write!( 235 formatter, 236 "secret identifier contains an invalid character at byte offset {byte_offset}" 237 ), 238 } 239 } 240 } 241 242 impl fmt::Display for Error { 243 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 244 match self { 245 Self::InvalidSecretId(reason) => reason.fmt(formatter), 246 Self::InvalidContextValue { field, reason } => { 247 write!(formatter, "envelope context {field:?} is {reason}") 248 } 249 Self::ContextMismatch => formatter.write_str("encrypted envelope context mismatch"), 250 Self::InvalidKeyVersion => formatter.write_str("secret key version must be non-zero"), 251 Self::InvalidSecretLength { 252 actual_bytes, 253 max_bytes, 254 } => write!( 255 formatter, 256 "secret material length is invalid: {actual_bytes} bytes; maximum is {max_bytes}" 257 ), 258 Self::InvalidWrappedLength { 259 actual_bytes, 260 max_bytes, 261 } => write!( 262 formatter, 263 "wrapped material length is invalid: {actual_bytes} bytes; maximum is {max_bytes}" 264 ), 265 Self::BackendUnavailable { backend } => { 266 write!(formatter, "secret backend {backend:?} is unavailable") 267 } 268 Self::PolicyUnsupported { 269 backend, 270 requirement, 271 } => write!( 272 formatter, 273 "secret backend {backend:?} does not satisfy {requirement:?}" 274 ), 275 Self::BackendMismatch { 276 provider, 277 reference, 278 } => write!( 279 formatter, 280 "secret reference backend {reference:?} does not match provider {provider:?}" 281 ), 282 Self::BackendFailure { backend, operation } => write!( 283 formatter, 284 "secret backend {backend:?} failed during {operation:?}" 285 ), 286 Self::SecretNotFound { 287 backend, 288 key_version, 289 } => write!( 290 formatter, 291 "secret backend {backend:?} has no key at version {key_version}" 292 ), 293 Self::SecretAlreadyExists { 294 backend, 295 key_version, 296 } => write!( 297 formatter, 298 "secret backend {backend:?} already has key version {key_version}" 299 ), 300 Self::InvalidRotation => { 301 formatter.write_str("secret rotation must preserve identity and advance version") 302 } 303 Self::UnsafePath => formatter.write_str("secret provider path is unsafe"), 304 Self::InsecurePermissions => { 305 formatter.write_str("secret provider permissions are insecure") 306 } 307 Self::InvalidServiceName => { 308 formatter.write_str("secret provider service name is invalid") 309 } 310 Self::EnvelopeTooLarge { 311 actual_bytes, 312 max_bytes, 313 } => write!( 314 formatter, 315 "encrypted envelope is too large: {actual_bytes} bytes; maximum is {max_bytes}" 316 ), 317 Self::EnvelopeMalformed => formatter.write_str("encrypted envelope is malformed"), 318 Self::UnsupportedEnvelopeVersion { version } => { 319 write!( 320 formatter, 321 "encrypted envelope version {version} is unsupported" 322 ) 323 } 324 Self::UnsupportedContextVersion { version } => { 325 write!( 326 formatter, 327 "envelope context version {version} is unsupported" 328 ) 329 } 330 Self::LegacyEnvelopeDenied => { 331 formatter.write_str("legacy encrypted envelope requires migration authority") 332 } 333 Self::ProviderReferenceMismatch => { 334 formatter.write_str("encrypted envelope provider reference mismatch") 335 } 336 Self::LegacyPayloadValidationFailed => { 337 formatter.write_str("legacy encrypted payload failed schema validation") 338 } 339 Self::LegacyEntropyReuse => { 340 formatter.write_str("legacy envelope cryptographic material cannot be reused") 341 } 342 Self::UnsupportedCipher { cipher } => { 343 write!( 344 formatter, 345 "encrypted envelope cipher {cipher} is unsupported" 346 ) 347 } 348 Self::UnsupportedKeySource { key_source } => write!( 349 formatter, 350 "encrypted envelope key source {key_source} is unsupported" 351 ), 352 Self::UnsupportedBackend { backend } => { 353 write!( 354 formatter, 355 "encrypted envelope backend {backend} is unsupported" 356 ) 357 } 358 Self::InvalidDataKeyLength { actual_bytes } => write!( 359 formatter, 360 "envelope data key must be 32 bytes; got {actual_bytes}" 361 ), 362 Self::EncryptFailed => formatter.write_str("encrypted envelope sealing failed"), 363 Self::DecryptFailed => formatter.write_str("encrypted envelope authentication failed"), 364 } 365 } 366 } 367 368 impl fmt::Display for ContextValueError { 369 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 370 match self { 371 Self::Empty => formatter.write_str("empty"), 372 Self::TooLong { 373 actual_bytes, 374 max_bytes, 375 } => write!( 376 formatter, 377 "too long ({actual_bytes} bytes; maximum is {max_bytes})" 378 ), 379 Self::NonCanonical => formatter.write_str("not canonical"), 380 } 381 } 382 } 383 384 #[cfg(feature = "std")] 385 impl std::error::Error for Error {} 386 387 #[cfg(test)] 388 mod tests { 389 use super::*; 390 use alloc::format; 391 use alloc::string::ToString; 392 393 #[test] 394 fn every_normalized_error_has_a_secret_safe_message() { 395 let id_errors = [ 396 SecretIdError::Empty, 397 SecretIdError::TooLong { 398 actual_bytes: 2, 399 max_bytes: 1, 400 }, 401 SecretIdError::InvalidCharacter { byte_offset: 3 }, 402 ]; 403 for error in id_errors { 404 assert!(!error.to_string().is_empty()); 405 } 406 let errors = [ 407 Error::InvalidSecretId(SecretIdError::Empty), 408 Error::InvalidContextValue { 409 field: ContextField::SubjectValue, 410 reason: ContextValueError::NonCanonical, 411 }, 412 Error::ContextMismatch, 413 Error::InvalidKeyVersion, 414 Error::InvalidSecretLength { 415 actual_bytes: 0, 416 max_bytes: 1, 417 }, 418 Error::InvalidWrappedLength { 419 actual_bytes: 0, 420 max_bytes: 1, 421 }, 422 Error::BackendUnavailable { 423 backend: BackendKind::Memory, 424 }, 425 Error::PolicyUnsupported { 426 backend: BackendKind::File, 427 requirement: PolicyRequirement::DeviceLocal, 428 }, 429 Error::BackendMismatch { 430 provider: BackendKind::Memory, 431 reference: BackendKind::File, 432 }, 433 Error::BackendFailure { 434 backend: BackendKind::Keyring, 435 operation: Operation::Open, 436 }, 437 Error::SecretNotFound { 438 backend: BackendKind::External, 439 key_version: 1, 440 }, 441 Error::SecretAlreadyExists { 442 backend: BackendKind::Memory, 443 key_version: 2, 444 }, 445 Error::InvalidRotation, 446 Error::UnsafePath, 447 Error::InsecurePermissions, 448 Error::InvalidServiceName, 449 Error::EnvelopeTooLarge { 450 actual_bytes: 2, 451 max_bytes: 1, 452 }, 453 Error::EnvelopeMalformed, 454 Error::UnsupportedEnvelopeVersion { version: 2 }, 455 Error::UnsupportedContextVersion { version: 2 }, 456 Error::LegacyEnvelopeDenied, 457 Error::ProviderReferenceMismatch, 458 Error::LegacyPayloadValidationFailed, 459 Error::LegacyEntropyReuse, 460 Error::UnsupportedCipher { cipher: 9 }, 461 Error::UnsupportedKeySource { key_source: 9 }, 462 Error::UnsupportedBackend { backend: 9 }, 463 Error::InvalidDataKeyLength { actual_bytes: 1 }, 464 Error::EncryptFailed, 465 Error::DecryptFailed, 466 ]; 467 for error in errors { 468 assert!(!error.to_string().is_empty()); 469 } 470 for operation in [ 471 Operation::Open, 472 Operation::Provision, 473 Operation::Rotate, 474 Operation::Remove, 475 Operation::Read, 476 Operation::Write, 477 Operation::Wrap, 478 Operation::Unwrap, 479 ] { 480 assert!(!format!("{operation:?}").is_empty()); 481 } 482 } 483 }