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
This commit is contained in:
Avi 2026-08-25 14:23:26 -05:00
commit fa5ba08578
8 changed files with 344 additions and 232 deletions

353
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -102,32 +102,30 @@ async fn contact_pubkeys(settings: &Settings, owner_hex: &str) -> Result<Vec<Pub
crate::relays::open_pool(Keys::generate(), &relay_urls, Some(CONNECT_TIMEOUT)).await?;
let filter = Filter::new().kind(Kind::ContactList).author(owner);
client
.subscribe(filter, None)
let mut events = client
.stream_events(filter)
.await
.map_err(|e| AppError::network(format!("Could not subscribe for contacts: {e}")))?;
let mut contacts: HashSet<PublicKey> = 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();

View file

@ -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;
}

View file

@ -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);
(

View file

@ -79,16 +79,21 @@ pub(crate) async fn open_pool(
relay_urls: &[String],
wait: Option<Duration>,
) -> Result<Client, AppError> {
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<RelayTestResult, AppError> {
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<RelayTestRe
let relay = client
.relay(url)
.await
.map_err(|e| AppError::network(format!("Could not look up relay {url}: {e}")))?;
.map_err(|e| AppError::network(format!("Could not look up relay {url}: {e}")))?
.ok_or_else(|| AppError::network(format!("Relay {url} is not in the pool")))?;
relay
.try_connect(timeout)
.try_connect()
.timeout(timeout)
.await
.map_err(|e| AppError::network(format!("Connection failed: {e}")))?;

View file

@ -17,9 +17,9 @@ use std::time::Duration;
use base64::engine::general_purpose::STANDARD as B64;
use base64::Engine;
use getrandom::getrandom;
use nostr::nips::nip44::v2;
use nostr::nips::nip44::v2::ConversationKey;
use nostr::JsonUtil;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tokio::sync::oneshot;
@ -363,7 +363,11 @@ fn percent_decode(raw: &str) -> Option<String> {
/// NIP-44 encrypt with the conversation key, returned base64-encoded.
fn nip44_encrypt(conversation: &ConversationKey, plaintext: &str) -> Result<String, AppError> {
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<String, String> {
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<tokio::sync::Mutex<App>>, 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<tokio::sync::Mutex<App>>, 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(

View file

@ -1,3 +1,4 @@
use nostr::nips::nip98::{HttpData, HttpMethod};
use nostr_sdk::prelude::*;
use crate::crypto::VaultKey;