Skip to main content

dryoc/
error.rs

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