[verified] Add QR codec and ratatui roster.
M2: onionwire:v1 signed payload, Unicode QR, same-k locator update, unknown-k approval in TUI. F4 is a no-op stub. No Tor in the bin.
This commit is contained in:
parent
0a0dd3a8d8
commit
4fb4648552
8 changed files with 959 additions and 32 deletions
362
Cargo.lock
generated
362
Cargo.lock
generated
|
|
@ -29,6 +29,12 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allocator-api2"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "amplify"
|
name = "amplify"
|
||||||
version = "4.9.0"
|
version = "4.9.0"
|
||||||
|
|
@ -84,6 +90,15 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "approx"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.7.8"
|
version = "0.7.8"
|
||||||
|
|
@ -416,6 +431,15 @@ version = "0.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23a6f83e57018d15e7615a6f8e5e2c6b6b0226e24a1c562cd705f855a2bc751a"
|
checksum = "23a6f83e57018d15e7615a6f8e5e2c6b6b0226e24a1c562cd705f855a2bc751a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "castaway"
|
||||||
|
version = "0.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.4.5"
|
version = "1.4.5"
|
||||||
|
|
@ -496,6 +520,20 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "compact_str"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab"
|
||||||
|
dependencies = [
|
||||||
|
"castaway",
|
||||||
|
"cfg-if",
|
||||||
|
"itoa",
|
||||||
|
"rustversion",
|
||||||
|
"ryu",
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "compression-codecs"
|
name = "compression-codecs"
|
||||||
version = "0.4.41"
|
version = "0.4.41"
|
||||||
|
|
@ -609,6 +647,33 @@ version = "0.8.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm"
|
||||||
|
version = "0.29.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.2",
|
||||||
|
"crossterm_winapi",
|
||||||
|
"derive_more",
|
||||||
|
"document-features",
|
||||||
|
"mio",
|
||||||
|
"parking_lot",
|
||||||
|
"rustix",
|
||||||
|
"signal-hook",
|
||||||
|
"signal-hook-mio",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossterm_winapi"
|
||||||
|
version = "0.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
|
||||||
|
dependencies = [
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-bigint"
|
name = "crypto-bigint"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
|
|
@ -877,7 +942,7 @@ checksum = "ba91f619216e76a5eb2515783f7ec2e271938b51aadac592f4930517f4626863"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"indexmap 2.14.2",
|
"indexmap 2.14.2",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"proc-macro-crate",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -1005,6 +1070,15 @@ dependencies = [
|
||||||
"syn 3.0.5",
|
"syn 3.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "document-features"
|
||||||
|
version = "0.2.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
|
||||||
|
dependencies = [
|
||||||
|
"litrs",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "downcast-rs"
|
name = "downcast-rs"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
|
|
@ -1269,6 +1343,12 @@ version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
|
@ -1519,7 +1599,7 @@ version = "0.15.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"foldhash",
|
"foldhash 0.1.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1527,12 +1607,22 @@ name = "hashbrown"
|
||||||
version = "0.16.1"
|
version = "0.16.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
"equivalent",
|
||||||
|
"foldhash 0.2.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||||
|
dependencies = [
|
||||||
|
"allocator-api2",
|
||||||
|
"equivalent",
|
||||||
|
"foldhash 0.2.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashlink"
|
name = "hashlink"
|
||||||
|
|
@ -1794,6 +1884,15 @@ dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "indoc"
|
||||||
|
version = "2.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inotify"
|
name = "inotify"
|
||||||
version = "0.11.5"
|
version = "0.11.5"
|
||||||
|
|
@ -1832,6 +1931,19 @@ dependencies = [
|
||||||
"hybrid-array",
|
"hybrid-array",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "instability"
|
||||||
|
version = "0.3.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2bf84e73fa6f27f299dec58e13223cf70db80da872eb921d4f6138342a0eabc8"
|
||||||
|
dependencies = [
|
||||||
|
"darling 0.24.1",
|
||||||
|
"indoc",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "inventory"
|
name = "inventory"
|
||||||
version = "0.3.24"
|
version = "0.3.24"
|
||||||
|
|
@ -1856,6 +1968,15 @@ dependencies = [
|
||||||
"ipnet",
|
"ipnet",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
|
|
@ -1955,6 +2076,17 @@ dependencies = [
|
||||||
"sha3 0.10.9",
|
"sha3 0.10.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kasuari"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.16.1",
|
||||||
|
"portable-atomic",
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keccak"
|
name = "keccak"
|
||||||
version = "0.1.6"
|
version = "0.1.6"
|
||||||
|
|
@ -2055,6 +2187,15 @@ dependencies = [
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "line-clipping"
|
||||||
|
version = "0.3.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e752191d037c44ad111a8caa762921926658402f01cc1253f7bef2020ece4f5e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.12.1"
|
version = "0.12.1"
|
||||||
|
|
@ -2067,6 +2208,12 @@ version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
|
checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "litrs"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
|
|
@ -2082,6 +2229,15 @@ version = "0.4.34"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru"
|
||||||
|
version = "0.18.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff9840bcc50b71349309900da0ce7279aa336ae71d73250b07998932c7d97c25"
|
||||||
|
dependencies = [
|
||||||
|
"hashbrown 0.17.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "matchers"
|
name = "matchers"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -2306,6 +2462,15 @@ dependencies = [
|
||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num_threads"
|
||||||
|
version = "0.1.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "objc2-core-foundation"
|
name = "objc2-core-foundation"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
|
|
@ -2347,7 +2512,9 @@ dependencies = [
|
||||||
"arti-client",
|
"arti-client",
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"futures",
|
"futures",
|
||||||
|
"qrcode",
|
||||||
"rand 0.8.8",
|
"rand 0.8.8",
|
||||||
|
"ratatui",
|
||||||
"rusqlite",
|
"rusqlite",
|
||||||
"safelog",
|
"safelog",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
|
@ -2463,6 +2630,39 @@ dependencies = [
|
||||||
"sha2",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "palette"
|
||||||
|
version = "0.7.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64"
|
||||||
|
dependencies = [
|
||||||
|
"approx",
|
||||||
|
"libm",
|
||||||
|
"palette_derive",
|
||||||
|
"palette_math",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "palette_derive"
|
||||||
|
version = "0.7.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be"
|
||||||
|
dependencies = [
|
||||||
|
"by_address",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.119",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "palette_math"
|
||||||
|
version = "0.7.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12"
|
||||||
|
dependencies = [
|
||||||
|
"libm",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
|
|
@ -2724,6 +2924,12 @@ dependencies = [
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "qrcode"
|
||||||
|
version = "0.14.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.47"
|
version = "1.0.47"
|
||||||
|
|
@ -2825,6 +3031,72 @@ version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d"
|
checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ratatui"
|
||||||
|
version = "0.30.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d"
|
||||||
|
dependencies = [
|
||||||
|
"instability",
|
||||||
|
"ratatui-core",
|
||||||
|
"ratatui-crossterm",
|
||||||
|
"ratatui-widgets",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ratatui-core"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.2",
|
||||||
|
"compact_str",
|
||||||
|
"hashbrown 0.17.1",
|
||||||
|
"itertools 0.14.0",
|
||||||
|
"kasuari",
|
||||||
|
"lru",
|
||||||
|
"palette",
|
||||||
|
"serde",
|
||||||
|
"strum",
|
||||||
|
"thiserror 2.0.20",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-truncate",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ratatui-crossterm"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"crossterm",
|
||||||
|
"instability",
|
||||||
|
"ratatui-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ratatui-widgets"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.2",
|
||||||
|
"hashbrown 0.17.1",
|
||||||
|
"indoc",
|
||||||
|
"instability",
|
||||||
|
"itertools 0.14.0",
|
||||||
|
"line-clipping",
|
||||||
|
"ratatui-core",
|
||||||
|
"serde",
|
||||||
|
"strum",
|
||||||
|
"time",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rdrand"
|
name = "rdrand"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|
@ -3006,6 +3278,12 @@ version = "1.0.23"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "safelog"
|
name = "safelog"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
|
|
@ -3318,6 +3596,27 @@ version = "2.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook"
|
||||||
|
version = "0.3.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"signal-hook-registry",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "signal-hook-mio"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"mio",
|
||||||
|
"signal-hook",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "signal-hook-registry"
|
name = "signal-hook-registry"
|
||||||
version = "1.4.8"
|
version = "1.4.8"
|
||||||
|
|
@ -3649,7 +3948,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
|
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
|
"libc",
|
||||||
"num-conv",
|
"num-conv",
|
||||||
|
"num_threads",
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"time-core",
|
"time-core",
|
||||||
|
|
@ -3869,7 +4170,7 @@ dependencies = [
|
||||||
"extend",
|
"extend",
|
||||||
"getrandom 0.4.3",
|
"getrandom 0.4.3",
|
||||||
"hex",
|
"hex",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"libc",
|
"libc",
|
||||||
"paste",
|
"paste",
|
||||||
"rand 0.10.2",
|
"rand 0.10.2",
|
||||||
|
|
@ -3914,7 +4215,7 @@ dependencies = [
|
||||||
"derive-deftly",
|
"derive-deftly",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"educe",
|
"educe",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"paste",
|
"paste",
|
||||||
"rand 0.10.2",
|
"rand 0.10.2",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
|
@ -4001,7 +4302,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d3872a5bda49647ad4c4a822c58774995ad3acd02327f1cabe6cfc019d714535"
|
checksum = "d3872a5bda49647ad4c4a822c58774995ad3acd02327f1cabe6cfc019d714535"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"humantime",
|
"humantime",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"signature",
|
"signature",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"tor-llcrypto",
|
"tor-llcrypto",
|
||||||
|
|
@ -4025,7 +4326,7 @@ dependencies = [
|
||||||
"educe",
|
"educe",
|
||||||
"futures",
|
"futures",
|
||||||
"humantime-serde",
|
"humantime-serde",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"oneshot-fused-workaround",
|
"oneshot-fused-workaround",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
|
|
@ -4075,7 +4376,7 @@ dependencies = [
|
||||||
"fs-mistrust",
|
"fs-mistrust",
|
||||||
"futures",
|
"futures",
|
||||||
"humantime-serde",
|
"humantime-serde",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"notify",
|
"notify",
|
||||||
"paste",
|
"paste",
|
||||||
"postage",
|
"postage",
|
||||||
|
|
@ -4138,7 +4439,7 @@ dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"httparse",
|
"httparse",
|
||||||
"httpdate",
|
"httpdate",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"memchr",
|
"memchr",
|
||||||
"thiserror 2.0.20",
|
"thiserror 2.0.20",
|
||||||
"tor-circmgr",
|
"tor-circmgr",
|
||||||
|
|
@ -4193,7 +4494,7 @@ dependencies = [
|
||||||
"hex",
|
"hex",
|
||||||
"humantime",
|
"humantime",
|
||||||
"humantime-serde",
|
"humantime-serde",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"memmap2",
|
"memmap2",
|
||||||
"oneshot-fused-workaround",
|
"oneshot-fused-workaround",
|
||||||
"paste",
|
"paste",
|
||||||
|
|
@ -4276,7 +4577,7 @@ dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"humantime",
|
"humantime",
|
||||||
"humantime-serde",
|
"humantime-serde",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"oneshot-fused-workaround",
|
"oneshot-fused-workaround",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
|
|
@ -4317,7 +4618,7 @@ dependencies = [
|
||||||
"educe",
|
"educe",
|
||||||
"either",
|
"either",
|
||||||
"futures",
|
"futures",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"oneshot-fused-workaround",
|
"oneshot-fused-workaround",
|
||||||
"postage",
|
"postage",
|
||||||
"rand 0.10.2",
|
"rand 0.10.2",
|
||||||
|
|
@ -4363,7 +4664,7 @@ dependencies = [
|
||||||
"digest 0.10.7",
|
"digest 0.10.7",
|
||||||
"hex",
|
"hex",
|
||||||
"humantime",
|
"humantime",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"paste",
|
"paste",
|
||||||
"rand 0.10.2",
|
"rand 0.10.2",
|
||||||
"safelog",
|
"safelog",
|
||||||
|
|
@ -4403,7 +4704,7 @@ dependencies = [
|
||||||
"growable-bloom-filter",
|
"growable-bloom-filter",
|
||||||
"hex",
|
"hex",
|
||||||
"humantime",
|
"humantime",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"k12",
|
"k12",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"oneshot-fused-workaround",
|
"oneshot-fused-workaround",
|
||||||
|
|
@ -4482,7 +4783,7 @@ dependencies = [
|
||||||
"glob-match",
|
"glob-match",
|
||||||
"humantime",
|
"humantime",
|
||||||
"inventory",
|
"inventory",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"rand 0.10.2",
|
"rand 0.10.2",
|
||||||
"safelog",
|
"safelog",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -4518,7 +4819,7 @@ dependencies = [
|
||||||
"derive_builder_fork_arti",
|
"derive_builder_fork_arti",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"hex",
|
"hex",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"safelog",
|
"safelog",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
|
|
@ -4604,7 +4905,7 @@ dependencies = [
|
||||||
"educe",
|
"educe",
|
||||||
"extend",
|
"extend",
|
||||||
"futures",
|
"futures",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"paste",
|
"paste",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -4630,7 +4931,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "86d6f7e1d842e6272e414e1f866dc688306b788d368df42e47c4995e040c679c"
|
checksum = "86d6f7e1d842e6272e414e1f866dc688306b788d368df42e47c4995e040c679c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive-deftly",
|
"derive-deftly",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"paste",
|
"paste",
|
||||||
"void",
|
"void",
|
||||||
]
|
]
|
||||||
|
|
@ -4648,7 +4949,7 @@ dependencies = [
|
||||||
"futures",
|
"futures",
|
||||||
"hex",
|
"hex",
|
||||||
"humantime",
|
"humantime",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"num_enum",
|
"num_enum",
|
||||||
"rand 0.10.2",
|
"rand 0.10.2",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -4689,7 +4990,7 @@ dependencies = [
|
||||||
"humantime",
|
"humantime",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"iprange",
|
"iprange",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"memchr",
|
"memchr",
|
||||||
"paste",
|
"paste",
|
||||||
"phf",
|
"phf",
|
||||||
|
|
@ -4735,7 +5036,7 @@ dependencies = [
|
||||||
"fs-mistrust",
|
"fs-mistrust",
|
||||||
"fslock-guard",
|
"fslock-guard",
|
||||||
"futures",
|
"futures",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"oneshot-fused-workaround",
|
"oneshot-fused-workaround",
|
||||||
"paste",
|
"paste",
|
||||||
"sanitize-filename",
|
"sanitize-filename",
|
||||||
|
|
@ -4778,7 +5079,7 @@ dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hkdf",
|
"hkdf",
|
||||||
"hmac",
|
"hmac",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"nonany",
|
"nonany",
|
||||||
"oneshot-fused-workaround",
|
"oneshot-fused-workaround",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
|
|
@ -4898,7 +5199,7 @@ dependencies = [
|
||||||
"educe",
|
"educe",
|
||||||
"futures",
|
"futures",
|
||||||
"humantime",
|
"humantime",
|
||||||
"itertools",
|
"itertools 0.15.0",
|
||||||
"oneshot-fused-workaround",
|
"oneshot-fused-workaround",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
"priority-queue",
|
"priority-queue",
|
||||||
|
|
@ -5063,6 +5364,23 @@ version = "1.13.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-truncate"
|
||||||
|
version = "2.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5"
|
||||||
|
dependencies = [
|
||||||
|
"itertools 0.14.0",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.6"
|
version = "0.2.6"
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-util", "ti
|
||||||
tor-cell = "0.46"
|
tor-cell = "0.46"
|
||||||
tor-hsservice = "0.46"
|
tor-hsservice = "0.46"
|
||||||
tor-rtcompat = { version = "0.46", features = ["tokio"] }
|
tor-rtcompat = { version = "0.46", features = ["tokio"] }
|
||||||
|
qrcode = { version = "0.14.1", default-features = false }
|
||||||
|
ratatui = { version = "0.30.2", default-features = false, features = ["crossterm"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
pub mod qr;
|
||||||
mod store;
|
mod store;
|
||||||
|
pub mod tui;
|
||||||
|
|
||||||
pub use store::{Friend, SelfIdentity, Store};
|
pub use store::{Friend, SelfIdentity, Store};
|
||||||
|
|
|
||||||
11
src/main.rs
11
src/main.rs
|
|
@ -1,16 +1,11 @@
|
||||||
fn main() {
|
fn main() {
|
||||||
match onionwire::Store::open() {
|
match onionwire::Store::open() {
|
||||||
Ok(store) => match store.self_identity() {
|
Ok(store) => {
|
||||||
Ok(me) => {
|
if let Err(e) = onionwire::tui::run(store) {
|
||||||
let fp: String = me.identity_pk.iter().map(|b| format!("{b:02x}")).collect();
|
|
||||||
println!("onionwire ready");
|
|
||||||
println!("fingerprint {fp}");
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
eprintln!("onionwire: {e}");
|
eprintln!("onionwire: {e}");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("onionwire: {e}");
|
eprintln!("onionwire: {e}");
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
|
|
|
||||||
138
src/qr.rs
Normal file
138
src/qr.rs
Normal file
|
|
@ -0,0 +1,138 @@
|
||||||
|
use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
|
||||||
|
use qrcode::render::unicode::Dense1x2;
|
||||||
|
use qrcode::QrCode;
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Error>;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Error(String);
|
||||||
|
|
||||||
|
impl std::fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct QrPayload {
|
||||||
|
pub pubkey: Vec<u8>,
|
||||||
|
pub onion: String,
|
||||||
|
pub signed_prekey: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `onionwire:v1:k={pubkey_hex}:o={v3onion}:spk={signed_prekey}:sig={sign(k||o||spk)}`
|
||||||
|
pub fn encode(identity_sk: &[u8], onion: &str, signed_prekey: &[u8]) -> Result<String> {
|
||||||
|
let sk_bytes: [u8; 32] = identity_sk
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error("identity secret key must be 32 bytes".into()))?;
|
||||||
|
let sk = SigningKey::from_bytes(&sk_bytes);
|
||||||
|
let k = to_hex(&sk.verifying_key().to_bytes());
|
||||||
|
let spk = to_hex(signed_prekey);
|
||||||
|
let msg = sign_msg(&k, onion, &spk);
|
||||||
|
let sig = to_hex(&sk.sign(&msg).to_bytes());
|
||||||
|
Ok(format!("onionwire:v1:k={k}:o={onion}:spk={spk}:sig={sig}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(raw: &str) -> Result<QrPayload> {
|
||||||
|
let (k, onion, spk, sig) = parse_fields(raw)?;
|
||||||
|
let pubkey = from_hex(&k)?;
|
||||||
|
let signed_prekey = from_hex(&spk)?;
|
||||||
|
let sig_bytes = from_hex(&sig)?;
|
||||||
|
if pubkey.len() != 32 {
|
||||||
|
return Err(Error("k must be 32 bytes".into()));
|
||||||
|
}
|
||||||
|
if sig_bytes.len() != 64 {
|
||||||
|
return Err(Error("sig must be 64 bytes".into()));
|
||||||
|
}
|
||||||
|
let pk_arr: [u8; 32] = pubkey
|
||||||
|
.as_slice()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error("k must be 32 bytes".into()))?;
|
||||||
|
let sig_arr: [u8; 64] = sig_bytes
|
||||||
|
.as_slice()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| Error("sig must be 64 bytes".into()))?;
|
||||||
|
let vk = VerifyingKey::from_bytes(&pk_arr).map_err(|e| Error(e.to_string()))?;
|
||||||
|
let signature = Signature::from_bytes(&sig_arr);
|
||||||
|
let msg = sign_msg(&k, &onion, &spk);
|
||||||
|
vk.verify(&msg, &signature)
|
||||||
|
.map_err(|_| Error("bad signature".into()))?;
|
||||||
|
Ok(QrPayload {
|
||||||
|
pubkey,
|
||||||
|
onion,
|
||||||
|
signed_prekey,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_unicode(payload: &str) -> Result<String> {
|
||||||
|
let code = QrCode::new(payload.as_bytes()).map_err(|e| Error(e.to_string()))?;
|
||||||
|
Ok(code.render::<Dense1x2>().build())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sign_msg(k: &str, onion: &str, spk: &str) -> Vec<u8> {
|
||||||
|
let mut msg = Vec::with_capacity(k.len() + onion.len() + spk.len());
|
||||||
|
msg.extend_from_slice(k.as_bytes());
|
||||||
|
msg.extend_from_slice(onion.as_bytes());
|
||||||
|
msg.extend_from_slice(spk.as_bytes());
|
||||||
|
msg
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_fields(raw: &str) -> Result<(String, String, String, String)> {
|
||||||
|
let rest = raw
|
||||||
|
.strip_prefix("onionwire:v1:")
|
||||||
|
.ok_or_else(|| Error("not onionwire:v1".into()))?;
|
||||||
|
let mut k = None;
|
||||||
|
let mut o = None;
|
||||||
|
let mut spk = None;
|
||||||
|
let mut sig = None;
|
||||||
|
for part in rest.split(':') {
|
||||||
|
if let Some(v) = part.strip_prefix("k=") {
|
||||||
|
if k.is_some() {
|
||||||
|
return Err(Error("duplicate k".into()));
|
||||||
|
}
|
||||||
|
k = Some(v.to_string());
|
||||||
|
} else if let Some(v) = part.strip_prefix("o=") {
|
||||||
|
if o.is_some() {
|
||||||
|
return Err(Error("duplicate o".into()));
|
||||||
|
}
|
||||||
|
o = Some(v.to_string());
|
||||||
|
} else if let Some(v) = part.strip_prefix("spk=") {
|
||||||
|
if spk.is_some() {
|
||||||
|
return Err(Error("duplicate spk".into()));
|
||||||
|
}
|
||||||
|
spk = Some(v.to_string());
|
||||||
|
} else if let Some(v) = part.strip_prefix("sig=") {
|
||||||
|
if sig.is_some() {
|
||||||
|
return Err(Error("duplicate sig".into()));
|
||||||
|
}
|
||||||
|
sig = Some(v.to_string());
|
||||||
|
} else {
|
||||||
|
return Err(Error("unknown field".into()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((
|
||||||
|
k.ok_or_else(|| Error("missing k".into()))?,
|
||||||
|
o.ok_or_else(|| Error("missing o".into()))?,
|
||||||
|
spk.ok_or_else(|| Error("missing spk".into()))?,
|
||||||
|
sig.ok_or_else(|| Error("missing sig".into()))?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_hex(bytes: &[u8]) -> String {
|
||||||
|
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_hex(s: &str) -> Result<Vec<u8>> {
|
||||||
|
if s.is_empty() || !s.len().is_multiple_of(2) {
|
||||||
|
return Err(Error("invalid hex".into()));
|
||||||
|
}
|
||||||
|
if !s.bytes().all(|c| c.is_ascii_hexdigit()) {
|
||||||
|
return Err(Error("invalid hex".into()));
|
||||||
|
}
|
||||||
|
(0..s.len())
|
||||||
|
.step_by(2)
|
||||||
|
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).map_err(|_| Error("invalid hex".into())))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
44
src/store.rs
44
src/store.rs
|
|
@ -47,6 +47,7 @@ pub struct Friend {
|
||||||
pub fingerprint: String,
|
pub fingerprint: String,
|
||||||
pub petname: Option<String>,
|
pub petname: Option<String>,
|
||||||
pub onion: String,
|
pub onion: String,
|
||||||
|
pub last_connect_ok: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Store {
|
impl Store {
|
||||||
|
|
@ -69,7 +70,8 @@ impl Store {
|
||||||
petname TEXT,
|
petname TEXT,
|
||||||
onion TEXT NOT NULL,
|
onion TEXT NOT NULL,
|
||||||
onion_updated_at INTEGER NOT NULL,
|
onion_updated_at INTEGER NOT NULL,
|
||||||
added_at INTEGER NOT NULL
|
added_at INTEGER NOT NULL,
|
||||||
|
last_connect_ok INTEGER NOT NULL DEFAULT 1
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS messages (
|
CREATE TABLE IF NOT EXISTS messages (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
|
|
@ -88,10 +90,26 @@ impl Store {
|
||||||
",
|
",
|
||||||
)?;
|
)?;
|
||||||
let store = Self { conn };
|
let store = Self { conn };
|
||||||
|
store.migrate()?;
|
||||||
store.ensure_self()?;
|
store.ensure_self()?;
|
||||||
Ok(store)
|
Ok(store)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn migrate(&self) -> Result<()> {
|
||||||
|
let has: i64 = self.conn.query_row(
|
||||||
|
"SELECT COUNT(*) FROM pragma_table_info('friends') WHERE name = 'last_connect_ok'",
|
||||||
|
[],
|
||||||
|
|row| row.get(0),
|
||||||
|
)?;
|
||||||
|
if has == 0 {
|
||||||
|
self.conn.execute(
|
||||||
|
"ALTER TABLE friends ADD COLUMN last_connect_ok INTEGER NOT NULL DEFAULT 1",
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn ensure_self(&self) -> Result<()> {
|
fn ensure_self(&self) -> Result<()> {
|
||||||
let exists: i64 =
|
let exists: i64 =
|
||||||
self.conn
|
self.conn
|
||||||
|
|
@ -151,7 +169,7 @@ impl Store {
|
||||||
pub fn get_friend(&self, pubkey: &[u8]) -> Result<Option<Friend>> {
|
pub fn get_friend(&self, pubkey: &[u8]) -> Result<Option<Friend>> {
|
||||||
self.conn
|
self.conn
|
||||||
.query_row(
|
.query_row(
|
||||||
"SELECT pubkey, fingerprint, petname, onion FROM friends WHERE pubkey = ?1",
|
"SELECT pubkey, fingerprint, petname, onion, last_connect_ok FROM friends WHERE pubkey = ?1",
|
||||||
params![pubkey],
|
params![pubkey],
|
||||||
|row| {
|
|row| {
|
||||||
Ok(Friend {
|
Ok(Friend {
|
||||||
|
|
@ -159,12 +177,34 @@ impl Store {
|
||||||
fingerprint: row.get(1)?,
|
fingerprint: row.get(1)?,
|
||||||
petname: row.get(2)?,
|
petname: row.get(2)?,
|
||||||
onion: row.get(3)?,
|
onion: row.get(3)?,
|
||||||
|
last_connect_ok: row.get::<_, i64>(4)? != 0,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.optional()
|
.optional()
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn list_friends(&self) -> Result<Vec<Friend>> {
|
||||||
|
let mut stmt = self.conn.prepare(
|
||||||
|
"SELECT pubkey, fingerprint, petname, onion, last_connect_ok FROM friends
|
||||||
|
ORDER BY COALESCE(petname, fingerprint) COLLATE NOCASE",
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map([], |row| {
|
||||||
|
Ok(Friend {
|
||||||
|
pubkey: row.get(0)?,
|
||||||
|
fingerprint: row.get(1)?,
|
||||||
|
petname: row.get(2)?,
|
||||||
|
onion: row.get(3)?,
|
||||||
|
last_connect_ok: row.get::<_, i64>(4)? != 0,
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
let mut out = Vec::new();
|
||||||
|
for row in rows {
|
||||||
|
out.push(row?);
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn home_dir() -> Result<PathBuf> {
|
fn home_dir() -> Result<PathBuf> {
|
||||||
|
|
|
||||||
300
src/tui.rs
Normal file
300
src/tui.rs
Normal file
|
|
@ -0,0 +1,300 @@
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use rand::RngCore;
|
||||||
|
use ratatui::crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||||
|
use ratatui::layout::{Constraint, Layout};
|
||||||
|
use ratatui::widgets::{Block, Borders, List, ListItem, Paragraph, Wrap};
|
||||||
|
use ratatui::{DefaultTerminal, Frame};
|
||||||
|
|
||||||
|
use crate::qr::{self, QrPayload};
|
||||||
|
use crate::store::{Friend, Store};
|
||||||
|
|
||||||
|
pub fn run(store: Store) -> Result<(), String> {
|
||||||
|
let mut app = App::new(store)?;
|
||||||
|
let mut terminal = ratatui::init();
|
||||||
|
let result = app.run(&mut terminal);
|
||||||
|
ratatui::restore();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
struct App {
|
||||||
|
store: Store,
|
||||||
|
screen: Screen,
|
||||||
|
friends: Vec<Friend>,
|
||||||
|
selected: usize,
|
||||||
|
me_fp: String,
|
||||||
|
me_onion: String,
|
||||||
|
identity_sk: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Screen {
|
||||||
|
Main,
|
||||||
|
Share { art: String, payload: String },
|
||||||
|
Paste { buf: String, err: Option<String> },
|
||||||
|
Approve { payload: QrPayload },
|
||||||
|
Rotate,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl App {
|
||||||
|
fn new(store: Store) -> Result<Self, String> {
|
||||||
|
let me = store.self_identity().map_err(|e| e.to_string())?;
|
||||||
|
let friends = store.list_friends().map_err(|e| e.to_string())?;
|
||||||
|
Ok(Self {
|
||||||
|
store,
|
||||||
|
screen: Screen::Main,
|
||||||
|
friends,
|
||||||
|
selected: 0,
|
||||||
|
me_fp: to_hex(&me.identity_pk),
|
||||||
|
me_onion: me.onion,
|
||||||
|
identity_sk: me.identity_sk,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&mut self, terminal: &mut DefaultTerminal) -> Result<(), String> {
|
||||||
|
loop {
|
||||||
|
terminal.draw(|f| self.draw(f)).map_err(|e| e.to_string())?;
|
||||||
|
let Event::Key(key) = event::read().map_err(io_err)? else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if key.kind != KeyEventKind::Press {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if key.code == KeyCode::Char('q') && key.modifiers.contains(KeyModifiers::CONTROL) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
self.handle_key(key)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_key(&mut self, key: KeyEvent) -> Result<(), String> {
|
||||||
|
match &mut self.screen {
|
||||||
|
Screen::Main => match key.code {
|
||||||
|
KeyCode::Up => {
|
||||||
|
self.selected = self.selected.saturating_sub(1);
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
if !self.friends.is_empty() {
|
||||||
|
self.selected = (self.selected + 1).min(self.friends.len() - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::F(2) => self.open_share()?,
|
||||||
|
KeyCode::F(3) => {
|
||||||
|
self.screen = Screen::Paste {
|
||||||
|
buf: String::new(),
|
||||||
|
err: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
KeyCode::F(4) => self.screen = Screen::Rotate,
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Screen::Share { .. } => {
|
||||||
|
if key.code == KeyCode::Esc {
|
||||||
|
self.screen = Screen::Main;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Screen::Paste { buf, err } => match key.code {
|
||||||
|
KeyCode::Esc => self.screen = Screen::Main,
|
||||||
|
KeyCode::Enter => {
|
||||||
|
let raw = buf.clone();
|
||||||
|
self.submit_paste(&raw)?;
|
||||||
|
}
|
||||||
|
KeyCode::Backspace => {
|
||||||
|
buf.pop();
|
||||||
|
*err = None;
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
|
||||||
|
buf.push(c);
|
||||||
|
*err = None;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Screen::Approve { payload } => match key.code {
|
||||||
|
KeyCode::Char('y') | KeyCode::Enter => {
|
||||||
|
let p = payload.clone();
|
||||||
|
self.accept_friend(&p)?;
|
||||||
|
}
|
||||||
|
KeyCode::Char('n') | KeyCode::Esc => self.screen = Screen::Main,
|
||||||
|
_ => {}
|
||||||
|
},
|
||||||
|
Screen::Rotate => {
|
||||||
|
if matches!(key.code, KeyCode::Esc | KeyCode::Enter) {
|
||||||
|
self.screen = Screen::Main;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_share(&mut self) -> Result<(), String> {
|
||||||
|
let mut spk = [0u8; 32];
|
||||||
|
rand::rngs::OsRng.fill_bytes(&mut spk);
|
||||||
|
let onion = if self.me_onion.is_empty() {
|
||||||
|
"unset"
|
||||||
|
} else {
|
||||||
|
self.me_onion.as_str()
|
||||||
|
};
|
||||||
|
let payload = qr::encode(&self.identity_sk, onion, &spk).map_err(|e| e.to_string())?;
|
||||||
|
let art = qr::render_unicode(&payload).map_err(|e| e.to_string())?;
|
||||||
|
self.screen = Screen::Share { art, payload };
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit_paste(&mut self, raw: &str) -> Result<(), String> {
|
||||||
|
match qr::decode(raw.trim()) {
|
||||||
|
Err(e) => {
|
||||||
|
self.screen = Screen::Paste {
|
||||||
|
buf: raw.to_string(),
|
||||||
|
err: Some(e.to_string()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(payload) => {
|
||||||
|
let known = self
|
||||||
|
.store
|
||||||
|
.get_friend(&payload.pubkey)
|
||||||
|
.map_err(|e| e.to_string())?
|
||||||
|
.is_some();
|
||||||
|
if known {
|
||||||
|
self.accept_friend(&payload)?;
|
||||||
|
} else {
|
||||||
|
self.screen = Screen::Approve { payload };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn accept_friend(&mut self, payload: &QrPayload) -> Result<(), String> {
|
||||||
|
self.store
|
||||||
|
.upsert_friend(&payload.pubkey, &payload.onion, None)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
self.reload_friends()?;
|
||||||
|
self.screen = Screen::Main;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reload_friends(&mut self) -> Result<(), String> {
|
||||||
|
self.friends = self.store.list_friends().map_err(|e| e.to_string())?;
|
||||||
|
if self.friends.is_empty() {
|
||||||
|
self.selected = 0;
|
||||||
|
} else {
|
||||||
|
self.selected = self.selected.min(self.friends.len() - 1);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, f: &mut Frame) {
|
||||||
|
match &self.screen {
|
||||||
|
Screen::Main => self.draw_main(f),
|
||||||
|
Screen::Share { art, payload } => draw_share(f, art, payload),
|
||||||
|
Screen::Paste { buf, err } => draw_paste(f, buf, err.as_deref()),
|
||||||
|
Screen::Approve { payload } => draw_approve(f, payload),
|
||||||
|
Screen::Rotate => draw_rotate(f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_main(&self, f: &mut Frame) {
|
||||||
|
let area = f.area();
|
||||||
|
let cols = Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).split(area);
|
||||||
|
let panes =
|
||||||
|
Layout::horizontal([Constraint::Percentage(36), Constraint::Min(10)]).split(cols[0]);
|
||||||
|
|
||||||
|
let items: Vec<ListItem> = self
|
||||||
|
.friends
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(i, friend)| {
|
||||||
|
let name = friend.petname.as_deref().unwrap_or(&friend.fingerprint);
|
||||||
|
let fp = truncate_fp(&friend.fingerprint);
|
||||||
|
let stale = if friend.last_connect_ok { "" } else { " stale" };
|
||||||
|
let mark = if i == self.selected { ">" } else { " " };
|
||||||
|
ListItem::new(format!("{mark} {name} {fp}{stale}"))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let roster = List::new(items).block(Block::default().borders(Borders::ALL).title("roster"));
|
||||||
|
f.render_widget(roster, panes[0]);
|
||||||
|
|
||||||
|
let chat_text = if let Some(friend) = self.friends.get(self.selected) {
|
||||||
|
let name = friend.petname.as_deref().unwrap_or(&friend.fingerprint);
|
||||||
|
format!("{name}\n\nchat arrives in M3")
|
||||||
|
} else {
|
||||||
|
"no friends yet\nF3 paste a QR to add one".into()
|
||||||
|
};
|
||||||
|
let chat = Paragraph::new(chat_text)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("chat"));
|
||||||
|
f.render_widget(chat, panes[1]);
|
||||||
|
|
||||||
|
let onion = if self.me_onion.is_empty() {
|
||||||
|
"unset"
|
||||||
|
} else {
|
||||||
|
self.me_onion.as_str()
|
||||||
|
};
|
||||||
|
let status = format!(
|
||||||
|
"TOR: offline | me: {} | onion: {} F2 share F3 paste F4 rotate Ctrl-Q quit",
|
||||||
|
truncate_fp(&self.me_fp),
|
||||||
|
onion
|
||||||
|
);
|
||||||
|
f.render_widget(Paragraph::new(status), cols[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_share(f: &mut Frame, art: &str, payload: &str) {
|
||||||
|
let body = format!("{art}\n{payload}\n\nEsc back");
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(body)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("F2 share")),
|
||||||
|
f.area(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_paste(f: &mut Frame, buf: &str, err: Option<&str>) {
|
||||||
|
let hint = err.unwrap_or("paste onionwire:v1 payload, Enter to submit");
|
||||||
|
let body = format!("{hint}\n\n{buf}");
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(body)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("F3 paste QR")),
|
||||||
|
f.area(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_approve(f: &mut Frame, payload: &QrPayload) {
|
||||||
|
let fp = to_hex(&payload.pubkey);
|
||||||
|
let body = format!(
|
||||||
|
"Unknown key. Add this friend?\n\nfp {}\nonion {}\n\ny accept n / Esc reject",
|
||||||
|
truncate_fp(&fp),
|
||||||
|
payload.onion
|
||||||
|
);
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(body).block(Block::default().borders(Borders::ALL).title("approve")),
|
||||||
|
f.area(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_rotate(f: &mut Frame) {
|
||||||
|
let body = "Rotate onion address?\n\
|
||||||
|
Your identity key stays the same.\n\
|
||||||
|
Online friends get a signed location update.\n\
|
||||||
|
Offline friends CANNOT find you until they rescan your QR.\n\n\
|
||||||
|
M4 will rotate the HS. Esc / Enter to go back (no change).";
|
||||||
|
f.render_widget(
|
||||||
|
Paragraph::new(body)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.block(Block::default().borders(Borders::ALL).title("F4 rotate")),
|
||||||
|
f.area(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn truncate_fp(fp: &str) -> String {
|
||||||
|
let s: String = fp.chars().take(8).collect();
|
||||||
|
format!("{s}…")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_hex(bytes: &[u8]) -> String {
|
||||||
|
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn io_err(e: io::Error) -> String {
|
||||||
|
e.to_string()
|
||||||
|
}
|
||||||
132
tests/qr.rs
Normal file
132
tests/qr.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
//! M2 QR: roundtrip, garbage → Err, same-k rescan does not duplicate.
|
||||||
|
|
||||||
|
use std::sync::{Mutex, MutexGuard};
|
||||||
|
|
||||||
|
use onionwire::qr;
|
||||||
|
use onionwire::Store;
|
||||||
|
|
||||||
|
static ENV_LOCK: Mutex<()> = Mutex::new(());
|
||||||
|
|
||||||
|
struct TempHome {
|
||||||
|
_dir: tempfile::TempDir,
|
||||||
|
_guard: MutexGuard<'static, ()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TempHome {
|
||||||
|
fn new() -> Self {
|
||||||
|
let dir = tempfile::tempdir().expect("tempdir");
|
||||||
|
let guard = ENV_LOCK.lock().expect("env lock");
|
||||||
|
unsafe {
|
||||||
|
std::env::set_var("ONIONWIRE_HOME", dir.path());
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
_dir: dir,
|
||||||
|
_guard: guard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for TempHome {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
unsafe {
|
||||||
|
std::env::remove_var("ONIONWIRE_HOME");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn v3onion(tag: &str) -> String {
|
||||||
|
format!("{tag}.onion")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode_decode_roundtrip() {
|
||||||
|
let _home = TempHome::new();
|
||||||
|
let store = Store::open().expect("open");
|
||||||
|
let me = store.self_identity().expect("self");
|
||||||
|
let spk = [7u8; 32];
|
||||||
|
let onion = v3onion("alice-locator");
|
||||||
|
let raw = qr::encode(&me.identity_sk, &onion, &spk).expect("encode");
|
||||||
|
assert!(raw.starts_with("onionwire:v1:k="), "payload prefix: {raw}");
|
||||||
|
assert!(raw.contains(":o="));
|
||||||
|
assert!(raw.contains(":spk="));
|
||||||
|
assert!(raw.contains(":sig="));
|
||||||
|
|
||||||
|
let p = qr::decode(&raw).expect("decode");
|
||||||
|
assert_eq!(p.pubkey, me.identity_pk);
|
||||||
|
assert_eq!(p.onion, onion);
|
||||||
|
assert_eq!(p.signed_prekey, spk);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn garbage_is_err() {
|
||||||
|
assert!(qr::decode("not a qr").is_err());
|
||||||
|
assert!(qr::decode("").is_err());
|
||||||
|
assert!(qr::decode("onionwire:v1:k=aa").is_err());
|
||||||
|
assert!(qr::decode("🔥").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_sig_is_err() {
|
||||||
|
let _home = TempHome::new();
|
||||||
|
let store = Store::open().expect("open");
|
||||||
|
let me = store.self_identity().expect("self");
|
||||||
|
let raw = qr::encode(&me.identity_sk, &v3onion("x"), &[1u8; 32]).expect("encode");
|
||||||
|
let stripped = raw.rsplit_once(":sig=").expect("sig field").0;
|
||||||
|
assert!(qr::decode(stripped).is_err());
|
||||||
|
assert!(qr::decode(&format!("{stripped}:sig=")).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bad_sig_is_err() {
|
||||||
|
let _home = TempHome::new();
|
||||||
|
let store = Store::open().expect("open");
|
||||||
|
let me = store.self_identity().expect("self");
|
||||||
|
let raw = qr::encode(&me.identity_sk, &v3onion("x"), &[1u8; 32]).expect("encode");
|
||||||
|
let (head, sig) = raw.rsplit_once(":sig=").expect("sig field");
|
||||||
|
let mut chars: Vec<char> = sig.chars().collect();
|
||||||
|
let last = chars.last_mut().expect("sig chars");
|
||||||
|
*last = if *last == '0' { '1' } else { '0' };
|
||||||
|
let flipped: String = chars.into_iter().collect();
|
||||||
|
assert!(qr::decode(&format!("{head}:sig={flipped}")).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unicode_qr_renders() {
|
||||||
|
let _home = TempHome::new();
|
||||||
|
let store = Store::open().expect("open");
|
||||||
|
let me = store.self_identity().expect("self");
|
||||||
|
let raw = qr::encode(&me.identity_sk, &v3onion("x"), &[1u8; 32]).expect("encode");
|
||||||
|
let art = qr::render_unicode(&raw).expect("render");
|
||||||
|
assert!(
|
||||||
|
art.contains('█') || art.contains('▀') || art.contains('▄') || art.contains('▌'),
|
||||||
|
"expected unicode blocks, got {art:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn same_k_rescan_updates_onion_not_duplicate() {
|
||||||
|
let _home = TempHome::new();
|
||||||
|
let store = Store::open().expect("open");
|
||||||
|
let me = store.self_identity().expect("self");
|
||||||
|
let spk = [9u8; 32];
|
||||||
|
let p1 = qr::decode(&qr::encode(&me.identity_sk, &v3onion("old"), &spk).unwrap()).unwrap();
|
||||||
|
store
|
||||||
|
.upsert_friend(&p1.pubkey, &p1.onion, Some("alice"))
|
||||||
|
.unwrap();
|
||||||
|
let p2 = qr::decode(&qr::encode(&me.identity_sk, &v3onion("new"), &spk).unwrap()).unwrap();
|
||||||
|
store.upsert_friend(&p2.pubkey, &p2.onion, None).unwrap();
|
||||||
|
assert_eq!(store.friend_count().unwrap(), 1);
|
||||||
|
let f = store.get_friend(&p2.pubkey).unwrap().unwrap();
|
||||||
|
assert_eq!(f.onion, v3onion("new"));
|
||||||
|
assert_eq!(f.petname.as_deref(), Some("alice"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_k_decode_does_not_insert() {
|
||||||
|
let _home = TempHome::new();
|
||||||
|
let store = Store::open().expect("open");
|
||||||
|
let me = store.self_identity().expect("self");
|
||||||
|
let raw = qr::encode(&me.identity_sk, &v3onion("x"), &[1u8; 32]).unwrap();
|
||||||
|
let _p = qr::decode(&raw).unwrap();
|
||||||
|
assert_eq!(store.friend_count().unwrap(), 0);
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue