use thiserror::Error;
use crate::UnauthenticatedRecipientMode;
use crate::recipient::policy::MixingPolicy;
const TYPE_NAME_DISPLAY_MAX: usize = 13;
const _: () = assert!(TYPE_NAME_DISPLAY_MAX >= 1);
struct DisplayableTypeName<'a>(&'a str);
impl std::fmt::Display for DisplayableTypeName<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut iter = self.0.chars();
for _ in 0..TYPE_NAME_DISPLAY_MAX - 1 {
match iter.next() {
Some(ch) => write!(f, "{ch}")?,
None => return Ok(()),
}
}
match iter.next() {
None => Ok(()),
Some(last) => {
if iter.next().is_some() {
f.write_str("…")
} else {
write!(f, "{last}")
}
}
}
}
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum CryptoError {
#[error(transparent)]
Io(std::io::Error),
#[error("Input file or folder missing")]
InputPath,
#[error("{0}")]
InvalidInput(String),
#[error("{0}")]
InvalidFormat(FormatDefect),
#[error("{0}")]
UnsupportedVersion(UnsupportedVersion),
#[error("{0}")]
InvalidKdfParams(InvalidKdfParams),
#[error("KDF resource cap exceeded ({mem_cost_kib} KiB, cap {local_cap_kib})")]
KdfResourceCapExceeded {
mem_cost_kib: u32,
local_cap_kib: u32,
},
#[error("Header length cap exceeded ({header_len} bytes, cap {local_cap})")]
HeaderLenCapExceeded {
header_len: u32,
local_cap: u32,
},
#[error("Recipient count cap exceeded ({count} entries, cap {local_cap})")]
RecipientCountCapExceeded {
count: u16,
local_cap: u16,
},
#[error("Recipient body cap exceeded ({body_len} bytes, cap {local_cap})")]
RecipientBodyCapExceeded {
body_len: u32,
local_cap: u32,
},
#[error("Recipient string cap exceeded ({input_chars} chars, cap {local_cap})")]
RecipientStringCapExceeded {
input_chars: u32,
local_cap: u32,
},
#[error("Private key unlock failed: wrong passphrase or tampered file")]
KeyFileUnlockFailed,
#[error("Decryption failed: header tampered after unlock")]
HeaderTampered,
#[error(
"Decryption failed: recipient `{}` MAC mismatch",
DisplayableTypeName(type_name)
)]
HeaderMacFailedAfterUnwrap {
type_name: String,
},
#[error(
"Decryption failed: recipient `{}` unwrap failed",
DisplayableTypeName(type_name)
)]
RecipientUnwrapFailed {
type_name: String,
},
#[error(
"Unknown critical recipient: `{}`. Upgrade FerroCrypt.",
DisplayableTypeName(type_name)
)]
UnknownCriticalRecipient {
type_name: String,
},
#[error("Decryption failed: no recipient could unlock the file")]
NoSupportedRecipient,
#[error("File is {found} encrypted; use {}", found.credential_name())]
DecryptorModeMismatch {
expected: UnauthenticatedRecipientMode,
found: UnauthenticatedRecipientMode,
},
#[error("Recipient list cannot be empty")]
EmptyRecipientList,
#[error(
"Recipient `{}` mixed with another recipient",
DisplayableTypeName(type_name)
)]
IncompatibleRecipients {
type_name: String,
policy: MixingPolicy,
},
#[error("Payload authentication failed: data tampered or corrupted")]
PayloadTampered,
#[error("Encrypted file is truncated")]
PayloadTruncated,
#[error("Encrypted file has unexpected trailing data")]
ExtraDataAfterPayload,
#[error("Encrypted payload exceeds chunk-count cap")]
PayloadChunkCountExceeded,
#[error("{0}")]
InternalInvariant(&'static str),
#[error("{0}")]
InternalCryptoFailure(&'static str),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FormatDefect {
Truncated,
BadMagic,
ExtTooLarge {
len: u32,
},
MalformedTlv,
UnknownCriticalTag {
tag: u16,
},
NotAKeyFile,
WrongKeyFileType,
MalformedPublicKey,
WrongKind {
kind: u8,
},
MalformedHeader,
OversizedHeader {
header_len: u32,
},
RecipientCountOutOfRange {
count: u16,
},
MalformedTypeName,
MalformedRecipientEntry,
RecipientFlagsReserved,
MalformedPrivateKey,
UnsupportedArchiveVersion {
version: u8,
},
}
impl std::fmt::Display for FormatDefect {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Truncated => f.write_str("File is truncated or corrupted"),
Self::BadMagic => f.write_str("Not a FerroCrypt file"),
Self::ExtTooLarge { len } => {
write!(f, "Extension region is too large ({len} bytes)")
}
Self::MalformedTlv => f.write_str("Extension region is malformed"),
Self::UnknownCriticalTag { tag } => write!(
f,
"Unknown required file feature (tag 0x{tag:04X}). Upgrade FerroCrypt."
),
Self::NotAKeyFile => f.write_str("Not a FerroCrypt key file"),
Self::WrongKeyFileType => f.write_str("Wrong key file kind (public vs private)"),
Self::MalformedPublicKey => f.write_str("Public key is malformed"),
Self::WrongKind { kind } => {
write!(f, "Wrong file kind: 0x{kind:02X}")
}
Self::MalformedHeader => f.write_str("File header is malformed"),
Self::OversizedHeader { header_len } => {
write!(f, "File header is too large ({header_len} bytes)")
}
Self::RecipientCountOutOfRange { count } => {
write!(f, "Recipient count out of range ({count})")
}
Self::MalformedTypeName => f.write_str("Recipient type name is malformed"),
Self::MalformedRecipientEntry => f.write_str("Recipient entry is malformed"),
Self::RecipientFlagsReserved => f.write_str("Recipient entry uses reserved flag bits"),
Self::MalformedPrivateKey => f.write_str("Private key is malformed"),
Self::UnsupportedArchiveVersion { version } => {
write!(
f,
"Unsupported archive version (v{version}). Upgrade FerroCrypt."
)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum UnsupportedVersion {
OlderFile {
version: u8,
},
NewerFile {
version: u8,
},
OlderKey {
version: u8,
},
NewerKey {
version: u8,
},
OlderPublicKey {
version: u8,
},
NewerPublicKey {
version: u8,
},
}
impl std::fmt::Display for UnsupportedVersion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::OlderFile { version } => {
write!(f, "Older file format (v{version}). Use a previous release.")
}
Self::NewerFile { version } => {
write!(f, "Newer file format (v{version}). Upgrade FerroCrypt.")
}
Self::OlderKey { version } => {
write!(f, "Older key format (v{version}). Use a previous release.")
}
Self::NewerKey { version } => {
write!(f, "Newer key format (v{version}). Upgrade FerroCrypt.")
}
Self::OlderPublicKey { version } => {
write!(
f,
"Older public-key format (v{version}). Generate a new key pair."
)
}
Self::NewerPublicKey { version } => {
write!(
f,
"Newer public-key format (v{version}). Upgrade FerroCrypt."
)
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum InvalidKdfParams {
Parallelism(u32),
MemoryCost(u32),
TimeCost(u32),
}
impl std::fmt::Display for InvalidKdfParams {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Parallelism(n) => {
write!(f, "File has invalid KDF settings (parallelism {n})")
}
Self::MemoryCost(n) => {
write!(f, "File has invalid KDF settings ({n} KiB memory)")
}
Self::TimeCost(n) => write!(f, "File has invalid KDF settings (time cost {n})"),
}
}
}
#[derive(Debug)]
pub(crate) enum StreamError {
DecryptAead,
EncryptAead,
Truncated,
ExtraData,
StateExhausted,
ChunkCountExceeded,
}
impl std::fmt::Display for StreamError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let msg = match self {
StreamError::DecryptAead => "Payload authentication failed",
StreamError::EncryptAead => "Internal error: payload encryption failed",
StreamError::Truncated => "Encrypted stream truncated",
StreamError::ExtraData => "Encrypted stream has trailing data",
StreamError::StateExhausted => "Internal error: stream state already finalized",
StreamError::ChunkCountExceeded => "Encrypted stream exceeds chunk-count cap",
};
f.write_str(msg)
}
}
impl std::error::Error for StreamError {}
impl From<std::io::Error> for CryptoError {
fn from(e: std::io::Error) -> Self {
if let Some(stream_err) = e
.get_ref()
.and_then(|inner| inner.downcast_ref::<StreamError>())
{
return match stream_err {
StreamError::DecryptAead => CryptoError::PayloadTampered,
StreamError::Truncated => CryptoError::PayloadTruncated,
StreamError::ExtraData => CryptoError::ExtraDataAfterPayload,
StreamError::ChunkCountExceeded => CryptoError::PayloadChunkCountExceeded,
StreamError::EncryptAead => {
CryptoError::InternalCryptoFailure("Internal error: payload encryption failed")
}
StreamError::StateExhausted => {
CryptoError::InternalInvariant("Internal error: stream state already finalized")
}
};
}
CryptoError::Io(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::recipient::policy::NativeMixingRule;
#[test]
fn typed_decryption_errors_display_exact_strings() {
assert_eq!(
CryptoError::InputPath.to_string(),
"Input file or folder missing"
);
assert_eq!(
CryptoError::KeyFileUnlockFailed.to_string(),
"Private key unlock failed: wrong passphrase or tampered file"
);
assert_eq!(
CryptoError::HeaderTampered.to_string(),
"Decryption failed: header tampered after unlock"
);
assert_eq!(
CryptoError::HeaderMacFailedAfterUnwrap {
type_name: "x25519".to_owned()
}
.to_string(),
"Decryption failed: recipient `x25519` MAC mismatch"
);
assert_eq!(
CryptoError::UnknownCriticalRecipient {
type_name: "mlkem768x25519".to_owned()
}
.to_string(),
"Unknown critical recipient: `mlkem768x255…`. Upgrade FerroCrypt."
);
assert_eq!(
CryptoError::NoSupportedRecipient.to_string(),
"Decryption failed: no recipient could unlock the file"
);
assert_eq!(
CryptoError::DecryptorModeMismatch {
expected: UnauthenticatedRecipientMode::Passphrase,
found: UnauthenticatedRecipientMode::PublicKey,
}
.to_string(),
"File is public-key encrypted; use a private key"
);
assert_eq!(
CryptoError::DecryptorModeMismatch {
expected: UnauthenticatedRecipientMode::PublicKey,
found: UnauthenticatedRecipientMode::Passphrase,
}
.to_string(),
"File is passphrase encrypted; use a passphrase"
);
assert_eq!(
CryptoError::EmptyRecipientList.to_string(),
"Recipient list cannot be empty"
);
assert_eq!(
CryptoError::IncompatibleRecipients {
type_name: "argon2id".to_owned(),
policy: MixingPolicy::Exclusive,
}
.to_string(),
"Recipient `argon2id` mixed with another recipient"
);
assert_eq!(
CryptoError::IncompatibleRecipients {
type_name: "mlkem768x25519".to_owned(),
policy: MixingPolicy::Exclusive,
}
.to_string(),
"Recipient `mlkem768x255…` mixed with another recipient"
);
assert_eq!(
CryptoError::IncompatibleRecipients {
type_name: "x25519-mlkem768".to_owned(),
policy: MixingPolicy::Custom {
compatibility_class: NativeMixingRule::POST_QUANTUM_CLASS,
},
}
.to_string(),
"Recipient `x25519-mlkem…` mixed with another recipient"
);
assert_eq!(
CryptoError::PayloadTampered.to_string(),
"Payload authentication failed: data tampered or corrupted"
);
assert_eq!(
CryptoError::PayloadTruncated.to_string(),
"Encrypted file is truncated"
);
assert_eq!(
CryptoError::ExtraDataAfterPayload.to_string(),
"Encrypted file has unexpected trailing data"
);
assert_eq!(
CryptoError::RecipientUnwrapFailed {
type_name: "x25519".to_owned()
}
.to_string(),
"Decryption failed: recipient `x25519` unwrap failed"
);
assert_eq!(
CryptoError::RecipientBodyCapExceeded {
body_len: 10_000,
local_cap: 8_192
}
.to_string(),
"Recipient body cap exceeded (10000 bytes, cap 8192)"
);
assert_eq!(
CryptoError::RecipientStringCapExceeded {
input_chars: 5_000,
local_cap: 1_024,
}
.to_string(),
"Recipient string cap exceeded (5000 chars, cap 1024)"
);
assert_eq!(
CryptoError::HeaderLenCapExceeded {
header_len: 2_000_000,
local_cap: 1_048_576,
}
.to_string(),
"Header length cap exceeded (2000000 bytes, cap 1048576)"
);
assert_eq!(
CryptoError::RecipientCountCapExceeded {
count: 100,
local_cap: 64,
}
.to_string(),
"Recipient count cap exceeded (100 entries, cap 64)"
);
assert_eq!(
CryptoError::KdfResourceCapExceeded {
mem_cost_kib: 1_048_576,
local_cap_kib: 524_288,
}
.to_string(),
"KDF resource cap exceeded (1048576 KiB, cap 524288)"
);
}
#[test]
fn typed_format_variants_display_exact_strings() {
assert_eq!(
FormatDefect::Truncated.to_string(),
"File is truncated or corrupted"
);
assert_eq!(FormatDefect::BadMagic.to_string(), "Not a FerroCrypt file");
assert_eq!(
FormatDefect::ExtTooLarge { len: 65_537 }.to_string(),
"Extension region is too large (65537 bytes)"
);
assert_eq!(
FormatDefect::MalformedTlv.to_string(),
"Extension region is malformed"
);
assert_eq!(
FormatDefect::UnknownCriticalTag { tag: 0x8001 }.to_string(),
"Unknown required file feature (tag 0x8001). Upgrade FerroCrypt."
);
assert_eq!(
FormatDefect::NotAKeyFile.to_string(),
"Not a FerroCrypt key file"
);
assert_eq!(
FormatDefect::WrongKeyFileType.to_string(),
"Wrong key file kind (public vs private)"
);
assert_eq!(
FormatDefect::MalformedPublicKey.to_string(),
"Public key is malformed"
);
assert_eq!(
FormatDefect::WrongKind { kind: 0x99 }.to_string(),
"Wrong file kind: 0x99"
);
assert_eq!(
FormatDefect::MalformedHeader.to_string(),
"File header is malformed"
);
assert_eq!(
FormatDefect::OversizedHeader {
header_len: 16_777_217
}
.to_string(),
"File header is too large (16777217 bytes)"
);
assert_eq!(
FormatDefect::MalformedTypeName.to_string(),
"Recipient type name is malformed"
);
assert_eq!(
FormatDefect::MalformedRecipientEntry.to_string(),
"Recipient entry is malformed"
);
assert_eq!(
FormatDefect::RecipientFlagsReserved.to_string(),
"Recipient entry uses reserved flag bits"
);
assert_eq!(
FormatDefect::MalformedPrivateKey.to_string(),
"Private key is malformed"
);
assert_eq!(
FormatDefect::UnsupportedArchiveVersion { version: 0xFF }.to_string(),
"Unsupported archive version (v255). Upgrade FerroCrypt."
);
assert_eq!(
FormatDefect::RecipientCountOutOfRange { count: 5000 }.to_string(),
"Recipient count out of range (5000)"
);
assert_eq!(
UnsupportedVersion::NewerFile { version: 9 }.to_string(),
"Newer file format (v9). Upgrade FerroCrypt."
);
assert_eq!(
UnsupportedVersion::OlderFile { version: 1 }.to_string(),
"Older file format (v1). Use a previous release."
);
assert_eq!(
UnsupportedVersion::NewerKey { version: 9 }.to_string(),
"Newer key format (v9). Upgrade FerroCrypt."
);
assert_eq!(
UnsupportedVersion::OlderKey { version: 1 }.to_string(),
"Older key format (v1). Use a previous release."
);
assert_eq!(
UnsupportedVersion::OlderPublicKey { version: 1 }.to_string(),
"Older public-key format (v1). Generate a new key pair."
);
assert_eq!(
UnsupportedVersion::NewerPublicKey { version: 9 }.to_string(),
"Newer public-key format (v9). Upgrade FerroCrypt."
);
assert_eq!(
InvalidKdfParams::Parallelism(9999).to_string(),
"File has invalid KDF settings (parallelism 9999)"
);
assert_eq!(
InvalidKdfParams::MemoryCost(42).to_string(),
"File has invalid KDF settings (42 KiB memory)"
);
assert_eq!(
InvalidKdfParams::TimeCost(7).to_string(),
"File has invalid KDF settings (time cost 7)"
);
assert_eq!(
StreamError::DecryptAead.to_string(),
"Payload authentication failed"
);
assert_eq!(
StreamError::EncryptAead.to_string(),
"Internal error: payload encryption failed"
);
assert_eq!(
StreamError::Truncated.to_string(),
"Encrypted stream truncated"
);
assert_eq!(
StreamError::ExtraData.to_string(),
"Encrypted stream has trailing data"
);
assert_eq!(
StreamError::StateExhausted.to_string(),
"Internal error: stream state already finalized"
);
assert_eq!(
StreamError::ChunkCountExceeded.to_string(),
"Encrypted stream exceeds chunk-count cap"
);
}
#[test]
fn user_facing_messages_fit_status_line_budget() {
const BUDGET: usize = 64;
fn check(label: &str, msg: &str) {
let chars = msg.chars().count();
assert!(
chars <= BUDGET,
"message over {BUDGET}-char budget ({chars} chars) [{label}]: {msg}",
);
}
check("InputPath", &CryptoError::InputPath.to_string());
check(
"KeyFileUnlockFailed",
&CryptoError::KeyFileUnlockFailed.to_string(),
);
check("HeaderTampered", &CryptoError::HeaderTampered.to_string());
check(
"NoSupportedRecipient",
&CryptoError::NoSupportedRecipient.to_string(),
);
check(
"DecryptorModeMismatch(passphrase, public-key)",
&CryptoError::DecryptorModeMismatch {
expected: UnauthenticatedRecipientMode::Passphrase,
found: UnauthenticatedRecipientMode::PublicKey,
}
.to_string(),
);
check(
"DecryptorModeMismatch(public-key, passphrase)",
&CryptoError::DecryptorModeMismatch {
expected: UnauthenticatedRecipientMode::PublicKey,
found: UnauthenticatedRecipientMode::Passphrase,
}
.to_string(),
);
check(
"EmptyRecipientList",
&CryptoError::EmptyRecipientList.to_string(),
);
check(
"IncompatibleRecipients(argon2id, Exclusive)",
&CryptoError::IncompatibleRecipients {
type_name: "argon2id".to_owned(),
policy: MixingPolicy::Exclusive,
}
.to_string(),
);
check(
"IncompatibleRecipients(truncated, Exclusive)",
&CryptoError::IncompatibleRecipients {
type_name: "mlkem768x25519".to_owned(),
policy: MixingPolicy::Exclusive,
}
.to_string(),
);
check(
"IncompatibleRecipients(argon2id, PublicKeyMixable)",
&CryptoError::IncompatibleRecipients {
type_name: "argon2id".to_owned(),
policy: MixingPolicy::PublicKeyMixable,
}
.to_string(),
);
check(
"IncompatibleRecipients(truncated, Custom)",
&CryptoError::IncompatibleRecipients {
type_name: "x25519-mlkem768".to_owned(),
policy: MixingPolicy::Custom {
compatibility_class: NativeMixingRule::POST_QUANTUM_CLASS,
},
}
.to_string(),
);
check("PayloadTampered", &CryptoError::PayloadTampered.to_string());
check(
"PayloadTruncated",
&CryptoError::PayloadTruncated.to_string(),
);
check(
"ExtraDataAfterPayload",
&CryptoError::ExtraDataAfterPayload.to_string(),
);
check(
"KdfResourceCapExceeded(max)",
&CryptoError::KdfResourceCapExceeded {
mem_cost_kib: u32::MAX,
local_cap_kib: u32::MAX,
}
.to_string(),
);
check(
"HeaderLenCapExceeded(max)",
&CryptoError::HeaderLenCapExceeded {
header_len: u32::MAX,
local_cap: u32::MAX,
}
.to_string(),
);
check(
"RecipientCountCapExceeded(max)",
&CryptoError::RecipientCountCapExceeded {
count: u16::MAX,
local_cap: u16::MAX,
}
.to_string(),
);
check(
"RecipientBodyCapExceeded(max)",
&CryptoError::RecipientBodyCapExceeded {
body_len: u32::MAX,
local_cap: u32::MAX,
}
.to_string(),
);
check(
"RecipientStringCapExceeded(max)",
&CryptoError::RecipientStringCapExceeded {
input_chars: u32::MAX,
local_cap: u32::MAX,
}
.to_string(),
);
let max_name = "x".repeat(u8::MAX as usize);
check(
"RecipientUnwrapFailed(max-name)",
&CryptoError::RecipientUnwrapFailed {
type_name: max_name.clone(),
}
.to_string(),
);
check(
"HeaderMacFailedAfterUnwrap(max-name)",
&CryptoError::HeaderMacFailedAfterUnwrap {
type_name: max_name.clone(),
}
.to_string(),
);
check(
"UnknownCriticalRecipient(max-name)",
&CryptoError::UnknownCriticalRecipient {
type_name: max_name,
}
.to_string(),
);
let defects: &[(&str, FormatDefect)] = &[
("Truncated", FormatDefect::Truncated),
("BadMagic", FormatDefect::BadMagic),
("ExtTooLarge", FormatDefect::ExtTooLarge { len: u32::MAX }),
("MalformedTlv", FormatDefect::MalformedTlv),
(
"UnknownCriticalTag",
FormatDefect::UnknownCriticalTag { tag: u16::MAX },
),
("NotAKeyFile", FormatDefect::NotAKeyFile),
("WrongKeyFileType", FormatDefect::WrongKeyFileType),
("MalformedPublicKey", FormatDefect::MalformedPublicKey),
("WrongKind", FormatDefect::WrongKind { kind: u8::MAX }),
("MalformedHeader", FormatDefect::MalformedHeader),
(
"OversizedHeader(max)",
FormatDefect::OversizedHeader {
header_len: u32::MAX,
},
),
(
"RecipientCountOutOfRange(max)",
FormatDefect::RecipientCountOutOfRange { count: u16::MAX },
),
("MalformedTypeName", FormatDefect::MalformedTypeName),
(
"MalformedRecipientEntry",
FormatDefect::MalformedRecipientEntry,
),
(
"RecipientFlagsReserved",
FormatDefect::RecipientFlagsReserved,
),
("MalformedPrivateKey", FormatDefect::MalformedPrivateKey),
(
"UnsupportedArchiveVersion(max)",
FormatDefect::UnsupportedArchiveVersion { version: u8::MAX },
),
];
for (label, d) in defects {
check(label, &d.to_string());
}
let versions: &[(&str, UnsupportedVersion)] = &[
(
"OlderFile(max)",
UnsupportedVersion::OlderFile { version: u8::MAX },
),
(
"NewerFile(max)",
UnsupportedVersion::NewerFile { version: u8::MAX },
),
(
"OlderKey(max)",
UnsupportedVersion::OlderKey { version: u8::MAX },
),
(
"NewerKey(max)",
UnsupportedVersion::NewerKey { version: u8::MAX },
),
(
"OlderPublicKey(max)",
UnsupportedVersion::OlderPublicKey { version: u8::MAX },
),
(
"NewerPublicKey(max)",
UnsupportedVersion::NewerPublicKey { version: u8::MAX },
),
];
for (label, v) in versions {
check(label, &v.to_string());
}
let kdf: &[(&str, InvalidKdfParams)] = &[
("Parallelism(max)", InvalidKdfParams::Parallelism(u32::MAX)),
("MemoryCost(max)", InvalidKdfParams::MemoryCost(u32::MAX)),
("TimeCost(max)", InvalidKdfParams::TimeCost(u32::MAX)),
];
for (label, p) in kdf {
check(label, &p.to_string());
}
check(
"StreamError::DecryptAead",
&StreamError::DecryptAead.to_string(),
);
check(
"StreamError::EncryptAead",
&StreamError::EncryptAead.to_string(),
);
check(
"StreamError::Truncated",
&StreamError::Truncated.to_string(),
);
check(
"StreamError::ExtraData",
&StreamError::ExtraData.to_string(),
);
check(
"StreamError::StateExhausted",
&StreamError::StateExhausted.to_string(),
);
check(
"StreamError::ChunkCountExceeded",
&StreamError::ChunkCountExceeded.to_string(),
);
}
#[test]
fn stream_error_markers_map_to_typed_variants() {
fn from_marker(marker: StreamError) -> CryptoError {
std::io::Error::other(marker).into()
}
assert!(matches!(
from_marker(StreamError::DecryptAead),
CryptoError::PayloadTampered
));
assert!(matches!(
from_marker(StreamError::Truncated),
CryptoError::PayloadTruncated
));
assert!(matches!(
from_marker(StreamError::ExtraData),
CryptoError::ExtraDataAfterPayload
));
assert!(matches!(
from_marker(StreamError::ChunkCountExceeded),
CryptoError::PayloadChunkCountExceeded
));
match from_marker(StreamError::EncryptAead) {
CryptoError::InternalCryptoFailure(msg) => {
assert_eq!(msg, "Internal error: payload encryption failed");
assert_eq!(msg, StreamError::EncryptAead.to_string());
}
other => panic!("expected InternalCryptoFailure, got {other:?}"),
}
match from_marker(StreamError::StateExhausted) {
CryptoError::InternalInvariant(msg) => {
assert_eq!(msg, "Internal error: stream state already finalized");
assert_eq!(msg, StreamError::StateExhausted.to_string());
}
other => panic!("expected InternalInvariant, got {other:?}"),
}
let plain: CryptoError = std::io::Error::other("bare message").into();
assert!(
matches!(plain, CryptoError::Io(_)),
"unmarked io::Error must map to CryptoError::Io, got {plain:?}"
);
}
}