ubl_crypto/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3//! Crypto helpers: Ed25519 keypairs/KID, BLAKE3 hashing, and HMAC utilities.
4
5use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
6use base64::Engine;
7use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
8use hmac::{Hmac, Mac};
9use rand_core::OsRng;
10use sha2::Sha256;
11use thiserror::Error;
12use zeroize::{Zeroize, ZeroizeOnDrop};
13
14/// Erros de crypto.
15#[derive(Debug, Error)]
16pub enum AtomicCryptoError {
17    /// Base64 decoding error.
18    #[error("base64 decode: {0}")]
19    Base64(#[from] base64::DecodeError),
20    /// Signature verification failed.
21    #[error("invalid signature")]
22    InvalidSig,
23    /// HMAC verification failed.
24    #[error("hmac verify failed")]
25    HmacVerify,
26    /// did:key payload is malformed.
27    #[error("bad did:key material")]
28    BadDidKey,
29}
30
31/// Chave secreta (zera memória ao sair).
32#[derive(Zeroize, ZeroizeOnDrop, Clone)]
33pub struct SecretKey(pub [u8; 32]);
34
35impl SecretKey {
36    /// Derives the Ed25519 verifying key from this secret key.
37    #[must_use]
38    pub fn verifying_key(&self) -> VerifyingKey {
39        SigningKey::from_bytes(&self.0).verifying_key()
40    }
41}
42
43/// Par de chaves (conveniência).
44pub struct Keypair {
45    /// Secret key; zeroized on drop.
46    pub sk: SecretKey,
47    /// Verifying/public key.
48    pub vk: VerifyingKey,
49}
50impl Keypair {
51    /// Gera par de chaves.
52    #[must_use]
53    pub fn generate() -> Self {
54        let sk = SigningKey::generate(&mut OsRng);
55        Self {
56            sk: SecretKey(sk.to_bytes()),
57            vk: sk.verifying_key(),
58        }
59    }
60    /// Signing key helper.
61    #[must_use]
62    pub fn signing_key(&self) -> SigningKey {
63        SigningKey::from_bytes(&self.sk.0)
64    }
65}
66
67/// Base64 URL-safe (sem padding).
68#[must_use]
69pub fn b64_encode(b: &[u8]) -> String {
70    B64.encode(b)
71}
72/// Decodifica Base64 URL-safe (sem padding).
73///
74/// # Errors
75///
76/// Retorna erro se a string não estiver em Base64 URL-safe sem padding.
77pub fn b64_decode(s: &str) -> Result<Vec<u8>, AtomicCryptoError> {
78    Ok(B64.decode(s)?)
79}
80/// Hash BLAKE3 → hex.
81#[must_use]
82pub fn blake3_hex(data: &[u8]) -> String {
83    blake3::hash(data).to_hex().to_string()
84}
85
86/// Identificador de chave (v1).
87#[must_use]
88pub fn key_id_v1(vk: &VerifyingKey) -> String {
89    let h = blake3::hash(vk.as_bytes());
90    format!("kid:v1:{}", B64.encode(h.as_bytes()))
91}
92/// Identificador de chave (v2 com versão dummy).
93#[must_use]
94pub fn key_id_v2(vk: &VerifyingKey) -> String {
95    let h = blake3::hash(vk.as_bytes());
96    format!("kid:v2:0001:{}", B64.encode(h.as_bytes()))
97}
98
99/// Assina um CID (hex) usando Ed25519.
100#[must_use]
101pub fn sign_cid_hex(sk: &SecretKey, cid_hex: &str) -> [u8; 64] {
102    let sig: Signature = SigningKey::from_bytes(&sk.0).sign(cid_hex.as_bytes());
103    sig.to_bytes()
104}
105/// Verifica assinatura de CID (hex).
106#[must_use]
107pub fn verify_cid_hex(vk: &VerifyingKey, cid_hex: &str, sig: &[u8]) -> bool {
108    Signature::from_slice(sig).is_ok_and(|parsed| vk.verify(cid_hex.as_bytes(), &parsed).is_ok())
109}
110
111/// HMAC (SHA-256) - retorna base64url sem padding.
112///
113/// # Panics
114///
115/// Pode entrar em pânico se a chave HMAC tiver tamanho inválido para SHA-256.
116#[must_use]
117pub fn hmac_sign(key: &[u8], data: &[u8]) -> String {
118    let mut mac: Hmac<Sha256> = Hmac::new_from_slice(key).expect("HMAC key");
119    mac.update(data);
120    let out = mac.finalize().into_bytes();
121    b64_encode(&out)
122}
123/// Verifica HMAC (base64url sem padding).
124///
125/// # Errors
126///
127/// Retorna `AtomicCryptoError::Base64` se a tag não for base64 válida ou `AtomicCryptoError::HmacVerify` se a verificação falhar.
128///
129/// # Panics
130///
131/// Pode entrar em pânico se a chave HMAC tiver tamanho inválido para SHA-256.
132pub fn hmac_verify(key: &[u8], data: &[u8], tag_b64: &str) -> Result<(), AtomicCryptoError> {
133    let want = b64_decode(tag_b64)?;
134    let mut mac: Hmac<Sha256> = Hmac::new_from_slice(key).expect("HMAC key");
135    mac.update(data);
136    mac.verify_slice(&want)
137        .map_err(|_| AtomicCryptoError::HmacVerify)
138}
139
140/// did:key (ed25519) encoding: returns did:key:z.... (multibase z + multicodec 0xED01 + pk).
141#[must_use]
142pub fn did_key_encode_ed25519(vk: &VerifyingKey) -> String {
143    // multicodec for Ed25519 public key: 0xED 0x01
144    let mut data = vec![0xED, 0x01];
145    data.extend_from_slice(vk.as_bytes());
146    let mut out = String::from("did:key:z");
147    // base58btc; minimal inline implementation via bs58
148    out.push_str(&bs58::encode(data).into_string());
149    out
150}
151/// did:key decode → `VerifyingKey`
152///
153/// # Errors
154///
155/// Retorna erro se o DID não estiver no formato esperado ou o material não for válido.
156pub fn did_key_decode_ed25519(did: &str) -> Result<VerifyingKey, AtomicCryptoError> {
157    let p = did
158        .strip_prefix("did:key:z")
159        .ok_or(AtomicCryptoError::BadDidKey)?;
160    let bytes = bs58::decode(p)
161        .into_vec()
162        .map_err(|_| AtomicCryptoError::BadDidKey)?;
163    if bytes.len() != 34 || bytes[0] != 0xED || bytes[1] != 0x01 {
164        return Err(AtomicCryptoError::BadDidKey);
165    }
166    let pk: [u8; 32] = bytes[2..]
167        .try_into()
168        .map_err(|_| AtomicCryptoError::BadDidKey)?;
169    VerifyingKey::from_bytes(&pk).map_err(|_| AtomicCryptoError::BadDidKey)
170}
171
172/// Verifica vários pares (vk, `cid_hex`, `sig_b64`).
173#[must_use]
174pub fn verify_many(vk: &VerifyingKey, items: &[(&str, &str)]) -> usize {
175    items
176        .iter()
177        .filter(|(cid, sig_b64)| b64_decode(sig_b64).is_ok_and(|sig| verify_cid_hex(vk, cid, &sig)))
178        .count()
179}
180
181// ══════════════════════════════════════════════════════════════════════════════
182// atomic-types integration: Cid32, PublicKeyBytes, SignatureBytes
183// ══════════════════════════════════════════════════════════════════════════════
184
185use ubl_types::{Cid32, PublicKeyBytes, SignatureBytes};
186
187/// Hash BLAKE3 de 32 bytes → `Cid32`.
188///
189/// Wrapper determinístico que retorna o tipo canônico `Cid32`.
190#[must_use]
191#[inline]
192pub fn blake3_cid(data: &[u8]) -> Cid32 {
193    let hash = blake3::hash(data);
194    let mut bytes = [0u8; 32];
195    bytes.copy_from_slice(hash.as_bytes());
196    Cid32(bytes)
197}
198
199/// Hash BLAKE3 de múltiplos chunks → `Cid32`.
200///
201/// Útil para hashing incremental/streaming.
202#[must_use]
203#[inline]
204pub fn blake3_cid_chunks<'a, I>(chunks: I) -> Cid32
205where
206    I: IntoIterator<Item = &'a [u8]>,
207{
208    let mut hasher = blake3::Hasher::new();
209    for c in chunks {
210        hasher.update(c);
211    }
212    let out = hasher.finalize();
213    let mut bytes = [0u8; 32];
214    bytes.copy_from_slice(out.as_bytes());
215    Cid32(bytes)
216}
217
218/// Deriva `PublicKeyBytes` a partir de secret seed Ed25519 (32B).
219#[must_use]
220#[inline]
221pub fn derive_public_bytes(secret_key: &[u8; 32]) -> PublicKeyBytes {
222    let sk = SigningKey::from_bytes(secret_key);
223    PublicKeyBytes(sk.verifying_key().to_bytes())
224}
225
226/// Assina `msg` com secret seed Ed25519 (32B) → `SignatureBytes`.
227#[must_use]
228#[inline]
229pub fn sign_bytes(msg: &[u8], secret_key: &[u8; 32]) -> SignatureBytes {
230    let sk = SigningKey::from_bytes(secret_key);
231    let sig: Signature = sk.sign(msg);
232    SignatureBytes(sig.to_bytes())
233}
234
235/// Verifica assinatura Ed25519 usando tipos `atomic-types`.
236///
237/// Retorna `true` se a assinatura for válida.
238#[must_use]
239#[inline]
240pub fn verify_bytes(msg: &[u8], pk: &PublicKeyBytes, sig: &SignatureBytes) -> bool {
241    match VerifyingKey::from_bytes(&pk.0) {
242        Ok(vk) => {
243            let s = Signature::from_bytes(&sig.0);
244            vk.verify_strict(msg, &s).is_ok()
245        }
246        Err(_) => false,
247    }
248}
249
250#[cfg(test)]
251mod atomic_types_tests {
252    use super::*;
253
254    #[test]
255    fn blake3_cid_deterministic() {
256        let a = blake3_cid(b"hello");
257        let b = blake3_cid(b"hello");
258        assert_eq!(a.0, b.0);
259
260        let c = blake3_cid_chunks([b"hel".as_slice(), b"lo".as_slice()]);
261        assert_eq!(a.0, c.0);
262    }
263
264    #[test]
265    fn ed25519_atomic_types_roundtrip() {
266        let sk = [7u8; 32]; // fixed seed for determinism
267        let pk = derive_public_bytes(&sk);
268        let msg = b"deterministic message";
269        let sig = sign_bytes(msg, &sk);
270
271        assert!(verify_bytes(msg, &pk, &sig));
272        assert!(!verify_bytes(b"wrong", &pk, &sig));
273    }
274
275    #[test]
276    fn cid_serializes_to_hex() {
277        let cid = blake3_cid(b"test");
278        let json = serde_json::to_string(&cid).unwrap();
279        // Should be 64 hex chars + 2 quotes
280        assert_eq!(json.len(), 66);
281    }
282}