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).
This commit is contained in:
Laan Tungir
2026-09-27 10:24:00 -04:00
parent 0b7085912c
commit 16253cea18
4 changed files with 361 additions and 113 deletions
Generated
+10 -9
View File
@@ -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",
+1
View File
@@ -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"
+1
View File
@@ -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
+349 -104
View File
@@ -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
//! (<https://github.com/nostr-protocol/nips/blob/master/44.md>).
//!
//! 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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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<Vec<u8>> {
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());
}
}