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}