gnip44/
lib.rs

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
41/// A conversation key is the long-term secret that two nostr identities share.
42fn 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); // toss the Y part
48    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/// Encrypt a plaintext message with a conversation key.
103/// The output is a base64 encoded string that can be placed into message contents.
104#[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
140/// Decrypt the base64 encrypted contents with a conversation key
141pub 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}