Skip to main content

tf_types/
crypto.rs

1//! Crypto primitives — thin wrappers over reviewed crates.
2//!
3//! Supported:
4//!   - ed25519 signing / verifying (via `ed25519-dalek`).
5//!   - SHA-256 and BLAKE3 hashing.
6//!
7//! Post-quantum ML-DSA is a Phase 3+ addition and is reserved in the
8//! `SignatureEnvelope` schema today. No custom crypto is introduced in
9//! this module — everything is a thin adapter.
10
11use crate::encoding::STANDARD as B64;
12use ed25519_dalek::{Signature, Signer as _, SigningKey, Verifier as _, VerifyingKey};
13use sha2::{Digest, Sha256};
14
15#[derive(Debug, thiserror::Error, PartialEq, Eq)]
16pub enum CryptoError {
17    #[error("invalid ed25519 private key: expected 32 raw bytes, got {0}")]
18    PrivateKeyLength(usize),
19    #[error("invalid ed25519 public key: expected 32 raw bytes, got {0}")]
20    PublicKeyLength(usize),
21    #[error("invalid ed25519 signature: expected 64 raw bytes, got {0}")]
22    SignatureLength(usize),
23    #[error("signature verification failed")]
24    BadSignature,
25    #[error("invalid public key encoding")]
26    BadPublicKey,
27    #[error("invalid base64: {0}")]
28    BadBase64(String),
29    #[error("unknown algorithm: {0}")]
30    UnknownAlgorithm(String),
31    #[error("{0}")]
32    Generic(String),
33}
34
35/// ED25519 signing key.
36pub struct Ed25519Signer {
37    inner: SigningKey,
38}
39
40impl Ed25519Signer {
41    pub fn from_bytes(seed: &[u8; 32]) -> Self {
42        Ed25519Signer {
43            inner: SigningKey::from_bytes(seed),
44        }
45    }
46
47    pub fn generate<R: rand::RngCore + rand::CryptoRng>(rng: &mut R) -> Self {
48        Ed25519Signer {
49            inner: SigningKey::generate(rng),
50        }
51    }
52
53    pub fn public_key_bytes(&self) -> [u8; 32] {
54        self.inner.verifying_key().to_bytes()
55    }
56
57    pub fn sign(&self, msg: &[u8]) -> [u8; 64] {
58        self.inner.sign(msg).to_bytes()
59    }
60}
61
62/// Verify an ed25519 signature.
63pub fn ed25519_verify(public_key: &[u8], msg: &[u8], signature: &[u8]) -> Result<(), CryptoError> {
64    let pk_bytes: &[u8; 32] = public_key
65        .try_into()
66        .map_err(|_| CryptoError::PublicKeyLength(public_key.len()))?;
67    let sig_bytes: &[u8; 64] = signature
68        .try_into()
69        .map_err(|_| CryptoError::SignatureLength(signature.len()))?;
70    let vk = VerifyingKey::from_bytes(pk_bytes).map_err(|_| CryptoError::BadPublicKey)?;
71    let sig = Signature::from_bytes(sig_bytes);
72    vk.verify(msg, &sig).map_err(|_| CryptoError::BadSignature)
73}
74
75/// SHA-256 of the input, returned as `"sha256:<hex>"`.
76pub fn sha256_hashref(bytes: &[u8]) -> String {
77    let digest = Sha256::digest(bytes);
78    format!("sha256:{}", hex(&digest))
79}
80
81/// BLAKE3 of the input, returned as `"blake3:<hex>"`.
82pub fn blake3_hashref(bytes: &[u8]) -> String {
83    let digest = blake3::hash(bytes);
84    format!("blake3:{}", hex(digest.as_bytes()))
85}
86
87pub fn hex(bytes: &[u8]) -> String {
88    let mut s = String::with_capacity(bytes.len() * 2);
89    for b in bytes {
90        s.push_str(&format!("{:02x}", b));
91    }
92    s
93}
94
95/// Parse `sha256:<hex>` back into raw bytes.
96pub fn parse_hashref(s: &str) -> Result<(String, Vec<u8>), CryptoError> {
97    let (algo, hex_part) = s
98        .split_once(':')
99        .ok_or_else(|| CryptoError::UnknownAlgorithm(s.to_owned()))?;
100    let mut out = Vec::with_capacity(hex_part.len() / 2);
101    let bytes = hex_part.as_bytes();
102    let mut i = 0;
103    while i < bytes.len() {
104        if i + 1 >= bytes.len() {
105            return Err(CryptoError::BadBase64("odd hex length".into()));
106        }
107        let hi = from_hex(bytes[i]).ok_or_else(|| CryptoError::BadBase64("non-hex char".into()))?;
108        let lo =
109            from_hex(bytes[i + 1]).ok_or_else(|| CryptoError::BadBase64("non-hex char".into()))?;
110        out.push((hi << 4) | lo);
111        i += 2;
112    }
113    Ok((algo.to_owned(), out))
114}
115
116fn from_hex(b: u8) -> Option<u8> {
117    match b {
118        b'0'..=b'9' => Some(b - b'0'),
119        b'a'..=b'f' => Some(b - b'a' + 10),
120        b'A'..=b'F' => Some(b - b'A' + 10),
121        _ => None,
122    }
123}
124
125/// Base64 encode / decode helpers for signature payloads.
126pub fn b64encode(bytes: &[u8]) -> String {
127    B64.encode(bytes)
128}
129
130pub fn b64decode(s: &str) -> Result<Vec<u8>, CryptoError> {
131    B64.decode(s.as_bytes())
132        .map_err(|e| CryptoError::BadBase64(e.to_string()))
133}
134
135// ---------- X25519 ----------
136
137pub struct X25519KeyPair {
138    pub private: [u8; 32],
139    pub public: [u8; 32],
140}
141
142pub fn x25519_generate<R: rand::RngCore + rand::CryptoRng>(rng: &mut R) -> X25519KeyPair {
143    let secret = x25519_dalek::StaticSecret::random_from_rng(rng);
144    let public = x25519_dalek::PublicKey::from(&secret);
145    X25519KeyPair {
146        private: secret.to_bytes(),
147        public: public.to_bytes(),
148    }
149}
150
151pub fn x25519_from_bytes(seed: &[u8; 32]) -> X25519KeyPair {
152    let secret = x25519_dalek::StaticSecret::from(*seed);
153    let public = x25519_dalek::PublicKey::from(&secret);
154    X25519KeyPair {
155        private: secret.to_bytes(),
156        public: public.to_bytes(),
157    }
158}
159
160pub fn x25519_diffie_hellman(private: &[u8; 32], peer_public: &[u8; 32]) -> [u8; 32] {
161    let secret = x25519_dalek::StaticSecret::from(*private);
162    let peer = x25519_dalek::PublicKey::from(*peer_public);
163    secret.diffie_hellman(&peer).to_bytes()
164}
165
166// ---------- HKDF-SHA256 ----------
167
168pub fn hkdf_sha256(input_key: &[u8], salt: &[u8], info: &[u8], output_len: usize) -> Vec<u8> {
169    let hk = hkdf::Hkdf::<Sha256>::new(Some(salt), input_key);
170    let mut out = vec![0u8; output_len];
171    hk.expand(info, &mut out).expect("output_len <= 255*32");
172    out
173}
174
175// ---------- ChaCha20-Poly1305-IETF ----------
176
177#[derive(Debug, thiserror::Error, PartialEq, Eq)]
178pub enum AeadError {
179    #[error("aead key must be 32 bytes")]
180    BadKey,
181    #[error("aead nonce must be 12 bytes")]
182    BadNonce,
183    #[error("aead authentication failed")]
184    AuthFailed,
185}
186
187pub fn chacha20poly1305_encrypt(
188    key: &[u8; 32],
189    nonce: &[u8; 12],
190    aad: &[u8],
191    plaintext: &[u8],
192) -> Vec<u8> {
193    use chacha20poly1305::aead::{Aead, KeyInit, Payload};
194    use chacha20poly1305::ChaCha20Poly1305;
195    let cipher = ChaCha20Poly1305::new_from_slice(key).expect("32-byte key");
196    cipher
197        .encrypt(
198            chacha20poly1305::Nonce::from_slice(nonce),
199            Payload {
200                msg: plaintext,
201                aad,
202            },
203        )
204        .expect("encrypt")
205}
206
207pub fn chacha20poly1305_decrypt(
208    key: &[u8; 32],
209    nonce: &[u8; 12],
210    aad: &[u8],
211    ciphertext: &[u8],
212) -> Result<Vec<u8>, AeadError> {
213    use chacha20poly1305::aead::{Aead, KeyInit, Payload};
214    use chacha20poly1305::ChaCha20Poly1305;
215    let cipher = ChaCha20Poly1305::new_from_slice(key).map_err(|_| AeadError::BadKey)?;
216    cipher
217        .decrypt(
218            chacha20poly1305::Nonce::from_slice(nonce),
219            Payload {
220                msg: ciphertext,
221                aad,
222            },
223        )
224        .map_err(|_| AeadError::AuthFailed)
225}