1use base64::Engine;
2use chacha20::cipher::{KeyIvInit, StreamCipher};
3use chacha20::ChaCha20;
4use hkdf::Hkdf;
5use hmac::{Hmac, Mac};
6use rand_core::{OsRng, RngCore};
7use secp256k1::ecdh::shared_secret_point;
8use secp256k1::{Parity, PublicKey, SecretKey, XOnlyPublicKey};
9use sha2::Sha256;
10
11mod error;
12pub use error::Error;
13
14#[cfg(test)]
15mod tests;
16
17struct MessageKeys([u8; 76]);
18
19impl MessageKeys {
20 #[inline]
21 pub fn zero() -> MessageKeys {
22 MessageKeys([0; 76])
23 }
24
25 #[inline]
26 pub fn encryption(&self) -> [u8; 32] {
27 self.0[0..32].try_into().unwrap()
28 }
29
30 #[inline]
31 pub fn nonce(&self) -> [u8; 12] {
32 self.0[32..44].try_into().unwrap()
33 }
34
35 #[inline]
36 pub fn auth(&self) -> [u8; 32] {
37 self.0[44..76].try_into().unwrap()
38 }
39}
40
41fn get_shared_point(private_key_a: SecretKey, x_only_public_key_b: XOnlyPublicKey) -> [u8; 32] {
43 let pubkey = PublicKey::from_x_only_public_key(x_only_public_key_b, Parity::Even);
44 let mut ssp = shared_secret_point(&pubkey, &private_key_a)
45 .as_slice()
46 .to_owned();
47 ssp.resize(32, 0); ssp.try_into().unwrap()
49}
50
51pub fn get_conversation_key(
52 private_key_a: SecretKey,
53 x_only_public_key_b: XOnlyPublicKey,
54) -> [u8; 32] {
55 let shared_point = get_shared_point(private_key_a, x_only_public_key_b);
56 let (convo_key, _hkdf) =
57 Hkdf::<Sha256>::extract(Some("nip44-v2".as_bytes()), shared_point.as_slice());
58 convo_key.into()
59}
60
61fn get_message_keys(conversation_key: &[u8; 32], nonce: &[u8; 32]) -> Result<MessageKeys, Error> {
62 let hk: Hkdf<Sha256> = match Hkdf::from_prk(conversation_key) {
63 Ok(hk) => hk,
64 Err(_) => return Err(Error::HkdfLength(conversation_key.len())),
65 };
66 let mut message_keys: MessageKeys = MessageKeys::zero();
67 if hk.expand(&nonce[..], &mut message_keys.0).is_err() {
68 return Err(Error::HkdfLength(message_keys.0.len()));
69 }
70 Ok(message_keys)
71}
72
73fn calc_padding(len: usize) -> usize {
74 if len < 32 {
75 return 32;
76 }
77 let nextpower = 1 << ((len - 1).ilog2() + 1);
78 let chunk = if nextpower <= 256 { 32 } else { nextpower / 8 };
79 if len <= 32 {
80 32
81 } else {
82 chunk * (((len - 1) / chunk) + 1)
83 }
84}
85
86fn pad(unpadded: &str) -> Result<Vec<u8>, Error> {
87 let len: usize = unpadded.len();
88 if len < 1 {
89 return Err(Error::MessageIsEmpty);
90 }
91 if len > 65536 - 128 {
92 return Err(Error::MessageIsTooLong);
93 }
94
95 let mut padded: Vec<u8> = Vec::new();
96 padded.extend_from_slice(&(len as u16).to_be_bytes());
97 padded.extend_from_slice(unpadded.as_bytes());
98 padded.extend(std::iter::repeat(0).take(calc_padding(len) - len));
99 Ok(padded)
100}
101
102#[inline]
105pub fn encrypt(conversation_key: &[u8; 32], plaintext: &str) -> Result<String, Error> {
106 encrypt_inner(conversation_key, plaintext, None)
107}
108
109fn encrypt_inner(
110 conversation_key: &[u8; 32],
111 plaintext: &str,
112 override_random_nonce: Option<&[u8; 32]>,
113) -> Result<String, Error> {
114 let nonce = match override_random_nonce {
115 Some(nonce) => nonce.to_owned(),
116 None => {
117 let mut nonce: [u8; 32] = [0; 32];
118 OsRng.fill_bytes(&mut nonce);
119 nonce
120 }
121 };
122
123 let keys = get_message_keys(conversation_key, &nonce)?;
124 let mut buffer = pad(plaintext)?;
125 let mut cipher = ChaCha20::new(&keys.encryption().into(), &keys.nonce().into());
126 cipher.apply_keystream(&mut buffer);
127 let mut mac = Hmac::<Sha256>::new_from_slice(&keys.auth())?;
128 mac.update(&nonce);
129 mac.update(&buffer);
130 let mac_bytes = mac.finalize().into_bytes();
131
132 let mut pre_base64: Vec<u8> = vec![2];
133 pre_base64.extend_from_slice(&nonce);
134 pre_base64.extend_from_slice(&buffer);
135 pre_base64.extend_from_slice(&mac_bytes);
136
137 Ok(base64::engine::general_purpose::STANDARD.encode(&pre_base64))
138}
139
140pub fn decrypt(conversation_key: &[u8; 32], base64_ciphertext: &str) -> Result<String, Error> {
142 if base64_ciphertext.as_bytes()[0] == b'#' {
143 return Err(Error::UnsupportedFutureVersion);
144 }
145 let binary_ciphertext: Vec<u8> =
146 base64::engine::general_purpose::STANDARD.decode(base64_ciphertext)?;
147 let version = binary_ciphertext[0];
148 if version != 2 {
149 return Err(Error::UnknownVersion);
150 }
151 let dlen = binary_ciphertext.len();
152 let nonce = &binary_ciphertext[1..33];
153 let mut buffer = binary_ciphertext[33..dlen - 32].to_owned();
154 let mac = &binary_ciphertext[dlen - 32..dlen];
155 let keys = get_message_keys(conversation_key, &nonce.try_into().unwrap())?;
156 let mut calculated_mac = Hmac::<Sha256>::new_from_slice(&keys.auth())?;
157 calculated_mac.update(&nonce);
158 calculated_mac.update(&buffer);
159 let calculated_mac_bytes = calculated_mac.finalize().into_bytes();
160 if !constant_time_eq::constant_time_eq(mac, calculated_mac_bytes.as_slice()) {
161 return Err(Error::InvalidMac);
162 }
163 let mut cipher = ChaCha20::new(&keys.encryption().into(), &keys.nonce().into());
164 cipher.apply_keystream(&mut buffer);
165 let unpadded_len = u16::from_be_bytes(buffer[0..2].try_into().unwrap()) as usize;
166 if buffer.len() < 2 + unpadded_len {
167 return Err(Error::InvalidPadding);
168 }
169 let unpadded = &buffer[2..2 + unpadded_len];
170 if unpadded.is_empty() {
171 return Err(Error::MessageIsEmpty);
172 }
173 if unpadded.len() != unpadded_len {
174 return Err(Error::InvalidPadding);
175 }
176 if buffer.len() != 2 + calc_padding(unpadded_len) {
177 return Err(Error::InvalidPadding);
178 }
179 Ok(String::from_utf8(unpadded.to_vec())?)
180}