native/metamorphic_crypto_nif/metamorphic-crypto/src/keys.rs

//! Key generation and private-key encrypt/decrypt helpers.

use crypto_box::SecretKey;
use zeroize::Zeroize;

use crate::CryptoError;
use crate::b64;
use crate::secretbox::{decrypt_secretbox_to_string, encrypt_secretbox_string};

/// Secretbox key length (32 bytes).
const KEY_LEN: usize = 32;
/// Argon2id salt length (16 bytes).
const SALT_LEN: usize = 16;

/// An X25519 keypair (base64-encoded).
#[derive(Debug, Clone)]
pub struct KeyPair {
    /// X25519 public key (base64, 32 bytes decoded).
    pub public_key: String,
    /// X25519 secret key (base64, 32 bytes decoded).
    pub private_key: String,
}

/// Fill `buf` with OS-random bytes.
#[inline]
fn random_bytes(buf: &mut [u8]) {
    getrandom::getrandom(buf).expect("OS CSPRNG unavailable");
}

/// Generate a random 32-byte symmetric key (base64-encoded).
///
/// Used for per-context keys (habit_key, group_key, reflection_key, etc.).
pub fn generate_key() -> String {
    let mut key = [0u8; KEY_LEN];
    random_bytes(&mut key);
    let encoded = b64::encode(&key);
    key.zeroize();
    encoded
}

/// Generate a random X25519 keypair (base64-encoded).
pub fn generate_keypair() -> KeyPair {
    let mut sk_bytes = [0u8; KEY_LEN];
    random_bytes(&mut sk_bytes);
    let sk = SecretKey::from_slice(&sk_bytes).expect("valid 32-byte key");
    let pk = sk.public_key();

    let kp = KeyPair {
        public_key: b64::encode(pk.as_bytes()),
        private_key: b64::encode(&sk_bytes),
    };
    sk_bytes.zeroize();
    kp
}

/// Generate a random 16-byte Argon2id salt (base64-encoded).
pub fn generate_salt() -> String {
    let mut salt = [0u8; SALT_LEN];
    random_bytes(&mut salt);
    b64::encode(&salt)
}

/// Encrypt a base64-encoded private key with a session key.
///
/// **Invariant**: the private key is stored as a base64 *string* (not raw bytes).
/// This function encrypts that string via secretbox, matching the JS implementation.
pub fn encrypt_private_key(
    private_key_b64: &str,
    session_key_b64: &str,
) -> Result<String, CryptoError> {
    encrypt_secretbox_string(private_key_b64, session_key_b64)
}

/// Decrypt an encrypted private key with a session key, returning the base64 private key.
pub fn decrypt_private_key(ct_b64: &str, session_key_b64: &str) -> Result<String, CryptoError> {
    decrypt_secretbox_to_string(ct_b64, session_key_b64)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn key_is_32_bytes() {
        let key = generate_key();
        assert_eq!(b64::decode(&key).unwrap().len(), 32);
    }

    #[test]
    fn keys_are_unique() {
        assert_ne!(generate_key(), generate_key());
    }

    #[test]
    fn keypair_sizes() {
        let kp = generate_keypair();
        assert_eq!(b64::decode(&kp.public_key).unwrap().len(), 32);
        assert_eq!(b64::decode(&kp.private_key).unwrap().len(), 32);
    }

    #[test]
    fn salt_is_16_bytes() {
        assert_eq!(b64::decode(&generate_salt()).unwrap().len(), 16);
    }

    #[test]
    fn private_key_encrypt_decrypt() {
        let kp = generate_keypair();
        let session_key = generate_key();
        let ct = encrypt_private_key(&kp.private_key, &session_key).unwrap();
        let pt = decrypt_private_key(&ct, &session_key).unwrap();
        assert_eq!(pt, kp.private_key);
    }

    #[test]
    fn private_key_wrong_session_key() {
        let kp = generate_keypair();
        let k1 = generate_key();
        let k2 = generate_key();
        let ct = encrypt_private_key(&kp.private_key, &k1).unwrap();
        assert!(decrypt_private_key(&ct, &k2).is_err());
    }
}