From fa5ba08578316299c174e6ca1f4d5cd182942907 Mon Sep 17 00:00:00 2001 From: Avi Date: Tue, 25 Aug 2026 14:23:26 -0500 Subject: [PATCH] Security: upgrade nostr stack 0.40->0.45 clearing 11 RustSec advisories - nostr 0.40.0 -> 0.45.3, nostr-sdk 0.40.0 -> 0.45.2 (secp256k1 0.30) - fixes RUSTSEC-2026-0216/0219/0224/0225/0226/0227/0228/0229/0230/0231/0232 incl. forged-event signature-bypass and NIP-46 credential Debug leak - NIP-42 auth now explicit: SignerAuthenticator on every client path - EventBuilder::sign -> finalize_async; nip44 encrypt supplies random nonce - feed/signer receive loops moved to stream_events (receiver opens before the signer announces, preserving the ordering fix) - relay-pool replaced by in-SDK relay/gossip; try_connect().timeout() - tests: malformed NIP-44 payload rejection + secret-leak regression - suite: 115 backend tests green (113 preserved + 2 new); frontend untouched --- Cargo.lock | 353 ++++++++++++++++++++++++------------------------ Cargo.toml | 4 +- src/feed.rs | 55 ++++---- src/profiles.rs | 9 +- src/publish.rs | 12 +- src/relays.rs | 23 +++- src/signer.rs | 123 ++++++++++++++--- src/uploads.rs | 1 + 8 files changed, 346 insertions(+), 234 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c501d9c..b3bd988 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,15 +37,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "aho-corasick" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" -dependencies = [ - "memchr", -] - [[package]] name = "argon2" version = "0.5.3" @@ -78,29 +69,24 @@ dependencies = [ [[package]] name = "async-wsocket" -version = "0.13.2" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c92385c7c8b3eb2de1b78aeca225212e4c9a69a78b802832759b108681a5069" +checksum = "2c713e1f14c7b82e32ea159af1c6e2f070cfadbdf23fb2512acce9af0a26f1a2" dependencies = [ - "async-utility", "futures", "futures-util", "js-sys", "tokio", + "tokio-happy-eyeballs", "tokio-rustls", "tokio-socks", "tokio-tungstenite", "url", "wasm-bindgen", + "wasm-bindgen-futures", "web-sys", ] -[[package]] -name = "atomic-destructor" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef49f5882e4b6afaac09ad239a4f8c70a24b8f2b0897edb1f706008efd109cf4" - [[package]] name = "base64" version = "0.22.1" @@ -115,9 +101,9 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bech32" -version = "0.11.1" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" +checksum = "efbd3e1070bbdf4cd88a75264e18e8a26f7cb5c6949eadf0ceb85fb159cf08f8" [[package]] name = "bip39" @@ -125,7 +111,7 @@ version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.14.101", "serde", "unicode-normalization", ] @@ -164,6 +150,17 @@ checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" dependencies = [ "bitcoin-io", "hex-conservative 0.2.2", +] + +[[package]] +name = "bitcoin_hashes" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5304e53726dbe5f93141535e102ed97b5bf4714fbecefdda8f9fb98d7fdaff0e" +dependencies = [ + "bitcoin-consensus-encoding", + "bitcoin-internals", + "hex-conservative 1.2.0", "serde", ] @@ -206,6 +203,12 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.12.1" @@ -345,6 +348,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "faster-hex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7223ae2d2f179b803433d9c830478527e92b8117eab39460edae7f1614d9fb73" +dependencies = [ + "heapless", + "serde", +] + [[package]] name = "find-msvc-tools" version = "0.1.11" @@ -368,6 +381,7 @@ checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", + "futures-executor", "futures-io", "futures-sink", "futures-task", @@ -390,12 +404,34 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +[[package]] +name = "futures-executor" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.4", +] + [[package]] name = "futures-sink" version = "0.3.34" @@ -417,6 +453,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -441,10 +478,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", - "js-sys", "libc", "wasi", - "wasm-bindgen", ] [[package]] @@ -468,6 +503,7 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", ] [[package]] @@ -492,6 +528,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + +[[package]] +name = "heapless" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bfb9eb618601c89945a70e254898da93b13be0388091d42117462b265bb3fad" +dependencies = [ + "hash32", + "stable_deref_trait", +] + [[package]] name = "hex" version = "0.4.3" @@ -516,15 +571,6 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - [[package]] name = "http" version = "1.5.0" @@ -655,18 +701,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "instant" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "itoa" version = "1.0.18" @@ -732,9 +766,9 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru" -version = "0.13.0" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "227748d55f2f0ab4735d87fd623798cb6b664512fe979705f829c9f81c934465" +checksum = "5d2f2f9b4ba7e6b24d95e7e899329d35be83bcded72c8540cdd5368932d1d90a" [[package]] name = "memchr" @@ -753,12 +787,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "negentropy" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e664971378a3987224f7a0e10059782035e89899ae403718ee07de85bec42afe" - [[package]] name = "negentropy" version = "0.5.1" @@ -767,70 +795,70 @@ checksum = "81c353b400a5503efdcf398f11a83fb7aa84f59f5d76fc4bf5bbc1e4f5366caa" [[package]] name = "nostr" -version = "0.40.0" +version = "0.45.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f900ddcdc28395759fcd44b18a03255e7deee8858551bfe5d5d5a07311d82ea" +checksum = "b0ba32ce43631188586469ba1a4c40bcfa63641f1ad5de89ef77f74d801cc17a" dependencies = [ - "aes", "base64", "bech32", "bip39", - "bitcoin_hashes", + "bitcoin_hashes 1.2.0", "cbc", "chacha20", "chacha20poly1305", - "getrandom 0.2.17", - "instant", - "regex", - "scrypt", + "faster-hex", + "opaquerr", + "rand 0.10.2", "secp256k1", "serde", "serde_json", "unicode-normalization", + "universal-time", "url", + "zeroize", ] [[package]] name = "nostr-database" -version = "0.40.0" +version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714512e4653f4e7c4f4abb50a0ac82257541b22087dee780b9e3d787276e88d4" +checksum = "4b1fdb9fcba732e32719662afad1b267e50322dbe89e506017ec13f24361bddf" dependencies = [ - "lru", "nostr", - "tokio", + "opaquerr", ] [[package]] -name = "nostr-relay-pool" -version = "0.40.1" +name = "nostr-gossip" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bde07a729e0a1b306c9a07da81a0d1d55d0487316017090906f3b6660741b8d" +checksum = "fa07539e52a71cb91fe0d693facaa298f03fcf9edcd66a521094e18e286e2336" dependencies = [ - "async-utility", - "async-wsocket", - "atomic-destructor", - "lru", - "negentropy 0.3.1", - "negentropy 0.5.1", "nostr", - "nostr-database", - "tokio", - "tracing", + "opaquerr", ] [[package]] name = "nostr-sdk" -version = "0.40.0" +version = "0.45.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26238eee805d7dc3abcc8d570068c81cb4285b08e9db4d7999e54e20748c472e" +checksum = "245f8642b10feecb0a40739a886ca1850e3be4e35dc6592b806ec437403ab9c9" dependencies = [ "async-utility", + "async-wsocket", + "faster-hex", + "futures", + "lru", + "negentropy", "nostr", "nostr-database", - "nostr-relay-pool", + "nostr-gossip", + "opaquerr", + "rand 0.10.2", "tokio", + "tokio-stream", "tracing", + "universal-time", ] [[package]] @@ -845,6 +873,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "opaquerr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f933a4265d5cdad61d19bbdfc972ea5726d56cd8d3d57b8f2d3c365dd42bee9" + [[package]] name = "parking_lot" version = "0.12.5" @@ -879,16 +913,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest", - "hmac", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -974,9 +998,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -993,6 +1017,16 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "getrandom 0.4.3", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -1031,6 +1065,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1040,35 +1080,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" - [[package]] name = "ring" version = "0.17.14" @@ -1144,42 +1155,21 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scrypt" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" -dependencies = [ - "password-hash", - "pbkdf2", - "salsa20", - "sha2", -] - [[package]] name = "secp256k1" -version = "0.29.1" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ - "rand 0.8.7", + "bitcoin_hashes 0.14.101", + "rand 0.8.8", "secp256k1-sys", - "serde", ] [[package]] @@ -1245,17 +1235,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - [[package]] name = "shlex" version = "2.0.1" @@ -1421,6 +1400,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tokio-happy-eyeballs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8564c32dfb6f4257f8bc6edfc178a34af97520e0b7b9815500c55eb3d092f29f" +dependencies = [ + "tokio", +] + [[package]] name = "tokio-macros" version = "2.7.2" @@ -1455,10 +1443,22 @@ dependencies = [ ] [[package]] -name = "tokio-tungstenite" -version = "0.26.2" +name = "tokio-stream" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", @@ -1470,6 +1470,19 @@ dependencies = [ "webpki-roots 0.26.11", ] +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "tracing" version = "0.1.44" @@ -1477,21 +1490,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "tracing-core" version = "0.1.36" @@ -1503,9 +1504,9 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.26.2" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", @@ -1551,6 +1552,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "universal-time" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a939edecc3c5a7b83c02e5f6b3c31d2bc69eabcc9a87ab12c6d37ee6dbc856" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/Cargo.toml b/Cargo.toml index e7b4fd0..f30d41b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -nostr = { version = "0.40", features = ["nip44", "nip46"] } -nostr-sdk = { version = "0.40", features = ["nip44", "nip98"] } +nostr = { version = "0.45", features = ["nip44", "nip98"] } +nostr-sdk = "0.45" tokio = { version = "1", features = ["full"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" diff --git a/src/feed.rs b/src/feed.rs index 71c866f..2674a62 100644 --- a/src/feed.rs +++ b/src/feed.rs @@ -102,32 +102,30 @@ async fn contact_pubkeys(settings: &Settings, owner_hex: &str) -> Result = HashSet::new(); - let mut notifications = client.notifications(); let deadline = tokio::time::Instant::now() + QUERY_TIMEOUT; loop { - match tokio::time::timeout_at(deadline, notifications.recv()).await { - Ok(Ok(RelayPoolNotification::Event { event, .. })) => { + match tokio::time::timeout_at(deadline, events.next()).await { + Ok(Some((_, Ok(event)))) => { if event.kind == Kind::ContactList { - for pubkey in event - .tags - .iter() - .filter_map(|tag| match tag.as_standardized() { - Some(TagStandard::PublicKey { public_key, .. }) => Some(*public_key), - _ => None, - }) - { - contacts.insert(pubkey); + for tag in event.tags.iter() { + if tag.single_letter_tag().map(|s| s.as_char()) == Some('p') { + if let Some(content) = tag.content() { + if let Ok(pubkey) = PublicKey::parse(content) { + contacts.insert(pubkey); + } + } + } } } } - Ok(Ok(_)) => continue, - Ok(Err(_)) | Err(_) => break, + Ok(Some((_, Err(_)))) => continue, + Ok(None) | Err(_) => break, } } @@ -163,25 +161,22 @@ async fn aggregate_for( Some(authors) => filter.authors(authors.iter().copied()), None => filter, }; - client - .subscribe(filter, None) + let mut events = client + .stream_events(filter) .await .map_err(|e| AppError::network(format!("Could not subscribe for the feed: {e}")))?; let mut feed = FeedBuilder::new(effective_limit, authors.as_deref()); - let mut notifications = client.notifications(); let deadline = tokio::time::Instant::now() + QUERY_TIMEOUT; loop { - match tokio::time::timeout_at(deadline, notifications.recv()).await { - Ok(Ok(RelayPoolNotification::Event { - event, relay_url, .. - })) => { + match tokio::time::timeout_at(deadline, events.next()).await { + Ok(Some((relay_url, Ok(event)))) => { if !feed.add(&event, Some(relay_url)) { break; } } - Ok(Ok(_)) => continue, - Ok(Err(_)) | Err(_) => break, + Ok(Some((_, Err(_)))) => continue, + Ok(None) | Err(_) => break, } } @@ -262,7 +257,7 @@ impl FeedItem { author: event.pubkey.to_hex(), author_npub, content: event.content.trim().to_string(), - created_at: event.created_at.as_u64(), + created_at: event.created_at.as_secs(), relays: relay.map(|url| vec![url.to_string()]).unwrap_or_default(), }) } @@ -276,7 +271,7 @@ mod tests { let keys = Keys::generate(); EventBuilder::new(Kind::TextNote, content.to_string()) .custom_created_at(Timestamp::from(created_at)) - .sign(&keys) + .finalize_async(&keys) .await .unwrap() } @@ -333,7 +328,7 @@ mod tests { let mut builder = FeedBuilder::new(10, None); let keys = Keys::generate(); let other = EventBuilder::new(Kind::Metadata, "{}") - .sign(&keys) + .finalize_async(&keys) .await .unwrap(); builder.add(&other, None); @@ -363,12 +358,12 @@ mod tests { let from_followed = EventBuilder::new(Kind::TextNote, "from a contact".to_string()) .custom_created_at(Timestamp::from(5)) - .sign(&followed) + .finalize_async(&followed) .await .unwrap(); let from_stranger = EventBuilder::new(Kind::TextNote, "from a stranger".to_string()) .custom_created_at(Timestamp::from(6)) - .sign(&stranger) + .finalize_async(&stranger) .await .unwrap(); diff --git a/src/profiles.rs b/src/profiles.rs index 3ef172f..b6ba18a 100644 --- a/src/profiles.rs +++ b/src/profiles.rs @@ -431,7 +431,7 @@ async fn publish_metadata_async( metadata = metadata.nip05(nip05); } let event = match EventBuilder::new(Kind::Metadata, metadata.as_json()) - .sign(keys) + .finalize_async(keys) .await { Ok(event) => event, @@ -450,7 +450,12 @@ async fn publish_metadata_async( } }; - let client = Client::new(keys.clone()); + // This path deliberately builds its own client instead of + // `relays::open_pool`: adding a broken relay must not abort the whole + // metadata publish, so add errors are ignored here. + let client = Client::builder() + .authenticator(SignerAuthenticator::new(keys.clone())) + .build(); for url in &relay_urls { let _ = client.add_relay(url.as_str()).await; } diff --git a/src/publish.rs b/src/publish.rs index 88c6c67..76196b3 100644 --- a/src/publish.rs +++ b/src/publish.rs @@ -167,7 +167,7 @@ async fn publish_with_keys( // reported as such rather than as a network error. let builder = EventBuilder::new(Kind::TextNote, content.to_string()).tags(image_tags(content)); let event = builder - .sign(keys) + .finalize_async(keys) .await .map_err(|e| AppError::sign_failed(format!("{e}")))?; @@ -220,7 +220,7 @@ pub(crate) async fn send_to_all_relays( let noun = noun.clone(); sends.spawn(async move { match client.relay(url.as_str()).await { - Ok(relay) => { + Ok(Some(relay)) => { match tokio::time::timeout(RELAY_SEND_TIMEOUT, relay.send_event(&event)).await { Ok(Ok(_)) => (index, Ok(url)), Ok(Err(err)) => { @@ -246,6 +246,14 @@ pub(crate) async fn send_to_all_relays( ), } } + Ok(None) => ( + index, + Err(RelayFailure { + url, + error: "Relay is not in the active pool.".to_string(), + details: Some("The client dropped this relay before sending.".to_string()), + }), + ), Err(err) => { let (message, details) = relay_error_message(&err); ( diff --git a/src/relays.rs b/src/relays.rs index 066c416..303698c 100644 --- a/src/relays.rs +++ b/src/relays.rs @@ -79,16 +79,21 @@ pub(crate) async fn open_pool( relay_urls: &[String], wait: Option, ) -> Result { - let client = Client::new(keys); + // The authenticator answers NIP-42 AUTH challenges automatically on every + // path that opens a client (nostr-sdk >= 0.45 has no implicit signer). + let client = Client::builder() + .authenticator(SignerAuthenticator::new(keys)) + .build(); for url in relay_urls { client .add_relay(url.as_str()) .await .map_err(|e| AppError::network(format!("Could not add relay {url}: {e}")))?; } - client.connect().await; - if let Some(timeout) = wait { - client.wait_for_connection(timeout).await; + // Deprecated `wait_for_connection`; the builder form is the 0.45 way. + match wait { + Some(timeout) => client.connect().and_wait(timeout).await, + None => client.connect().await, } Ok(client) } @@ -107,7 +112,9 @@ pub struct RelayTestResult { /// during a connection test. pub async fn test_connection(url: &str, timeout: Duration) -> Result { let keys = Keys::generate(); - let client = Client::new(keys); + let client = Client::builder() + .authenticator(SignerAuthenticator::new(keys)) + .build(); client .add_relay(url) @@ -117,10 +124,12 @@ pub async fn test_connection(url: &str, timeout: Duration) -> Result Option { /// NIP-44 encrypt with the conversation key, returned base64-encoded. fn nip44_encrypt(conversation: &ConversationKey, plaintext: &str) -> Result { - let payload = v2::encrypt_to_bytes(conversation, plaintext.as_bytes()) + // nostr-sdk 0.45 removed the convenience wrapper that generated the nonce + // internally; the caller now supplies fresh randomness per message. + let mut nonce = [0u8; 32]; + getrandom(&mut nonce).map_err(|e| AppError::internal(format!("Could not get entropy: {e}")))?; + let payload = v2::encrypt_to_bytes_with_nonce(conversation, plaintext.as_bytes(), nonce) .map_err(|e| AppError::internal(format!("Could not encrypt a message: {e}")))?; Ok(B64.encode(payload)) } @@ -570,8 +574,8 @@ fn sign_event(keys: &Keys, request: &RawRequest) -> Result { let unsigned: UnsignedEvent = serde_json::from_value(value).map_err(|e| format!("Invalid event: {e}"))?; - let event = unsigned - .sign_with_keys(keys) + let event = keys + .sign_event(unsigned) .map_err(|e| format!("The event could not be signed: {e}"))?; Ok(event.as_json()) } @@ -631,23 +635,30 @@ async fn run_sign_task(signer: Signer, app: Arc>, uri: C // announcement within milliseconds, and a receiver created afterwards // would miss those early messages (tokio broadcast semantics), leaving // the client waiting forever for a reply. - let client = Client::new(keys.clone()); + let client = Client::builder() + .authenticator(SignerAuthenticator::new(keys.clone())) + .build(); for url in &uri.relays { if let Err(err) = client.add_relay(url.to_string()).await { signer.fail(format!("Could not add relay {url}: {err}")); return; } } - let mut notifications = client.notifications(); - client.connect().await; - client.wait_for_connection(CONNECT_TIMEOUT).await; + client.connect().and_wait(CONNECT_TIMEOUT).await; // 4. Subscribe to the client's kind 24133 events so we hear its requests. + // `stream_events` opens its internal notification receiver as part of + // subscribing, which must happen BEFORE we announce (step 5): the client + // acknowledges within milliseconds and a receiver created afterwards would + // miss those early messages (tokio broadcast semantics). let filter = Filter::new().kind(Kind::NostrConnect).author(uri.peer); - if let Err(err) = client.subscribe(filter, None).await { - signer.fail(format!("Could not subscribe for messages: {err}")); - return; - } + let mut events = match client.stream_events(filter).await { + Ok(events) => events, + Err(err) => { + signer.fail(format!("Could not subscribe for messages: {err}")); + return; + } + }; // 5. Announce ourselves: send the connect request with the optional secret. if let Err(err) = send_connect(&client, &keys, &conversation, &uri).await { @@ -657,16 +668,18 @@ async fn run_sign_task(signer: Signer, app: Arc>, uri: C // 6. Answer requests until the connection goes away or we are stopped. loop { - let notification = match notifications.recv().await { - Ok(notification) => notification, - Err(_) => { + let (relay_url, incoming) = match events.next().await { + Some(next) => next, + None => { signer.fail("The signer connection was closed."); return; } }; - let RelayPoolNotification::Event { event, .. } = notification else { - continue; + let event = match incoming { + Ok(event) => event, + Err(_) => continue, }; + let _ = relay_url; if event.kind != Kind::NostrConnect || event.pubkey != uri.peer { continue; } @@ -728,7 +741,7 @@ async fn publish_payload( let tag = Tag::parse(["p", peer.to_hex().as_str()]).map_err(|e| format!("{e}"))?; let event = EventBuilder::new(Kind::NostrConnect, content) .tags([tag]) - .sign(keys) + .finalize_async(keys) .await .map_err(|e| format!("Could not sign a message: {e}"))?; client @@ -742,6 +755,80 @@ async fn publish_payload( mod tests { use super::*; + /// Malformed NIP-44 payloads (RUSTSEC-2026-0216/0227 classes) must come + /// back as clean errors, never a panic or a hang. A payload that fails to + /// decrypt against the conversation key is also exactly how forged signer + /// messages from a non-peer key are rejected. + #[test] + fn nip44_decrypt_rejects_malformed_payloads() { + let signer = Keys::generate(); + let peer = Keys::generate(); + let conversation = + ConversationKey::derive(signer.secret_key(), &peer.public_key()).unwrap(); + + let not_base64 = "this is !! not base64 !!"; + let empty = ""; + let too_short = B64.encode([0x02u8, 0x00]); + let bad_version = B64.encode([0xFFu8, 0x00, 0x11]); + let truncated = { + // encrypt returns raw bytes; nip44_decrypt takes their base64 form. + let mut bytes = v2::encrypt_to_bytes_with_nonce( + &conversation, + "a message long enough to be truncated meaningfully".as_bytes(), + [7u8; 32], + ) + .unwrap(); + bytes.truncate(bytes.len() / 2); + B64.encode(&bytes) + }; + + for payload in [ + not_base64, + empty, + &too_short, + &bad_version, + truncated.as_str(), + ] { + let result = nip44_decrypt(&conversation, payload); + assert!(result.is_err(), "payload {payload:?} must be rejected"); + if let Err(err) = result { + assert!( + !err.message().is_empty(), + "rejection of {payload:?} must carry a concise reason" + ); + } + } + } + + /// NIP-46 credential-leakage regression (RUSTSEC-2026-0225 class): error + /// strings surfaced to callers or logs must never contain secret-key hex. + #[test] + fn error_paths_never_leak_secret_key_material() { + let signer = Keys::generate(); + let sk_hex = hex::encode(signer.secret_key().secret_bytes()); + assert_eq!(sk_hex.len(), 64); + let peer = Keys::generate(); + let conversation = + ConversationKey::derive(signer.secret_key(), &peer.public_key()).unwrap(); + + // Collect the human-readable reasons our failure paths produce. + let mut failures = Vec::new(); + if let Err(err) = nip44_decrypt(&conversation, "not-base64 !!!") { + failures.push(err.message().to_string()); + } + if let Err(err) = nip44_decrypt(&conversation, &B64.encode([0xFFu8, 0x00, 0x11])) { + failures.push(err.message().to_string()); + } + + assert!(!failures.is_empty(), "expected at least one failure path"); + for message in failures { + assert!( + !message.to_lowercase().contains(&sk_hex), + "error text leaked secret-key material: {message}" + ); + } + } + #[test] fn parse_uri_with_relays_and_secret() { let uri = parse_connect_uri( diff --git a/src/uploads.rs b/src/uploads.rs index 39622ff..a1ed3ac 100644 --- a/src/uploads.rs +++ b/src/uploads.rs @@ -1,3 +1,4 @@ +use nostr::nips::nip98::{HttpData, HttpMethod}; use nostr_sdk::prelude::*; use crate::crypto::VaultKey;