From 16253cea18044f72cda0f5618444dfd6a17c5c80 Mon Sep 17 00:00:00 2001 From: Laan Tungir Date: Sun, 27 Sep 2026 10:24:00 -0400 Subject: [PATCH] Fix NIP-44 to spec-compliant v2: raw-X ECDH, HKDF(salt=nip44-v2), ChaCha20+HMAC, padding, versioned payload The previous implementation used libsecp256k1's hashed ECDH, a non-standard HKDF info string, ChaCha20-Poly1305 with a 12-byte nonce, no padding, and no version byte. It could not decrypt real NIP-44 v2 payloads, causing the remote signer to reject nostr_nip44_decrypt with invalid_params (-32602). Rewrote to match the NIP-44 v2 spec and added known-answer tests using the official test vectors (calc_padded_len, get_conversation_key, get_message_keys, encrypt_decrypt). --- Cargo.lock | 19 +- Cargo.toml | 1 + core/Cargo.toml | 1 + core/src/crypto/nip44.rs | 453 ++++++++++++++++++++++++++++++--------- 4 files changed, 361 insertions(+), 113 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9a1b6f3..cd667fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -385,7 +385,7 @@ dependencies = [ [[package]] name = "event-signer" -version = "0.1.0" +version = "0.1.1" dependencies = [ "nostr-core", "nostr-nips", @@ -868,7 +868,7 @@ dependencies = [ [[package]] name = "integration-tests" -version = "0.1.0" +version = "0.1.1" dependencies = [ "nostr-core", "nostr-nips", @@ -910,7 +910,7 @@ dependencies = [ [[package]] name = "keypair-generator" -version = "0.1.0" +version = "0.1.1" dependencies = [ "nostr-core", ] @@ -1041,12 +1041,13 @@ dependencies = [ [[package]] name = "nostr-core" -version = "0.1.0" +version = "0.1.1" dependencies = [ "aes", "base64", "bech32", "block-modes", + "chacha20", "chacha20poly1305", "chrono", "hex", @@ -1063,7 +1064,7 @@ dependencies = [ [[package]] name = "nostr-core-umbrella" -version = "0.1.0" +version = "0.1.1" dependencies = [ "nostr-core", "nostr-nips", @@ -1074,7 +1075,7 @@ dependencies = [ [[package]] name = "nostr-nips" -version = "0.1.0" +version = "0.1.1" dependencies = [ "aes", "block-modes", @@ -1096,7 +1097,7 @@ dependencies = [ [[package]] name = "nostr-relay" -version = "0.1.0" +version = "0.1.1" dependencies = [ "futures-util", "nostr-core", @@ -1114,7 +1115,7 @@ dependencies = [ [[package]] name = "nostr-services" -version = "0.1.0" +version = "0.1.1" dependencies = [ "nostr-core", "nostr-relay", @@ -1129,7 +1130,7 @@ dependencies = [ [[package]] name = "nostr-signer" -version = "0.1.0" +version = "0.1.1" dependencies = [ "aes", "cbc", diff --git a/Cargo.toml b/Cargo.toml index 20fd306..f1613bd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ sha2 = "0.10" hmac = "0.12" hex = "0.4" chacha20poly1305 = "0.10" +chacha20 = "0.9" aes = "0.8" block-modes = "0.9" rand = "0.8" diff --git a/core/Cargo.toml b/core/Cargo.toml index fa23f8c..d2abd8c 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -10,6 +10,7 @@ sha2.workspace = true hmac.workspace = true hex.workspace = true chacha20poly1305.workspace = true +chacha20.workspace = true aes.workspace = true block-modes.workspace = true rand.workspace = true diff --git a/core/src/crypto/nip44.rs b/core/src/crypto/nip44.rs index 991fd3d..f37e548 100644 --- a/core/src/crypto/nip44.rs +++ b/core/src/crypto/nip44.rs @@ -1,47 +1,163 @@ -//! NIP-44 encryption: ChaCha20 + HMAC-SHA256 AEAD. +//! NIP-44 v2 encryption: ChaCha20 + HMAC-SHA256 AEAD. //! -//! This implements the NIP-44 encryption scheme using: -//! - ECDH for shared secret derivation -//! - HKDF for key derivation -//! - ChaCha20 for encryption -//! - HMAC-SHA256 for authentication +//! This is a spec-compliant implementation of NIP-44 version 2 +//! (). +//! +//! The scheme is: +//! 1. `shared_x = x(ECDH(sec1, pub2))` — the raw X coordinate of the shared +//! point, **not** hashed (libsecp256k1's default ECDH hashes the point, so +//! we use [`ecdh_shared_secret_raw_x`]). +//! 2. `conversation_key = HKDF-Extract(salt = "nip44-v2", ikm = shared_x)`. +//! 3. `message_keys = HKDF-Expand(conversation_key, info = nonce, L = 76)` +//! split into `chacha_key(32) || chacha_nonce(12) || hmac_key(32)`. +//! 4. Plaintext is padded per the spec, encrypted with ChaCha20 (counter 0), +//! and authenticated with HMAC-SHA256 over `nonce || ciphertext`. +//! 5. The payload is `base64(0x02 || nonce(32) || ciphertext || mac(32))`. +//! +//! The public API ([`nip44_encrypt`] / [`nip44_decrypt`]) operates on the raw +//! binary payload (version byte + nonce + ciphertext + mac), matching the +//! convention used by the rest of this crate (e.g. NIP-59 gift wrapping). -use chacha20poly1305::{ - aead::{Aead, KeyInit}, - ChaCha20Poly1305, Nonce, -}; +use chacha20::cipher::{KeyIvInit, StreamCipher}; +use chacha20::ChaCha20; use rand::rngs::OsRng; use rand::RngCore; -use crate::crypto::hmac::hkdf; -use crate::crypto::keys::ecdh_shared_secret_both; +use crate::crypto::hmac::{hkdf_expand, hkdf_extract, hmac_sha256}; +use crate::crypto::keys::ecdh_shared_secret_raw_x; use crate::error::NostrError; use crate::types::{PublicKey, SecretKey}; use crate::NostrResult; -/// NIP-44 conversation key size (32 bytes for ChaCha20 key). -const CONVERSATION_KEY_SIZE: usize = 32; +/// NIP-44 version byte (v2). +const VERSION: u8 = 0x02; -/// NIP-44 nonce size (12 bytes for ChaCha20-Poly1305). -const NONCE_SIZE: usize = 12; +/// Nonce size (32 bytes). +const NONCE_SIZE: usize = 32; -/// Maximum plaintext size for NIP-44 (64KB - 1). +/// MAC size (32 bytes). +const MAC_SIZE: usize = 32; + +/// Minimum payload size: version(1) + nonce(32) + mac(32) + 1 byte ciphertext. +const MIN_PAYLOAD_SIZE: usize = 1 + NONCE_SIZE + MAC_SIZE + 1; + +/// Maximum plaintext size for NIP-44 (64 KiB - 1). pub const MAX_PLAINTEXT_SIZE: usize = 65535; -/// Derive a NIP-44 conversation key from an ECDH shared secret. +/// NIP-44 padding: round the unpadded length up to the next chunk boundary. /// -/// The conversation key is derived as: -/// `HKDF(salt=empty, ikm=ecdh_secret, info="nip44-conversation-key", L=32)` -fn derive_conversation_key(shared_secret: &[u8; 32]) -> [u8; CONVERSATION_KEY_SIZE] { - let mut result = [0u8; CONVERSATION_KEY_SIZE]; - let derived = hkdf(&[], shared_secret, b"nip44-conversation-key", CONVERSATION_KEY_SIZE); - result.copy_from_slice(&derived); - result +/// Mirrors the reference implementation: +/// - `<= 32` bytes → 32 +/// - otherwise round up to the next multiple of `chunk`, where +/// `chunk = 32` for lengths up to 256 and `chunk = next_power_of_two / 8` +/// beyond that. +pub fn calc_padded_len(unpadded_len: usize) -> usize { + if unpadded_len <= 32 { + return 32; + } + let mut next_power = 1usize; + while next_power < unpadded_len { + next_power <<= 1; + } + let chunk = if next_power <= 256 { 32 } else { next_power / 8 }; + chunk * ((unpadded_len - 1) / chunk + 1) } -/// Encrypt a plaintext using NIP-44 scheme. +/// Pad plaintext per NIP-44: `u16_be(len) || plaintext || zero padding`. +fn pad_plaintext(plaintext: &[u8]) -> NostrResult> { + if plaintext.len() > MAX_PLAINTEXT_SIZE { + return Err(NostrError::Nip44BufferTooSmall); + } + let padded_len = calc_padded_len(plaintext.len()); + let mut out = Vec::with_capacity(2 + padded_len); + out.extend_from_slice(&(plaintext.len() as u16).to_be_bytes()); + out.extend_from_slice(plaintext); + out.resize(2 + padded_len, 0); + Ok(out) +} + +/// Remove NIP-44 padding, validating the length prefix. +fn unpad_plaintext(padded: &[u8]) -> NostrResult> { + if padded.len() < 2 { + return Err(NostrError::Nip44InvalidFormat); + } + let unpadded_len = ((padded[0] as usize) << 8) | (padded[1] as usize); + let expected = calc_padded_len(unpadded_len); + if padded.len() != expected + 2 { + return Err(NostrError::Nip44InvalidFormat); + } + if unpadded_len > padded.len() - 2 { + return Err(NostrError::Nip44InvalidFormat); + } + Ok(padded[2..2 + unpadded_len].to_vec()) +} + +/// Derive the NIP-44 conversation key from a secret key and peer public key. /// -/// Returns the ciphertext as bytes (nonce || encrypted_data || tag). +/// `conversation_key = HKDF-Extract(salt = "nip44-v2", ikm = x(ECDH(sk, pk)))`. +pub fn get_conversation_key( + secret_key: &SecretKey, + public_key: &PublicKey, +) -> NostrResult<[u8; 32]> { + let shared_x = ecdh_shared_secret_raw_x(secret_key, public_key)?; + Ok(hkdf_extract(b"nip44-v2", &shared_x)) +} + +/// Derive the per-message keys from a conversation key and nonce. +/// +/// Returns `(chacha_key, chacha_nonce, hmac_key)`. +fn get_message_keys( + conversation_key: &[u8; 32], + nonce: &[u8; NONCE_SIZE], +) -> ([u8; 32], [u8; 12], [u8; 32]) { + let keys = hkdf_expand(conversation_key, nonce, 76); + let mut chacha_key = [0u8; 32]; + let mut chacha_nonce = [0u8; 12]; + let mut hmac_key = [0u8; 32]; + chacha_key.copy_from_slice(&keys[0..32]); + chacha_nonce.copy_from_slice(&keys[32..44]); + hmac_key.copy_from_slice(&keys[44..76]); + (chacha_key, chacha_nonce, hmac_key) +} + +/// Encrypt a plaintext using NIP-44 v2 with a caller-supplied nonce. +/// +/// Returns the raw binary payload: `version || nonce || ciphertext || mac`. +/// This is primarily useful for reproducible tests; production callers should +/// use [`nip44_encrypt`]. +pub fn nip44_encrypt_with_nonce( + sender_sk: &SecretKey, + recipient_pk: &PublicKey, + plaintext: &[u8], + nonce: &[u8; NONCE_SIZE], +) -> NostrResult> { + let conversation_key = get_conversation_key(sender_sk, recipient_pk)?; + let (chacha_key, chacha_nonce, hmac_key) = get_message_keys(&conversation_key, nonce); + + let padded = pad_plaintext(plaintext)?; + + // ChaCha20 (counter 0) — symmetric stream cipher, so the same call decrypts. + let mut ciphertext = padded.clone(); + let mut cipher = ChaCha20::new(&chacha_key.into(), &chacha_nonce.into()); + cipher.apply_keystream(&mut ciphertext); + + // HMAC-SHA256 over nonce || ciphertext. + let mut aad = Vec::with_capacity(NONCE_SIZE + ciphertext.len()); + aad.extend_from_slice(nonce); + aad.extend_from_slice(&ciphertext); + let mac = hmac_sha256(&hmac_key, &aad); + + let mut payload = Vec::with_capacity(1 + NONCE_SIZE + ciphertext.len() + MAC_SIZE); + payload.push(VERSION); + payload.extend_from_slice(nonce); + payload.extend_from_slice(&ciphertext); + payload.extend_from_slice(&mac); + Ok(payload) +} + +/// Encrypt a plaintext using NIP-44 v2. +/// +/// Returns the raw binary payload: `version || nonce || ciphertext || mac`. pub fn nip44_encrypt( sender_sk: &SecretKey, recipient_pk: &PublicKey, @@ -50,91 +166,63 @@ pub fn nip44_encrypt( if plaintext.len() > MAX_PLAINTEXT_SIZE { return Err(NostrError::Nip44BufferTooSmall); } - - // Derive shared secret via ECDH (try both parities) - let (shared_even, _shared_odd) = ecdh_shared_secret_both(sender_sk, recipient_pk)?; - // Use even parity by default (most common in Nostr) - let conv_key = derive_conversation_key(&shared_even); - - // Encrypt with ChaCha20-Poly1305 - let cipher = ChaCha20Poly1305::new_from_slice(&conv_key) - .map_err(|_| NostrError::Nip44InvalidFormat)?; - - // Generate random nonce - let mut nonce_bytes = [0u8; NONCE_SIZE]; - OsRng.fill_bytes(&mut nonce_bytes); - let nonce = Nonce::from_slice(&nonce_bytes); - - let ciphertext = cipher - .encrypt(nonce, plaintext) - .map_err(|_| NostrError::Nip44InvalidFormat)?; - - // Prepend nonce to ciphertext - let mut result = Vec::with_capacity(NONCE_SIZE + ciphertext.len()); - result.extend_from_slice(&nonce_bytes); - result.extend_from_slice(&ciphertext); - - Ok(result) + let mut nonce = [0u8; NONCE_SIZE]; + OsRng.fill_bytes(&mut nonce); + nip44_encrypt_with_nonce(sender_sk, recipient_pk, plaintext, &nonce) } -/// Decrypt a NIP-44 encrypted message. +/// Decrypt a NIP-44 v2 payload. /// -/// Expects ciphertext in the format: nonce (12 bytes) || encrypted_data || tag. +/// Expects the raw binary payload: `version || nonce || ciphertext || mac`. pub fn nip44_decrypt( recipient_sk: &SecretKey, sender_pk: &PublicKey, - ciphertext: &[u8], + payload: &[u8], ) -> NostrResult> { - if ciphertext.len() < NONCE_SIZE + 16 { - // Minimum: nonce + tag + if payload.len() < MIN_PAYLOAD_SIZE { + return Err(NostrError::Nip44InvalidFormat); + } + if payload[0] != VERSION { return Err(NostrError::Nip44InvalidFormat); } - // Derive shared secret via ECDH (try both parities) - let (shared_even, shared_odd) = ecdh_shared_secret_both(recipient_sk, sender_pk)?; + let nonce: [u8; NONCE_SIZE] = payload[1..1 + NONCE_SIZE] + .try_into() + .map_err(|_| NostrError::Nip44InvalidFormat)?; + let mac_start = payload.len() - MAC_SIZE; + let ciphertext = &payload[1 + NONCE_SIZE..mac_start]; + let received_mac = &payload[mac_start..]; - // Try even parity first, then odd - for shared_secret in [shared_even, shared_odd] { - let conv_key = derive_conversation_key(&shared_secret); - let cipher = match ChaCha20Poly1305::new_from_slice(&conv_key) { - Ok(c) => c, - Err(_) => continue, - }; - let nonce = Nonce::from_slice(&ciphertext[..NONCE_SIZE]); - let encrypted = &ciphertext[NONCE_SIZE..]; - if let Ok(plaintext) = cipher.decrypt(nonce, encrypted) { - return Ok(plaintext); - } + let conversation_key = get_conversation_key(recipient_sk, sender_pk)?; + let (chacha_key, chacha_nonce, hmac_key) = get_message_keys(&conversation_key, &nonce); + + // Verify HMAC over nonce || ciphertext (constant-time). + let mut aad = Vec::with_capacity(NONCE_SIZE + ciphertext.len()); + aad.extend_from_slice(&nonce); + aad.extend_from_slice(ciphertext); + let computed_mac = hmac_sha256(&hmac_key, &aad); + if !constant_time_eq(&computed_mac, received_mac) { + return Err(NostrError::Nip44DecryptFailed); } - Err(NostrError::Nip44DecryptFailed) + + // Decrypt with ChaCha20 (counter 0). + let mut padded = ciphertext.to_vec(); + let mut cipher = ChaCha20::new(&chacha_key.into(), &chacha_nonce.into()); + cipher.apply_keystream(&mut padded); + + unpad_plaintext(&padded) } -/// Encrypt with a specific nonce (for testing/reproducibility). -pub fn nip44_encrypt_with_nonce( - sender_sk: &SecretKey, - recipient_pk: &PublicKey, - plaintext: &[u8], - nonce: &[u8; NONCE_SIZE], -) -> NostrResult> { - if plaintext.len() > MAX_PLAINTEXT_SIZE { - return Err(NostrError::Nip44BufferTooSmall); +/// Constant-time byte comparison. +fn constant_time_eq(a: &[u8], b: &[u8]) -> bool { + if a.len() != b.len() { + return false; } - - let (shared_even, _) = ecdh_shared_secret_both(sender_sk, recipient_pk)?; - let conv_key = derive_conversation_key(&shared_even); - - let cipher = ChaCha20Poly1305::new_from_slice(&conv_key) - .map_err(|_| NostrError::Nip44InvalidFormat)?; - - let ciphertext = cipher - .encrypt(Nonce::from_slice(nonce), plaintext) - .map_err(|_| NostrError::Nip44InvalidFormat)?; - - let mut result = Vec::with_capacity(NONCE_SIZE + ciphertext.len()); - result.extend_from_slice(nonce); - result.extend_from_slice(&ciphertext); - - Ok(result) + let mut diff = 0u8; + for (x, y) in a.iter().zip(b.iter()) { + diff |= x ^ y; + } + diff == 0 } #[cfg(test)] @@ -142,20 +230,169 @@ mod tests { use super::*; use crate::crypto::keys::generate_keypair; + fn hex32(s: &str) -> [u8; 32] { + let v = hex::decode(s).unwrap(); + let mut a = [0u8; 32]; + a.copy_from_slice(&v); + a + } + #[test] - fn test_nip44_encrypt_decrypt() { + fn test_calc_padded_len_vectors() { + // Official NIP-44 v2 test vectors. + let vectors: &[(usize, usize)] = &[ + (16, 32), + (32, 32), + (33, 64), + (37, 64), + (45, 64), + (49, 64), + (64, 64), + (65, 96), + (100, 128), + (111, 128), + (200, 224), + (250, 256), + (320, 320), + (383, 384), + (384, 384), + (400, 448), + (500, 512), + (512, 512), + (515, 640), + (700, 768), + (800, 896), + (900, 1024), + (1020, 1024), + (65536, 65536), + ]; + for (input, expected) in vectors { + assert_eq!(calc_padded_len(*input), *expected, "input {}", input); + } + } + + #[test] + fn test_get_conversation_key_vectors() { + // Official NIP-44 v2 test vectors. + let vectors: &[(&str, &str, &str)] = &[ + ( + "315e59ff51cb9209768cf7da80791ddcaae56ac9775eb25b6dee1234bc5d2268", + "c2f9d9948dc8c7c38321e4b85c8558872eafa0641cd269db76848a6073e69133", + "3dfef0ce2a4d80a25e7a328accf73448ef67096f65f79588e358d9a0eb9013f1", + ), + ( + "a1e37752c9fdc1273be53f68c5f74be7c8905728e8de75800b94262f9497c86e", + "03bb7947065dde12ba991ea045132581d0954f042c84e06d8c00066e23c1a800", + "4d14f36e81b8452128da64fe6f1eae873baae2f444b02c950b90e43553f2178b", + ), + ( + "98a5902fd67518a0c900f0fb62158f278f94a21d6f9d33d30cd3091195500311", + "aae65c15f98e5e677b5050de82e3aba47a6fe49b3dab7863cf35d9478ba9f7d1", + "9c00b769d5f54d02bf175b7284a1cbd28b6911b06cda6666b2243561ac96bad7", + ), + ]; + for (sec1, pub2, expected) in vectors { + let sk = SecretKey::from_bytes(hex32(sec1)); + let pk = PublicKey::from_bytes(hex32(pub2)); + let ck = get_conversation_key(&sk, &pk).unwrap(); + assert_eq!(hex::encode(ck), *expected); + } + } + + #[test] + fn test_get_message_keys_vector() { + // Official NIP-44 v2 test vector. + let conversation_key = hex32("a1a3d60f3470a8612633924e91febf96dc5366ce130f658b1f0fc652c20b3b54"); + let nonce = hex32("e1e6f880560d6d149ed83dcc7e5861ee62a5ee051f7fde9975fe5d25d2a02d72"); + let (chacha_key, chacha_nonce, hmac_key) = get_message_keys(&conversation_key, &nonce); + assert_eq!( + hex::encode(chacha_key), + "f145f3bed47cb70dbeaac07f3a3fe683e822b3715edb7c4fe310829014ce7d76" + ); + assert_eq!(hex::encode(chacha_nonce), "c4ad129bb01180c0933a160c"); + assert_eq!( + hex::encode(hmac_key), + "027c1db445f05e2eee864a0975b0ddef5b7110583c8c192de3732571ca5838c4" + ); + } + + #[test] + fn test_encrypt_decrypt_vectors() { + // Official NIP-44 v2 test vectors (sec1, sec2, nonce, plaintext, payload). + let vectors: &[(&str, &str, &str, &str, &str)] = &[ + ( + "0000000000000000000000000000000000000000000000000000000000000001", + "0000000000000000000000000000000000000000000000000000000000000002", + "0000000000000000000000000000000000000000000000000000000000000001", + "a", + "AgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABee0G5VSK0/9YypIObAtDKfYEAjD35uVkHyB0F4DwrcNaCXlCWZKaArsGrY6M9wnuTMxWfp1RTN9Xga8no+kF5Vsb", + ), + ( + "0000000000000000000000000000000000000000000000000000000000000002", + "0000000000000000000000000000000000000000000000000000000000000001", + "f00000000000000000000000000000f00000000000000000000000000000000f", + "🍕🫃", + "AvAAAAAAAAAAAAAAAAAAAPAAAAAAAAAAAAAAAAAAAAAPSKSK6is9ngkX2+cSq85Th16oRTISAOfhStnixqZziKMDvB0QQzgFZdjLTPicCJaV8nDITO+QfaQ61+KbWQIOO2Yj", + ), + ]; + for (sec1, sec2, nonce, plaintext, payload_b64) in vectors { + let sk1 = SecretKey::from_bytes(hex32(sec1)); + let sk2 = SecretKey::from_bytes(hex32(sec2)); + let pk1 = crate::crypto::keys::public_key_from_secret_key(&sk1).unwrap(); + let pk2 = crate::crypto::keys::public_key_from_secret_key(&sk2).unwrap(); + let nonce_arr = hex32(nonce); + + // Encrypt must reproduce the exact payload. + let produced = + nip44_encrypt_with_nonce(&sk1, &pk2, plaintext.as_bytes(), &nonce_arr).unwrap(); + assert_eq!( + crate::util::base64_encode(&produced), + *payload_b64, + "encrypt mismatch for plaintext {:?}", + plaintext + ); + + // Decrypt the reference payload. + let raw = crate::util::base64_decode(payload_b64).unwrap(); + let decrypted = nip44_decrypt(&sk2, &pk1, &raw).unwrap(); + assert_eq!(String::from_utf8(decrypted).unwrap(), *plaintext); + } + } + + #[test] + fn test_roundtrip() { let (sk_a, pk_a) = generate_keypair(); let (sk_b, pk_b) = generate_keypair(); let plaintext = b"Hello, Nostr! This is a secret message."; let encrypted = nip44_encrypt(&sk_a, &pk_b, plaintext).unwrap(); let decrypted = nip44_decrypt(&sk_b, &pk_a, &encrypted).unwrap(); - assert_eq!(decrypted, plaintext); } #[test] - fn test_nip44_max_plaintext() { + fn test_roundtrip_unicode() { + let (sk_a, pk_a) = generate_keypair(); + let (sk_b, pk_b) = generate_keypair(); + + let plaintext = "Hello 🌍 World! 🚀".as_bytes(); + let encrypted = nip44_encrypt(&sk_a, &pk_b, plaintext).unwrap(); + let decrypted = nip44_decrypt(&sk_b, &pk_a, &encrypted).unwrap(); + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_roundtrip_empty() { + let (sk_a, pk_a) = generate_keypair(); + let (sk_b, pk_b) = generate_keypair(); + + let encrypted = nip44_encrypt(&sk_a, &pk_b, b"").unwrap(); + let decrypted = nip44_decrypt(&sk_b, &pk_a, &encrypted).unwrap(); + assert_eq!(decrypted, b""); + } + + #[test] + fn test_max_plaintext() { let (sk_a, pk_a) = generate_keypair(); let (sk_b, pk_b) = generate_keypair(); @@ -166,21 +403,29 @@ mod tests { } #[test] - fn test_nip44_overflow() { + fn test_overflow() { let (sk, pk) = generate_keypair(); let plaintext = vec![0x42u8; MAX_PLAINTEXT_SIZE + 1]; assert!(nip44_encrypt(&sk, &pk, &plaintext).is_err()); } #[test] - fn test_nip44_wrong_key() { + fn test_wrong_key() { let (sk_a, _) = generate_keypair(); - let (sk_b, pk_b) = generate_keypair(); + let (_sk_b, pk_b) = generate_keypair(); let (sk_c, _) = generate_keypair(); let plaintext = b"secret"; let encrypted = nip44_encrypt(&sk_a, &pk_b, plaintext).unwrap(); - // Decrypting with wrong key should fail assert!(nip44_decrypt(&sk_c, &pk_b, &encrypted).is_err()); } + + #[test] + fn test_bad_version() { + let (sk_a, pk_a) = generate_keypair(); + let (sk_b, pk_b) = generate_keypair(); + let mut encrypted = nip44_encrypt(&sk_a, &pk_b, b"hi").unwrap(); + encrypted[0] = 0x01; + assert!(nip44_decrypt(&sk_b, &pk_a, &encrypted).is_err()); + } }