Skip to main content

crypto_hkdf/
policy.rs

1// SPDX-FileCopyrightText: Copyright © 2026 ReallyMe LLC. All rights reserved
2//
3// SPDX-License-Identifier: Apache-2.0
4
5use crypto_core::{CryptoError, HkdfFailureKind, HkdfHash};
6use zeroize::{Zeroize, ZeroizeOnDrop};
7
8/// Selects the hash function backing an HKDF operation.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub enum HkdfSuite {
11    /// HKDF using SHA-256.
12    Sha2_256,
13    /// HKDF using SHA-384.
14    Sha2_384,
15    /// HKDF using SHA3-256.
16    Sha3_256,
17}
18
19impl HkdfSuite {
20    /// Return the [`HkdfHash`] backing this suite.
21    pub const fn hash(self) -> HkdfHash {
22        match self {
23            Self::Sha2_256 => HkdfHash::Sha2_256,
24            Self::Sha2_384 => HkdfHash::Sha2_384,
25            Self::Sha3_256 => HkdfHash::Sha3_256,
26        }
27    }
28}
29
30/// Named purpose that binds a derived domain key to a specific use.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
32pub enum DomainKeyPurpose {
33    /// Key used to encrypt content with an AEAD.
34    AeadContentKey,
35    /// Key used to wrap (encrypt) other keys with an AEAD.
36    AeadWrapKey,
37    /// Key used to produce authentication proofs.
38    AuthProofKey,
39    /// Key used to commit to a manifest.
40    ManifestCommitmentKey,
41}
42
43impl DomainKeyPurpose {
44    pub(crate) const fn as_bytes(self) -> &'static [u8] {
45        match self {
46            Self::AeadContentKey => b"aead_content_key",
47            Self::AeadWrapKey => b"aead_wrap_key",
48            Self::AuthProofKey => b"auth_proof_key",
49            Self::ManifestCommitmentKey => b"manifest_commitment_key",
50        }
51    }
52}
53
54/// Validated domain-separation tag appended to derived-key `info`.
55///
56/// A tag may identify a tenant, account, or protocol scope. It is therefore
57/// redacted in diagnostics and clears its owned bytes on drop.
58#[derive(Zeroize, ZeroizeOnDrop, PartialEq, Eq)]
59pub struct DomainTag {
60    bytes: Vec<u8>,
61}
62
63impl core::fmt::Debug for DomainTag {
64    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
65        formatter.write_str("DomainTag(<redacted>)")
66    }
67}
68
69impl DomainTag {
70    /// Build a domain tag, requiring 1..=48 bytes drawn only from lowercase
71    /// ASCII letters, digits, and `/`, `_`, or `-`; otherwise returns an error.
72    pub fn from_slice(input: &[u8]) -> Result<Self, CryptoError> {
73        if input.is_empty() || input.len() > 48 {
74            return Err(CryptoError::Hkdf {
75                hash: HkdfHash::Sha3_256,
76                kind: HkdfFailureKind::InvalidDomainTagLength,
77            });
78        }
79
80        for byte in input {
81            let is_lower_alpha = byte.is_ascii_lowercase();
82            let is_digit = byte.is_ascii_digit();
83            let is_punctuation = matches!(*byte, b'/' | b'_' | b'-');
84            if !(is_lower_alpha || is_digit || is_punctuation) {
85                return Err(CryptoError::Hkdf {
86                    hash: HkdfHash::Sha3_256,
87                    kind: HkdfFailureKind::InvalidDomainTagByte,
88                });
89            }
90        }
91
92        Ok(Self {
93            bytes: input.to_vec(),
94        })
95    }
96
97    /// Borrow the domain tag bytes.
98    pub fn as_bytes(&self) -> &[u8] {
99        &self.bytes
100    }
101}