Skip to main content

metamorphic_crypto/
keys.rs

1//! Key generation and private-key encrypt/decrypt helpers.
2
3use crypto_box::SecretKey;
4use zeroize::Zeroize;
5
6use crate::CryptoError;
7use crate::b64;
8use crate::secretbox::{decrypt_secretbox_to_string, encrypt_secretbox_string};
9
10/// Secretbox key length (32 bytes).
11const KEY_LEN: usize = 32;
12/// Argon2id salt length (16 bytes).
13const SALT_LEN: usize = 16;
14
15/// An X25519 keypair (base64-encoded).
16#[derive(Debug, Clone)]
17pub struct KeyPair {
18    /// X25519 public key (base64, 32 bytes decoded).
19    pub public_key: String,
20    /// X25519 secret key (base64, 32 bytes decoded).
21    pub private_key: String,
22}
23
24/// Fill `buf` with OS-random bytes.
25#[inline]
26fn random_bytes(buf: &mut [u8]) {
27    getrandom::getrandom(buf).expect("OS CSPRNG unavailable");
28}
29
30/// Generate a random 32-byte symmetric key (base64-encoded).
31///
32/// Used for per-context keys (habit_key, group_key, reflection_key, etc.).
33pub fn generate_key() -> String {
34    let mut key = [0u8; KEY_LEN];
35    random_bytes(&mut key);
36    let encoded = b64::encode(&key);
37    key.zeroize();
38    encoded
39}
40
41/// Generate a random X25519 keypair (base64-encoded).
42pub fn generate_keypair() -> KeyPair {
43    let mut sk_bytes = [0u8; KEY_LEN];
44    random_bytes(&mut sk_bytes);
45    let sk = SecretKey::from_slice(&sk_bytes).expect("valid 32-byte key");
46    let pk = sk.public_key();
47
48    let kp = KeyPair {
49        public_key: b64::encode(pk.as_bytes()),
50        private_key: b64::encode(&sk_bytes),
51    };
52    sk_bytes.zeroize();
53    kp
54}
55
56/// Generate a random 16-byte Argon2id salt (base64-encoded).
57pub fn generate_salt() -> String {
58    let mut salt = [0u8; SALT_LEN];
59    random_bytes(&mut salt);
60    b64::encode(&salt)
61}
62
63/// Encrypt a base64-encoded private key with a session key.
64///
65/// **Invariant**: the private key is stored as a base64 *string* (not raw bytes).
66/// This function encrypts that string via secretbox, matching the JS implementation.
67pub fn encrypt_private_key(
68    private_key_b64: &str,
69    session_key_b64: &str,
70) -> Result<String, CryptoError> {
71    encrypt_secretbox_string(private_key_b64, session_key_b64)
72}
73
74/// Decrypt an encrypted private key with a session key, returning the base64 private key.
75pub fn decrypt_private_key(ct_b64: &str, session_key_b64: &str) -> Result<String, CryptoError> {
76    decrypt_secretbox_to_string(ct_b64, session_key_b64)
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn key_is_32_bytes() {
85        let key = generate_key();
86        assert_eq!(b64::decode(&key).unwrap().len(), 32);
87    }
88
89    #[test]
90    fn keys_are_unique() {
91        assert_ne!(generate_key(), generate_key());
92    }
93
94    #[test]
95    fn keypair_sizes() {
96        let kp = generate_keypair();
97        assert_eq!(b64::decode(&kp.public_key).unwrap().len(), 32);
98        assert_eq!(b64::decode(&kp.private_key).unwrap().len(), 32);
99    }
100
101    #[test]
102    fn salt_is_16_bytes() {
103        assert_eq!(b64::decode(&generate_salt()).unwrap().len(), 16);
104    }
105
106    #[test]
107    fn private_key_encrypt_decrypt() {
108        let kp = generate_keypair();
109        let session_key = generate_key();
110        let ct = encrypt_private_key(&kp.private_key, &session_key).unwrap();
111        let pt = decrypt_private_key(&ct, &session_key).unwrap();
112        assert_eq!(pt, kp.private_key);
113    }
114
115    #[test]
116    fn private_key_wrong_session_key() {
117        let kp = generate_keypair();
118        let k1 = generate_key();
119        let k2 = generate_key();
120        let ct = encrypt_private_key(&kp.private_key, &k1).unwrap();
121        assert!(decrypt_private_key(&ct, &k2).is_err());
122    }
123}