Skip to main content

crypto_hkdf/
derive.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 hkdf::Hkdf;
7#[cfg(feature = "sha3")]
8use hkdf::SimpleHkdf;
9use sha2::{Sha256, Sha384};
10#[cfg(feature = "sha3")]
11use sha3::Sha3_256;
12
13use crate::material::{HkdfInfo, HkdfInputKeyMaterial, HkdfOutput, HkdfSalt};
14use crate::policy::{DomainKeyPurpose, DomainTag, HkdfSuite};
15
16/// Parameters for a single HKDF extract-and-expand derivation.
17pub struct DeriveRequest<'a> {
18    /// Hash suite to use.
19    pub suite: HkdfSuite,
20    /// Input keying material to extract from.
21    pub ikm: &'a HkdfInputKeyMaterial,
22    /// Optional salt for the extract step.
23    pub salt: Option<&'a HkdfSalt>,
24    /// Context/application-binding `info` for the expand step.
25    pub info: &'a HkdfInfo,
26}
27
28/// Run HKDF-Expand for `request`, producing `N` bytes of output.
29///
30/// Errors if the input keying material is empty or `N` is zero.
31///
32pub fn derive<const N: usize>(request: &DeriveRequest<'_>) -> Result<HkdfOutput<N>, CryptoError> {
33    if request.ikm.as_bytes().is_empty() {
34        return Err(CryptoError::Hkdf {
35            hash: request.suite.hash(),
36            kind: HkdfFailureKind::InvalidIkmLength,
37        });
38    }
39
40    if N == 0 {
41        return Err(CryptoError::Hkdf {
42            hash: request.suite.hash(),
43            kind: HkdfFailureKind::InvalidOutputLength,
44        });
45    }
46
47    let mut output = [0u8; N];
48
49    match request.suite {
50        HkdfSuite::Sha2_256 => {
51            let hkdf =
52                Hkdf::<Sha256>::new(request.salt.map(HkdfSalt::as_bytes), request.ikm.as_bytes());
53            hkdf.expand(request.info.as_bytes(), &mut output)
54                .map_err(|_| CryptoError::Hkdf {
55                    hash: HkdfHash::Sha2_256,
56                    kind: HkdfFailureKind::ExpandFailed,
57                })?;
58        }
59        HkdfSuite::Sha2_384 => {
60            let hkdf =
61                Hkdf::<Sha384>::new(request.salt.map(HkdfSalt::as_bytes), request.ikm.as_bytes());
62            hkdf.expand(request.info.as_bytes(), &mut output)
63                .map_err(|_| CryptoError::Hkdf {
64                    hash: HkdfHash::Sha2_384,
65                    kind: HkdfFailureKind::ExpandFailed,
66                })?;
67        }
68        HkdfSuite::Sha3_256 => {
69            #[cfg(feature = "sha3")]
70            {
71                // The generic RustCrypto HKDF implementation supports SHA3-256
72                // directly. Keeping this path aligned with the SHA-2 suites
73                // removes a parallel HMAC implementation and inherits the
74                // workspace's zeroizing digest-state feature policy.
75                let hkdf = SimpleHkdf::<Sha3_256>::new(
76                    request.salt.map(HkdfSalt::as_bytes),
77                    request.ikm.as_bytes(),
78                );
79                hkdf.expand(request.info.as_bytes(), &mut output)
80                    .map_err(|_| CryptoError::Hkdf {
81                        hash: HkdfHash::Sha3_256,
82                        kind: HkdfFailureKind::ExpandFailed,
83                    })?;
84            }
85
86            #[cfg(not(feature = "sha3"))]
87            {
88                return Err(CryptoError::Unsupported);
89            }
90        }
91    }
92
93    Ok(HkdfOutput::from_array(output))
94}
95
96/// Derive a 32-byte domain-separated key using SHA3-256 HKDF.
97///
98/// Builds the `info` as `reallyme/crypto/hkdf/v1/<purpose>/<domain_tag>` and
99/// expands it; errors on length overflow or an underlying HKDF failure.
100/// Returns [`CryptoError::Unsupported`] unless the crate's `sha3` feature is
101/// enabled.
102pub fn derive_domain_key_32(
103    ikm: &HkdfInputKeyMaterial,
104    salt: Option<&HkdfSalt>,
105    purpose: DomainKeyPurpose,
106    domain_tag: &DomainTag,
107) -> Result<HkdfOutput<32>, CryptoError> {
108    const PREFIX: &[u8] = b"reallyme/crypto/hkdf/v1/";
109    const SEPARATOR: &[u8] = b"/";
110
111    let capacity = PREFIX
112        .len()
113        .checked_add(purpose.as_bytes().len())
114        .and_then(|value| value.checked_add(SEPARATOR.len()))
115        .and_then(|value| value.checked_add(domain_tag.as_bytes().len()))
116        .ok_or(CryptoError::Hkdf {
117            hash: HkdfHash::Sha3_256,
118            kind: HkdfFailureKind::LengthOverflow,
119        })?;
120
121    let mut info = Vec::<u8>::with_capacity(capacity);
122    info.extend_from_slice(PREFIX);
123    info.extend_from_slice(purpose.as_bytes());
124    info.extend_from_slice(SEPARATOR);
125    info.extend_from_slice(domain_tag.as_bytes());
126
127    let hkdf_info = HkdfInfo::from_vec(info);
128    derive::<32>(&DeriveRequest {
129        suite: HkdfSuite::Sha3_256,
130        ikm,
131        salt,
132        info: &hkdf_info,
133    })
134}