Skip to main content

dstu_core/
crypto_auth.rs

1//! `crypto_auth`/`crypto_onetimeauth` equivalent (`docs/dstu-crypto-project.md` "Mapping onto the
2//! libsodium API", `docs/TASKS.md` T-105, roadmap Step 3 item 2 - `docs/DECISIONS.md` D-66) - a thin
3//! libsodium-ergonomics wrapper over [`crate::hazmat::kupyna_kmac::Kupyna256Kmac`].
4//!
5//! Two departures from the raw `hazmat` API, both following D-47's "delete the knob" criterion
6//! (the same rule `crypto_secretbox` applied to Kalyna's five variants, D-51):
7//! - **Only the 256-bit MAC size is exposed** - `hazmat::kupyna_kmac` also has `Kupyna384Kmac`/
8//!   `Kupyna512Kmac`, matching this crate's existing default-to-256-bit convention
9//!   (`crypto_secretbox`'s `Kalyna256_256Gcm`, `crypto_sign`'s internal `Kupyna256` message hash).
10//!   The other two sizes remain available at `hazmat::kupyna_kmac` for callers who need them.
11//! - **The key is an opaque, `Zeroize`-on-drop [`Key`] type**, not a raw `&[u8]` - this also
12//!   forecloses `hazmat::kupyna_kmac::KmacError::WrongKeyLength` at this layer entirely: `Key` can
13//!   only ever be exactly 32 bytes (`from_bytes([u8; 32])` or [`Key::generate`]), so [`auth`] is
14//!   infallible and [`verify`]'s error type has only one variant. This is a type-signature
15//!   foreclosure, not an untested code path - see `docs/DECISIONS.md` D-66.
16//!
17//! Provenance is otherwise identical to the `hazmat` layer: dual-oracle-cited, not yet confirmed
18//! against the primary DSTU 7564:2014 text (D-44).
19//!
20//! # Example
21//!
22//! A MAC lets two parties who share a secret key confirm a message hasn't been altered in transit.
23//! Unlike a signature, either party can both create and check a tag (there's no "public" half), so
24//! this only proves "someone who has the key", not "specifically you".
25//!
26//! ```rust
27//! use dstu_core::crypto_auth::{auth, verify, Key};
28//!
29//! let key = Key::generate().expect("OS CSPRNG should not fail");
30//! let message = b"a message both parties want to confirm is unmodified";
31//!
32//! let tag = auth(&key, message);
33//! assert!(verify(&key, message, &tag).is_ok());
34//!
35//! // A tampered message, or the wrong key, is rejected.
36//! assert!(verify(&key, b"a different message", &tag).is_err());
37//! ```
38
39use crate::hazmat::kupyna_kmac::{KmacError, Kupyna256Kmac};
40use core::fmt;
41use zeroize::Zeroize;
42
43/// A `crypto_auth` key. Always exactly 32 bytes - [`Kupyna256Kmac`]'s fixed MAC/key length (see
44/// the module doc).
45pub struct Key([u8; 32]);
46
47impl Drop for Key {
48    fn drop(&mut self) {
49        self.0.zeroize();
50    }
51}
52
53impl Key {
54    /// Generates a fresh key from the OS CSPRNG - libsodium's `crypto_auth_keygen` equivalent.
55    ///
56    /// # Errors
57    ///
58    /// Returns [`crate::randombytes::RandomError`] if the OS CSPRNG fails.
59    #[cfg(any(feature = "std", feature = "getrandom"))]
60    pub fn generate() -> Result<Self, crate::randombytes::RandomError> {
61        let mut bytes = [0u8; 32];
62        crate::randombytes::randombytes_buf(&mut bytes)?;
63        Ok(Key(bytes))
64    }
65
66    #[must_use]
67    pub fn from_bytes(bytes: [u8; 32]) -> Self {
68        Key(bytes)
69    }
70
71    #[must_use]
72    pub fn as_bytes(&self) -> &[u8; 32] {
73        &self.0
74    }
75}
76
77/// `verify` can fail only one way: [`auth`] cannot fail at all (see the module doc's "delete the
78/// knob" section - `Key`'s fixed length forecloses `hazmat`'s `WrongKeyLength` case here).
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub struct TagMismatch;
81
82impl fmt::Display for TagMismatch {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        write!(f, "authentication failed")
85    }
86}
87
88impl core::error::Error for TagMismatch {}
89
90/// Computes the MAC of `message` under `key`.
91#[must_use]
92pub fn auth(key: &Key, message: &[u8]) -> [u8; 32] {
93    let Ok(tag) = Kupyna256Kmac::mac(key.as_bytes(), message) else {
94        unreachable!("Key::as_bytes() is always exactly 32 bytes, Kupyna256Kmac's own mac_len")
95    };
96    tag
97}
98
99/// Verifies `tag` against `message` under `key`, in constant time
100/// ([`crate::hazmat::kupyna_kmac`]'s own `subtle::ConstantTimeEq` comparison).
101///
102/// # Errors
103///
104/// Returns [`TagMismatch`] if `tag` does not match.
105pub fn verify(key: &Key, message: &[u8], tag: &[u8; 32]) -> Result<(), TagMismatch> {
106    Kupyna256Kmac::verify(key.as_bytes(), message, tag).map_err(|e| match e {
107        KmacError::TagMismatch => TagMismatch,
108        KmacError::WrongKeyLength => {
109            unreachable!("Key::as_bytes() is always exactly 32 bytes, Kupyna256Kmac's own mac_len")
110        }
111    })
112}