clementine_core/
encryption.rs1use bitcoin::secp256k1::rand::{self, RngCore};
2use chacha20poly1305::{
3 aead::{Aead, KeyInit},
4 XChaCha20Poly1305, XNonce,
5};
6#[cfg(test)]
7use x25519_dalek::StaticSecret as X25519StaticSecret;
8use x25519_dalek::{EphemeralSecret, PublicKey as X25519PublicKey};
9
10pub fn encrypt_bytes(recipient_pubkey: [u8; 32], message: &[u8]) -> Result<Vec<u8>, eyre::Report> {
25 let recipient_pubkey = X25519PublicKey::from(recipient_pubkey);
26
27 let ephemeral_secret = EphemeralSecret::random_from_rng(rand::thread_rng());
28 let ephemeral_public = X25519PublicKey::from(&ephemeral_secret);
29
30 let shared_secret = ephemeral_secret.diffie_hellman(&recipient_pubkey);
31 let cipher = XChaCha20Poly1305::new_from_slice(shared_secret.as_bytes())
32 .map_err(|e| eyre::eyre!("Failed to create cipher: {e}"))?;
33
34 let mut nonce_bytes = [0u8; 24];
35 rand::thread_rng().fill_bytes(&mut nonce_bytes);
36 let nonce = XNonce::from_slice(&nonce_bytes);
37
38 let ciphertext = cipher
39 .encrypt(nonce, message)
40 .map_err(|e| eyre::eyre!("Failed to encrypt message: {e}"))?;
41
42 let mut output = vec![];
43 output.extend_from_slice(ephemeral_public.as_bytes());
44 output.extend_from_slice(&nonce_bytes);
45 output.extend_from_slice(&ciphertext);
46 Ok(output)
47}
48
49#[cfg(test)]
50const MIN_ENCRYPTED_LEN: usize = 56;
51#[cfg(test)]
52const EPHEMERAL_PUBKEY_LEN: usize = 32;
53
54#[cfg(test)]
67pub fn decrypt_bytes(recipient_privkey: &[u8], encrypted: &[u8]) -> Result<Vec<u8>, eyre::Report> {
68 if encrypted.len() < MIN_ENCRYPTED_LEN {
69 return Err(eyre::eyre!("Invalid encrypted length"));
70 }
71
72 let ephemeral_pubkey_bytes: [u8; EPHEMERAL_PUBKEY_LEN] = encrypted[0..EPHEMERAL_PUBKEY_LEN]
73 .try_into()
74 .map_err(|_| eyre::eyre!("Invalid ephemeral public key length"))?;
75 let ephemeral_pubkey = X25519PublicKey::from(ephemeral_pubkey_bytes);
76 let nonce = XNonce::from_slice(&encrypted[EPHEMERAL_PUBKEY_LEN..MIN_ENCRYPTED_LEN]);
77 let ciphertext = &encrypted[MIN_ENCRYPTED_LEN..];
78
79 let recipient_priv_bytes: [u8; 32] = recipient_privkey
80 .try_into()
81 .map_err(|_| eyre::eyre!("Invalid recipient private key length"))?;
82 let recipient_secret = X25519StaticSecret::from(recipient_priv_bytes);
83
84 let shared_secret = recipient_secret.diffie_hellman(&ephemeral_pubkey);
85 let cipher = XChaCha20Poly1305::new_from_slice(shared_secret.as_bytes())
86 .map_err(|_| eyre::eyre!("Failed to create cipher"))?;
87
88 cipher
89 .decrypt(nonce, ciphertext)
90 .map_err(|_| eyre::eyre!("Failed to decrypt message"))
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use hex::FromHex;
97
98 #[test]
99 fn test_encrypt_decrypt() {
100 let privkey = <[u8; 32]>::from_hex(
102 "a80bc8cf095c2b37d4c6233114e0dd91f43d75de5602466232dbfcc1fc66c542",
103 )
104 .unwrap();
105 let pubkey = <[u8; 32]>::from_hex(
106 "025d32d10ec7b899df4eeb4d80918b7f0a1f2a28f6af24f71aa2a59c69c0d531",
107 )
108 .unwrap();
109
110 let message = b"Hello, Clementine!";
112
113 let encrypted = encrypt_bytes(pubkey, message).unwrap();
115
116 let decrypted = decrypt_bytes(&privkey, &encrypted).unwrap();
118
119 assert_eq!(message, decrypted.as_slice());
121 }
122}