Skip to main content

dryoc/
error.rs

1use core::fmt::{Display, Formatter};
2
3/// The input, output, or operation associated with an [`Error`].
4#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5#[non_exhaustive]
6pub enum ErrorContext {
7    /// Associated data supplied to an authenticated operation.
8    AssociatedData,
9    /// An authenticated-encryption ciphertext.
10    AeadCiphertext,
11    /// An authenticated-encryption envelope.
12    AeadEnvelope,
13    /// A message authentication tag.
14    AuthenticationTag,
15    /// A BLAKE2b key.
16    Blake2bKey,
17    /// BLAKE2b output.
18    Blake2bOutput,
19    /// A BLAKE2b operation or state.
20    Blake2b,
21    /// An authenticated public-key box.
22    Box,
23    /// Ciphertext input or output.
24    Ciphertext,
25    /// A Curve25519 public key.
26    Curve25519PublicKey,
27    /// An in-place data buffer.
28    Data,
29    /// A domain-separation byte.
30    Domain,
31    /// An Ed25519 public key.
32    Ed25519PublicKey,
33    /// An ephemeral public key.
34    EphemeralPublicKey,
35    /// An Argon2 memory-cost parameter.
36    MemoryCost,
37    /// A password-hashing memory limit.
38    MemoryLimit,
39    /// Plaintext message input or output.
40    Message,
41    /// A nonce.
42    Nonce,
43    /// A password-hashing operations limit.
44    OperationsLimit,
45    /// A generic output buffer.
46    Output,
47    /// An Argon2 parallelism parameter.
48    Parallelism,
49    /// A password.
50    Password,
51    /// An encoded password hash or its hash field.
52    PasswordHash,
53    /// A password-hash algorithm field.
54    PasswordHashAlgorithm,
55    /// A password-hash memory-cost field.
56    PasswordHashMemoryCost,
57    /// A password-hash parallelism field.
58    PasswordHashParallelism,
59    /// A password-hash salt field.
60    PasswordHashSalt,
61    /// A password-hash time-cost field.
62    PasswordHashTimeCost,
63    /// A password-hash version field.
64    PasswordHashVersion,
65    /// A protected-memory value or operation.
66    ProtectedMemory,
67    /// A public key.
68    PublicKey,
69    /// A sealed public-key box.
70    SealedBox,
71    /// An Argon2 secret input.
72    Secret,
73    /// An authenticated secret-key box.
74    SecretBox,
75    /// A secret key.
76    SecretKey,
77    /// A signature.
78    Signature,
79    /// A signed message.
80    SignedMessage,
81    /// A byte slice.
82    Slice,
83    /// A derived subkey.
84    Subkey,
85    /// A secretstream message tag.
86    Tag,
87    /// An Argon2 time-cost parameter.
88    TimeCost,
89    /// An extendable-output function (XOF) state.
90    Xof,
91}
92
93impl Display for ErrorContext {
94    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
95        f.write_str(match self {
96            Self::AssociatedData => "associated data",
97            Self::AeadCiphertext => "AEAD ciphertext",
98            Self::AeadEnvelope => "AEAD envelope",
99            Self::AuthenticationTag => "authentication tag",
100            Self::Blake2bKey => "BLAKE2b key",
101            Self::Blake2bOutput => "BLAKE2b output",
102            Self::Blake2b => "BLAKE2b",
103            Self::Box => "box",
104            Self::Ciphertext => "ciphertext",
105            Self::Curve25519PublicKey => "Curve25519 public key",
106            Self::Data => "data",
107            Self::Domain => "domain separator",
108            Self::Ed25519PublicKey => "Ed25519 public key",
109            Self::EphemeralPublicKey => "ephemeral public key",
110            Self::MemoryCost => "memory cost",
111            Self::MemoryLimit => "memory limit",
112            Self::Message => "message",
113            Self::Nonce => "nonce",
114            Self::OperationsLimit => "operations limit",
115            Self::Output => "output",
116            Self::Parallelism => "parallelism",
117            Self::Password => "password",
118            Self::PasswordHash => "password hash",
119            Self::PasswordHashAlgorithm => "password hash algorithm",
120            Self::PasswordHashMemoryCost => "password hash memory cost",
121            Self::PasswordHashParallelism => "password hash parallelism",
122            Self::PasswordHashSalt => "password hash salt",
123            Self::PasswordHashTimeCost => "password hash time cost",
124            Self::PasswordHashVersion => "password hash version",
125            Self::ProtectedMemory => "protected memory",
126            Self::PublicKey => "public key",
127            Self::SealedBox => "sealed box",
128            Self::Secret => "secret",
129            Self::SecretBox => "secretbox",
130            Self::SecretKey => "secret key",
131            Self::Signature => "signature",
132            Self::SignedMessage => "signed message",
133            Self::Slice => "slice",
134            Self::Subkey => "subkey",
135            Self::Tag => "tag",
136            Self::TimeCost => "time cost",
137            Self::Xof => "XOF",
138        })
139    }
140}
141
142/// A constraint on a byte or buffer length.
143#[derive(Clone, Copy, Debug, Eq, PartialEq)]
144#[non_exhaustive]
145pub enum LengthConstraint {
146    /// The length must equal this value.
147    Exact(usize),
148    /// The length must be at least this value.
149    AtLeast(usize),
150    /// The length must be at most this value.
151    AtMost(usize),
152    /// The length must be within this inclusive range.
153    Between { min: usize, max: usize },
154}
155
156impl Display for LengthConstraint {
157    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
158        match self {
159            Self::Exact(expected) => write!(f, "exactly {expected}"),
160            Self::AtLeast(min) => write!(f, "at least {min}"),
161            Self::AtMost(max) => write!(f, "at most {max}"),
162            Self::Between { min, max } => write!(f, "between {min} and {max} (inclusive)"),
163        }
164    }
165}
166
167/// A constraint on a numeric parameter.
168#[derive(Clone, Copy, Debug, Eq, PartialEq)]
169#[non_exhaustive]
170pub enum ValueConstraint {
171    /// The value must be within this inclusive range.
172    Between { min: u64, max: u64 },
173    /// Only bits present in this mask may be set.
174    AllowedBits { mask: u64 },
175}
176
177impl Display for ValueConstraint {
178    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
179        match self {
180            Self::Between { min, max } => write!(f, "between {min} and {max} (inclusive)"),
181            Self::AllowedBits { mask } => {
182                write!(f, "a value containing only bits from mask 0x{mask:x}")
183            }
184        }
185    }
186}
187
188/// Errors generated by Dryoc.
189///
190/// Variants are structured so callers can handle failures without parsing the
191/// human-readable message. Display text is not part of the API contract.
192#[derive(Debug)]
193#[non_exhaustive]
194pub enum Error {
195    /// Authentication or signature verification failed.
196    AuthenticationFailed,
197
198    /// A byte string or buffer had an invalid length.
199    InvalidLength {
200        /// The input or output whose length was invalid.
201        context: ErrorContext,
202        /// The supplied length.
203        actual: usize,
204        /// The required length constraint.
205        constraint: LengthConstraint,
206    },
207
208    /// A numeric parameter was outside its supported range.
209    InvalidValue {
210        /// The parameter whose value was invalid.
211        context: ErrorContext,
212        /// The supplied value.
213        actual: u64,
214        /// The required value constraint.
215        constraint: ValueConstraint,
216    },
217
218    /// Encoded data was malformed or unsupported.
219    InvalidEncoding {
220        /// The encoded field or format that was invalid.
221        context: ErrorContext,
222    },
223
224    /// A cryptographic key was invalid or unsafe to use.
225    InvalidKey {
226        /// The key whose value was invalid.
227        context: ErrorContext,
228    },
229
230    /// Required data was absent.
231    MissingData {
232        /// The missing field or value.
233        context: ErrorContext,
234    },
235
236    /// The requested operation was invalid for the current state.
237    InvalidState {
238        /// The state or operation that was invalid.
239        context: ErrorContext,
240    },
241
242    /// An arithmetic operation overflowed.
243    ArithmeticOverflow {
244        /// The value being calculated.
245        context: ErrorContext,
246    },
247
248    /// Memory required for an operation could not be allocated.
249    AllocationFailed {
250        /// The input or operation requiring the allocation.
251        context: ErrorContext,
252    },
253
254    /// An operating-system I/O operation failed. Only protected memory, which
255    /// requires the `std` feature, returns it.
256    #[cfg(feature = "std")]
257    Io(std::io::Error),
258}
259
260impl Error {
261    pub(crate) const fn invalid_encoding(context: ErrorContext) -> Self {
262        Self::InvalidEncoding { context }
263    }
264
265    pub(crate) const fn invalid_key(context: ErrorContext) -> Self {
266        Self::InvalidKey { context }
267    }
268
269    pub(crate) const fn missing_data(context: ErrorContext) -> Self {
270        Self::MissingData { context }
271    }
272
273    pub(crate) const fn invalid_state(context: ErrorContext) -> Self {
274        Self::InvalidState { context }
275    }
276
277    pub(crate) const fn arithmetic_overflow(context: ErrorContext) -> Self {
278        Self::ArithmeticOverflow { context }
279    }
280
281    #[cfg(feature = "alloc")]
282    pub(crate) const fn allocation_failed(context: ErrorContext) -> Self {
283        Self::AllocationFailed { context }
284    }
285}
286
287#[cfg(feature = "std")]
288impl From<std::io::Error> for Error {
289    fn from(error: std::io::Error) -> Self {
290        Self::Io(error)
291    }
292}
293
294impl Display for Error {
295    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
296        match self {
297            Self::AuthenticationFailed => f.write_str("authentication failed"),
298            Self::InvalidLength {
299                context,
300                actual,
301                constraint,
302            } => write!(
303                f,
304                "invalid {context} length: expected {constraint}, got {actual}"
305            ),
306            Self::InvalidValue {
307                context,
308                actual,
309                constraint,
310            } => write!(
311                f,
312                "invalid {context} value: expected {constraint}, got {actual}"
313            ),
314            Self::InvalidEncoding { context } => write!(f, "invalid {context} encoding"),
315            Self::InvalidKey { context } => write!(f, "invalid {context}"),
316            Self::MissingData { context } => write!(f, "missing {context}"),
317            Self::InvalidState { context } => write!(f, "invalid {context} state"),
318            Self::ArithmeticOverflow { context } => {
319                write!(f, "arithmetic overflow while calculating {context} length")
320            }
321            Self::AllocationFailed { context } => {
322                write!(f, "unable to allocate memory for {context}")
323            }
324            #[cfg(feature = "std")]
325            Self::Io(error) => write!(f, "I/O error: {error}"),
326        }
327    }
328}
329
330impl core::error::Error for Error {
331    #[cfg(feature = "std")]
332    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
333        match self {
334            Self::Io(error) => Some(error),
335            _ => None,
336        }
337    }
338}
339
340macro_rules! length_error {
341    ($context:expr_2021, $actual:expr_2021,exact $expected:expr_2021) => {
342        crate::error::Error::InvalidLength {
343            context: $context,
344            actual: $actual,
345            constraint: crate::error::LengthConstraint::Exact($expected),
346        }
347    };
348    ($context:expr_2021, $actual:expr_2021,min $min:expr_2021) => {
349        crate::error::Error::InvalidLength {
350            context: $context,
351            actual: $actual,
352            constraint: crate::error::LengthConstraint::AtLeast($min),
353        }
354    };
355    ($context:expr_2021, $actual:expr_2021,max $max:expr_2021) => {
356        crate::error::Error::InvalidLength {
357            context: $context,
358            actual: $actual,
359            constraint: crate::error::LengthConstraint::AtMost($max),
360        }
361    };
362    ($context:expr_2021, $actual:expr_2021,range $min:expr_2021, $max:expr_2021) => {
363        crate::error::Error::InvalidLength {
364            context: $context,
365            actual: $actual,
366            constraint: crate::error::LengthConstraint::Between {
367                min: $min,
368                max: $max,
369            },
370        }
371    };
372}
373
374macro_rules! validate_value {
375    ($min:expr_2021, $max:expr_2021, $value:expr_2021, $context:expr_2021) => {
376        if !($min..=$max).contains(&$value) {
377            return Err(crate::error::Error::InvalidValue {
378                context: $context,
379                actual: $value as u64,
380                constraint: crate::error::ValueConstraint::Between {
381                    min: $min as u64,
382                    max: $max as u64,
383                },
384            });
385        }
386    };
387}
388
389macro_rules! validate_length {
390    (exact $expected:expr_2021, $value:expr_2021, $context:expr_2021) => {
391        if $value != $expected {
392            return Err(length_error!($context, $value, exact $expected));
393        }
394    };
395    (min $min:expr_2021, $value:expr_2021, $context:expr_2021) => {
396        if $value < $min {
397            return Err(length_error!($context, $value, min $min));
398        }
399    };
400    (max $max:expr_2021, $value:expr_2021, $context:expr_2021) => {
401        if $value > $max {
402            return Err(length_error!($context, $value, max $max));
403        }
404    };
405    ($min:expr_2021, $max:expr_2021, $value:expr_2021, $context:expr_2021) => {
406        if !($min..=$max).contains(&$value) {
407            return Err(length_error!($context, $value, range $min, $max));
408        }
409    };
410}
411
412#[cfg(test)]
413mod tests {
414    use super::*;
415    use crate::test_prelude::*;
416
417    #[test]
418    fn contexts_have_clear_human_readable_names() {
419        let cases = [
420            (ErrorContext::AssociatedData, "associated data"),
421            (ErrorContext::AeadCiphertext, "AEAD ciphertext"),
422            (ErrorContext::AeadEnvelope, "AEAD envelope"),
423            (ErrorContext::AuthenticationTag, "authentication tag"),
424            (ErrorContext::Blake2bKey, "BLAKE2b key"),
425            (ErrorContext::Blake2bOutput, "BLAKE2b output"),
426            (ErrorContext::Blake2b, "BLAKE2b"),
427            (ErrorContext::Box, "box"),
428            (ErrorContext::Ciphertext, "ciphertext"),
429            (ErrorContext::Curve25519PublicKey, "Curve25519 public key"),
430            (ErrorContext::Data, "data"),
431            (ErrorContext::Ed25519PublicKey, "Ed25519 public key"),
432            (ErrorContext::EphemeralPublicKey, "ephemeral public key"),
433            (ErrorContext::MemoryCost, "memory cost"),
434            (ErrorContext::MemoryLimit, "memory limit"),
435            (ErrorContext::Message, "message"),
436            (ErrorContext::Nonce, "nonce"),
437            (ErrorContext::OperationsLimit, "operations limit"),
438            (ErrorContext::Output, "output"),
439            (ErrorContext::Parallelism, "parallelism"),
440            (ErrorContext::Password, "password"),
441            (ErrorContext::PasswordHash, "password hash"),
442            (
443                ErrorContext::PasswordHashAlgorithm,
444                "password hash algorithm",
445            ),
446            (
447                ErrorContext::PasswordHashMemoryCost,
448                "password hash memory cost",
449            ),
450            (
451                ErrorContext::PasswordHashParallelism,
452                "password hash parallelism",
453            ),
454            (ErrorContext::PasswordHashSalt, "password hash salt"),
455            (
456                ErrorContext::PasswordHashTimeCost,
457                "password hash time cost",
458            ),
459            (ErrorContext::PasswordHashVersion, "password hash version"),
460            (ErrorContext::ProtectedMemory, "protected memory"),
461            (ErrorContext::PublicKey, "public key"),
462            (ErrorContext::SealedBox, "sealed box"),
463            (ErrorContext::Secret, "secret"),
464            (ErrorContext::SecretBox, "secretbox"),
465            (ErrorContext::SecretKey, "secret key"),
466            (ErrorContext::Signature, "signature"),
467            (ErrorContext::SignedMessage, "signed message"),
468            (ErrorContext::Slice, "slice"),
469            (ErrorContext::Subkey, "subkey"),
470            (ErrorContext::Tag, "tag"),
471            (ErrorContext::TimeCost, "time cost"),
472        ];
473
474        for (context, expected) in cases {
475            assert_eq!(context.to_string(), expected);
476        }
477    }
478
479    #[test]
480    fn constraints_describe_their_requirements() {
481        let length_cases = [
482            (LengthConstraint::Exact(4), "exactly 4"),
483            (LengthConstraint::AtLeast(4), "at least 4"),
484            (LengthConstraint::AtMost(4), "at most 4"),
485            (
486                LengthConstraint::Between { min: 2, max: 4 },
487                "between 2 and 4 (inclusive)",
488            ),
489        ];
490        for (constraint, expected) in length_cases {
491            assert_eq!(constraint.to_string(), expected);
492        }
493
494        let value_cases = [
495            (
496                ValueConstraint::Between { min: 2, max: 4 },
497                "between 2 and 4 (inclusive)",
498            ),
499            (
500                ValueConstraint::AllowedBits { mask: 0x3 },
501                "a value containing only bits from mask 0x3",
502            ),
503        ];
504        for (constraint, expected) in value_cases {
505            assert_eq!(constraint.to_string(), expected);
506        }
507    }
508
509    #[test]
510    fn display_is_human_readable_without_source_locations() {
511        let cases = [
512            (Error::AuthenticationFailed, "authentication failed"),
513            (
514                Error::InvalidLength {
515                    context: ErrorContext::Nonce,
516                    actual: 12,
517                    constraint: LengthConstraint::Exact(24),
518                },
519                "invalid nonce length: expected exactly 24, got 12",
520            ),
521            (
522                Error::InvalidLength {
523                    context: ErrorContext::Blake2bOutput,
524                    actual: 0,
525                    constraint: LengthConstraint::Between { min: 1, max: 64 },
526                },
527                "invalid BLAKE2b output length: expected between 1 and 64 (inclusive), got 0",
528            ),
529            (
530                Error::InvalidValue {
531                    context: ErrorContext::Parallelism,
532                    actual: 8,
533                    constraint: ValueConstraint::Between { min: 1, max: 4 },
534                },
535                "invalid parallelism value: expected between 1 and 4 (inclusive), got 8",
536            ),
537            (
538                Error::InvalidValue {
539                    context: ErrorContext::Tag,
540                    actual: 128,
541                    constraint: ValueConstraint::AllowedBits { mask: 3 },
542                },
543                "invalid tag value: expected a value containing only bits from mask 0x3, got 128",
544            ),
545            (
546                Error::InvalidEncoding {
547                    context: ErrorContext::PasswordHashSalt,
548                },
549                "invalid password hash salt encoding",
550            ),
551            (
552                Error::InvalidKey {
553                    context: ErrorContext::Ed25519PublicKey,
554                },
555                "invalid Ed25519 public key",
556            ),
557            (
558                Error::MissingData {
559                    context: ErrorContext::EphemeralPublicKey,
560                },
561                "missing ephemeral public key",
562            ),
563            (
564                Error::InvalidState {
565                    context: ErrorContext::Blake2b,
566                },
567                "invalid BLAKE2b state",
568            ),
569            (
570                Error::ArithmeticOverflow {
571                    context: ErrorContext::Ciphertext,
572                },
573                "arithmetic overflow while calculating ciphertext length",
574            ),
575            (
576                Error::AllocationFailed {
577                    context: ErrorContext::MemoryCost,
578                },
579                "unable to allocate memory for memory cost",
580            ),
581        ];
582
583        for (error, expected) in cases {
584            assert_eq!(error.to_string(), expected);
585        }
586    }
587
588    #[test]
589    fn debug_is_structured_and_does_not_include_internal_source_locations() {
590        let error = Error::InvalidLength {
591            context: ErrorContext::Ciphertext,
592            actual: 7,
593            constraint: LengthConstraint::AtLeast(16),
594        };
595
596        assert_eq!(
597            format!("{error:?}"),
598            "InvalidLength { context: Ciphertext, actual: 7, constraint: AtLeast(16) }"
599        );
600    }
601
602    #[test]
603    #[cfg(feature = "std")]
604    fn wrapped_errors_preserve_their_source() {
605        use core::error::Error as _;
606
607        let error = Error::from(std::io::Error::new(
608            std::io::ErrorKind::PermissionDenied,
609            "access denied",
610        ));
611        assert_eq!(error.to_string(), "I/O error: access denied");
612        let debug = format!("{error:?}");
613        assert!(debug.contains("Io"));
614        assert!(debug.contains("PermissionDenied"));
615        assert!(debug.contains("access denied"));
616        assert!(error.source().is_some());
617        assert!(Error::AuthenticationFailed.source().is_none());
618    }
619}