Skip to main content

vector_core/community/
cipher.rs

1//! Raw-key NIP-44 v2 sealing — the single symmetric-encryption primitive of the
2//! Community protocol. The channel key (message plane) and the server-root key
3//! (metadata plane) are both raw 32-byte `ConversationKey`s; ciphertext is
4//! base64'd for carriage in an event's string `content` field.
5
6use nostr_sdk::prelude::nip44::v2::{decrypt_to_bytes, encrypt_to_bytes_with_nonce, ConversationKey};
7use rand::RngCore;
8
9/// NIP-44 v2 encrypt with a freshly generated nonce.
10///
11/// The nonce MUST be unique per message under a given conversation key: reuse
12/// repeats the ChaCha20 keystream and forfeits both confidentiality and MAC
13/// integrity. Drawn from the OS CSPRNG here so no call site can supply its own.
14/// Wire output is unchanged (`[2][nonce][ct][mac]`).
15pub fn encrypt_with_random_nonce(
16    ck: &ConversationKey,
17    plaintext: &[u8],
18) -> Result<Vec<u8>, String> {
19    let mut nonce = [0u8; 32];
20    rand::rngs::OsRng.fill_bytes(&mut nonce);
21    encrypt_to_bytes_with_nonce(ck, plaintext, nonce).map_err(|e| e.to_string())
22}
23
24/// Encrypt `plaintext` under a raw 32-byte key, returning base64 for event content.
25pub fn seal(key: &[u8; 32], plaintext: &[u8]) -> Result<String, String> {
26    let ck = ConversationKey::new(*key);
27    let ciphertext = encrypt_with_random_nonce(&ck, plaintext)?;
28    Ok(base64_simd::STANDARD.encode_to_string(&ciphertext))
29}
30
31/// Inverse of [`seal`]: base64-decode then NIP-44-decrypt under the raw key. A
32/// wrong key or tampered payload fails the MAC and returns `Err`.
33pub fn open(key: &[u8; 32], content_b64: &str) -> Result<Vec<u8>, String> {
34    let ciphertext = base64_simd::STANDARD
35        .decode_to_vec(content_b64.as_bytes())
36        .map_err(|e| e.to_string())?;
37    let ck = ConversationKey::new(*key);
38    decrypt_to_bytes(&ck, &ciphertext).map_err(|e| e.to_string())
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44
45    #[test]
46    fn round_trip() {
47        let key = [0x5au8; 32];
48        let sealed = seal(&key, b"hello community").unwrap();
49        assert_eq!(open(&key, &sealed).unwrap(), b"hello community");
50    }
51
52    #[test]
53    fn wrong_key_fails() {
54        let sealed = seal(&[1u8; 32], b"secret").unwrap();
55        assert!(open(&[2u8; 32], &sealed).is_err());
56    }
57
58    #[test]
59    fn distinct_ciphertext_per_call() {
60        let key = [9u8; 32];
61        assert_ne!(seal(&key, b"x").unwrap(), seal(&key, b"x").unwrap());
62    }
63}