mirror of
https://github.com/block/buzz.git
synced 2026-10-31 14:31:02 +00:00
Add printable recovery sheet PDFs
Signed-off-by: Jordan Mecom <jm@squareup.com>
This commit is contained in:
307
desktop/src-tauri/Cargo.lock
generated
307
desktop/src-tauri/Cargo.lock
generated
@@ -122,6 +122,12 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aliasable"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
|
||||
|
||||
[[package]]
|
||||
name = "alloc-no-stdlib"
|
||||
version = "2.0.4"
|
||||
@@ -143,6 +149,29 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "allsorts-azul"
|
||||
version = "0.17.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab097a7be305dd66b2b6917f5efb6b723129ce4b2cd11aa1a0afcc293c7dc08e"
|
||||
dependencies = [
|
||||
"bitflags 2.13.0",
|
||||
"brotli-decompressor",
|
||||
"encoding_rs",
|
||||
"enumflags2",
|
||||
"flate2",
|
||||
"glyph-names",
|
||||
"log",
|
||||
"ouroboros",
|
||||
"pathfinder_geometry",
|
||||
"rustc-hash",
|
||||
"tinyvec",
|
||||
"ucd-trie",
|
||||
"unicode-canonical-combining-class",
|
||||
"unicode-general-category",
|
||||
"unicode-joining-type",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alsa"
|
||||
version = "0.11.0"
|
||||
@@ -210,7 +239,7 @@ version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -221,7 +250,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -932,6 +961,15 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block-padding"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b"
|
||||
dependencies = [
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block2"
|
||||
version = "0.6.2"
|
||||
@@ -1135,6 +1173,8 @@ dependencies = [
|
||||
"plist",
|
||||
"png 0.18.1",
|
||||
"portable-pty",
|
||||
"printpdf",
|
||||
"qrcode",
|
||||
"regex",
|
||||
"reqwest 0.13.4",
|
||||
"rodio",
|
||||
@@ -1420,6 +1460,15 @@ dependencies = [
|
||||
"cipher 0.4.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cbc"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896"
|
||||
dependencies = [
|
||||
"cipher 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.66"
|
||||
@@ -1644,7 +1693,7 @@ version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2311,7 +2360,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2332,8 +2381,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6"
|
||||
dependencies = [
|
||||
"aes 0.8.4",
|
||||
"block-padding",
|
||||
"cbc",
|
||||
"block-padding 0.3.3",
|
||||
"cbc 0.1.2",
|
||||
"dbus",
|
||||
"fastrand",
|
||||
"hkdf",
|
||||
@@ -2489,7 +2538,7 @@ dependencies = [
|
||||
"libc",
|
||||
"option-ext",
|
||||
"redox_users",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2634,6 +2683,15 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7984231f8b4c72eb3b88c70040dc1e4ff6803fa9169e93c0ac465942d74fa36a"
|
||||
|
||||
[[package]]
|
||||
name = "ecb"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26f2a8b3e564eba0877223dc343703ad0385794e882e6d13f3a4dd5c6b1f41ac"
|
||||
dependencies = [
|
||||
"cipher 0.5.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ed25519"
|
||||
version = "3.0.0"
|
||||
@@ -2770,7 +2828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3288,7 +3346,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"rustversion",
|
||||
"windows-link 0.1.3",
|
||||
"windows-link 0.2.1",
|
||||
"windows-result 0.4.1",
|
||||
]
|
||||
|
||||
@@ -3369,7 +3427,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
"weezl 0.1.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3520,6 +3578,12 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glyph-names"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3531d702d6c1a3ba92a5fb55a404c7b8c476c8e7ca249951077afcbe4bc807f"
|
||||
|
||||
[[package]]
|
||||
name = "gobject-sys"
|
||||
version = "0.18.0"
|
||||
@@ -4300,7 +4364,7 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
|
||||
dependencies = [
|
||||
"block-padding",
|
||||
"block-padding 0.3.3",
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
@@ -4310,6 +4374,7 @@ version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
|
||||
dependencies = [
|
||||
"block-padding 0.4.2",
|
||||
"hybrid-array",
|
||||
]
|
||||
|
||||
@@ -4983,6 +5048,32 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lopdf"
|
||||
version = "0.44.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e2ec995d822e05cabc3f06d196ee43650af3fe4fe38012cacb35e0c3d113b68"
|
||||
dependencies = [
|
||||
"aes 0.9.1",
|
||||
"bitflags 2.13.0",
|
||||
"cbc 0.2.1",
|
||||
"ecb",
|
||||
"encoding_rs",
|
||||
"flate2",
|
||||
"getrandom 0.4.3",
|
||||
"indexmap 2.14.0",
|
||||
"itoa",
|
||||
"log",
|
||||
"md-5",
|
||||
"nom 8.0.0",
|
||||
"rand 0.10.2",
|
||||
"rangemap",
|
||||
"sha2 0.11.0",
|
||||
"stringprep",
|
||||
"thiserror 2.0.18",
|
||||
"weezl 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.18.1"
|
||||
@@ -5136,6 +5227,16 @@ dependencies = [
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98"
|
||||
dependencies = [
|
||||
"cfg-if 1.0.4",
|
||||
"digest 0.11.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md5"
|
||||
version = "0.8.1"
|
||||
@@ -5746,7 +5847,7 @@ version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08"
|
||||
dependencies = [
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5905,7 +6006,7 @@ dependencies = [
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6307,7 +6408,7 @@ dependencies = [
|
||||
"bech32 0.11.1",
|
||||
"bip39",
|
||||
"bitcoin_hashes 0.14.101",
|
||||
"cbc",
|
||||
"cbc 0.1.2",
|
||||
"chacha20 0.9.1",
|
||||
"chacha20poly1305",
|
||||
"getrandom 0.2.17",
|
||||
@@ -6331,7 +6432,7 @@ dependencies = [
|
||||
"bech32 0.12.0",
|
||||
"bip39",
|
||||
"bitcoin_hashes 1.2.0",
|
||||
"cbc",
|
||||
"cbc 0.1.2",
|
||||
"chacha20 0.9.1",
|
||||
"chacha20poly1305",
|
||||
"faster-hex",
|
||||
@@ -6418,7 +6519,7 @@ version = "0.50.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7122,7 +7223,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.45.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7148,6 +7249,30 @@ dependencies = [
|
||||
"thiserror 2.0.18",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros"
|
||||
version = "0.18.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59"
|
||||
dependencies = [
|
||||
"aliasable",
|
||||
"ouroboros_macro",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros_macro"
|
||||
version = "0.18.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"proc-macro2-diagnostics",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "palette"
|
||||
version = "0.7.6"
|
||||
@@ -7265,6 +7390,25 @@ version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
|
||||
|
||||
[[package]]
|
||||
name = "pathfinder_geometry"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b7b7e7b4ea703700ce73ebf128e1450eb69c3a8329199ffbfb9b2a0418e5ad3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pathfinder_simd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathfinder_simd"
|
||||
version = "0.5.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4500030c302e4af1d423f36f3b958d1aecb6c04184356ed5a833bf6b60435777"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pbkdf2"
|
||||
version = "0.12.2"
|
||||
@@ -7765,6 +7909,28 @@ dependencies = [
|
||||
"num-integer",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "printpdf"
|
||||
version = "0.12.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e75e1f6d16f80c8f34918a4cc68743d703e8ee5016fac9508e223eb8e60445a0"
|
||||
dependencies = [
|
||||
"allsorts-azul",
|
||||
"base64 0.22.1",
|
||||
"flate2",
|
||||
"getrandom 0.4.3",
|
||||
"image",
|
||||
"lopdf",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"time",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"weezl 0.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "1.3.1"
|
||||
@@ -7827,6 +7993,19 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2-diagnostics"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.118",
|
||||
"version_check",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "process-wrap"
|
||||
version = "9.1.0"
|
||||
@@ -8069,6 +8248,12 @@ version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||
|
||||
[[package]]
|
||||
name = "qrcode"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d68782463e408eb1e668cf6152704bd856c78c5b6417adaee3203d8f4c1fc9ec"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
@@ -8148,7 +8333,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8276,6 +8461,12 @@ dependencies = [
|
||||
"rand_core 0.9.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rangemap"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d"
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.30.2"
|
||||
@@ -8879,7 +9070,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys 0.12.1",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8949,7 +9140,7 @@ dependencies = [
|
||||
"security-framework 3.7.0",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9161,7 +9352,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4d35ad99a181be0a60ffcbe85d680d98f87bdc4d7644ade319b87076b9dbfd4"
|
||||
dependencies = [
|
||||
"aes 0.8.4",
|
||||
"cbc",
|
||||
"cbc 0.1.2",
|
||||
"futures-util",
|
||||
"generic-array",
|
||||
"hkdf",
|
||||
@@ -9216,7 +9407,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9816,6 +10007,9 @@ name = "smallvec"
|
||||
version = "1.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket-pktinfo"
|
||||
@@ -9835,7 +10029,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10005,6 +10199,17 @@ dependencies = [
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stringprep"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
|
||||
dependencies = [
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
"unicode-properties",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strip-ansi-escapes"
|
||||
version = "0.2.1"
|
||||
@@ -10849,10 +11054,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.3",
|
||||
"once_cell",
|
||||
"rustix 1.1.4",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10874,7 +11079,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"rustix 1.1.4",
|
||||
"signal-hook 0.3.18",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10999,7 +11204,7 @@ dependencies = [
|
||||
"flate2",
|
||||
"half",
|
||||
"quick-error",
|
||||
"weezl",
|
||||
"weezl 0.1.12",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
@@ -11616,7 +11821,7 @@ dependencies = [
|
||||
"png 0.18.1",
|
||||
"serde",
|
||||
"thiserror 2.0.18",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11717,7 +11922,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
||||
dependencies = [
|
||||
"memoffset",
|
||||
"tempfile",
|
||||
"windows-sys 0.60.2",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11783,12 +11988,36 @@ version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-canonical-combining-class"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41c99d5174052d02ce765418e826597a1be18f32c114e35d9e22f92390239561"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-general-category"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-joining-type"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8d00a78170970967fdb83f9d49b92f959ab2bb829186b113e4f4604ad98e180"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.25"
|
||||
@@ -11807,6 +12036,12 @@ dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-properties"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.13.3"
|
||||
@@ -12455,6 +12690,12 @@ version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4ca08e5ef825b65b056d9efbd95c8750683f0a6d0466d02e96dc2e4e360f3d2"
|
||||
|
||||
[[package]]
|
||||
name = "wezterm-bidi"
|
||||
version = "0.2.3"
|
||||
@@ -12568,7 +12809,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -13503,6 +13744,12 @@ dependencies = [
|
||||
"xml-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.3"
|
||||
|
||||
@@ -133,6 +133,8 @@ tauri-plugin-global-shortcut = "2"
|
||||
tauri-plugin-notification = "2.3.3"
|
||||
uuid = { version = "1", features = ["v4", "v5"] }
|
||||
png = "0.18"
|
||||
printpdf = { version = "0.12.6", default-features = false, features = ["png"] }
|
||||
qrcode = { version = "0.14.1", default-features = false }
|
||||
# wayland-data-control: without it arboard is X11-only on Linux, so copies made
|
||||
# in a Wayland session land in XWayland's clipboard where Wayland-native apps
|
||||
# never see them (set_text still returns Ok). The backing wl-clipboard-rs dep is
|
||||
|
||||
@@ -414,6 +414,65 @@ pub async fn save_2skd_backup_copy(
|
||||
Ok(Some(dest.display().to_string()))
|
||||
}
|
||||
|
||||
/// Render and save the secret half of a recovery kit as a printable PDF.
|
||||
#[tauri::command]
|
||||
pub async fn save_2skd_recovery_sheet(
|
||||
backup: String,
|
||||
recovery_secret: String,
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<Option<String>, String> {
|
||||
use chrono::Datelike;
|
||||
|
||||
let recovery_secret = zeroize::Zeroizing::new(recovery_secret);
|
||||
crate::two_skd::validate_recovery_secret(&recovery_secret)?;
|
||||
let npub = crate::two_skd::backup_public_key(&backup)?
|
||||
.to_bech32()
|
||||
.map_err(|error| format!("encode recovery-sheet identity: {error}"))?;
|
||||
|
||||
let date = chrono::Local::now().date_naive();
|
||||
let month = [
|
||||
"January",
|
||||
"February",
|
||||
"March",
|
||||
"April",
|
||||
"May",
|
||||
"June",
|
||||
"July",
|
||||
"August",
|
||||
"September",
|
||||
"October",
|
||||
"November",
|
||||
"December",
|
||||
][date.month0() as usize];
|
||||
let created_date = format!("{month} {}, {}", date.day(), date.year());
|
||||
|
||||
let dest = match crate::commands::export_util::pick_save_path(
|
||||
&app_handle,
|
||||
crate::recovery_sheet::RECOVERY_SHEET_FILE_NAME,
|
||||
"Buzz recovery sheet",
|
||||
&["pdf"],
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(path) => path,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
let dest_for_write = dest.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let pdf = zeroize::Zeroizing::new(crate::recovery_sheet::render_recovery_sheet(
|
||||
&recovery_secret,
|
||||
&npub,
|
||||
&created_date,
|
||||
)?);
|
||||
crate::key_backup::write_portable_secret_file(&dest_for_write, &pdf)
|
||||
})
|
||||
.await
|
||||
.map_err(|error| format!("spawn_blocking failed: {error}"))??;
|
||||
|
||||
Ok(Some(dest.display().to_string()))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn import_identity(
|
||||
nsec: String,
|
||||
|
||||
@@ -188,6 +188,22 @@ pub fn write_backup_file(path: &std::path::Path, ncryptsec: &str) -> Result<(),
|
||||
/// the destination already exists. After writing, the file is synced and its
|
||||
/// persisted bytes are reread before success is reported.
|
||||
pub fn write_portable_backup_file(path: &std::path::Path, ncryptsec: &str) -> Result<(), String> {
|
||||
write_portable_secret_file_with_label(path, ncryptsec.as_bytes(), "backup file")
|
||||
}
|
||||
|
||||
/// Write secret-bearing bytes to an exclusively created, owner-only file.
|
||||
///
|
||||
/// This supports portable non-text artifacts such as recovery-sheet PDFs while
|
||||
/// preserving the save-panel and reread guarantees used for key backups.
|
||||
pub fn write_portable_secret_file(path: &std::path::Path, contents: &[u8]) -> Result<(), String> {
|
||||
write_portable_secret_file_with_label(path, contents, "recovery sheet")
|
||||
}
|
||||
|
||||
fn write_portable_secret_file_with_label(
|
||||
path: &std::path::Path,
|
||||
contents: &[u8],
|
||||
label: &str,
|
||||
) -> Result<(), String> {
|
||||
use std::io::Write;
|
||||
|
||||
let mut options = std::fs::OpenOptions::new();
|
||||
@@ -200,23 +216,22 @@ pub fn write_portable_backup_file(path: &std::path::Path, ncryptsec: &str) -> Re
|
||||
|
||||
let mut file = options.open(path).map_err(|error| {
|
||||
if error.kind() == std::io::ErrorKind::AlreadyExists {
|
||||
"backup file already exists; choose a new filename so the existing backup stays safe"
|
||||
.to_string()
|
||||
format!("{label} already exists; choose a new filename so the existing copy stays safe")
|
||||
} else {
|
||||
format!("create portable backup file: {error}")
|
||||
format!("create portable {label}: {error}")
|
||||
}
|
||||
})?;
|
||||
|
||||
let write_result = file
|
||||
.write_all(ncryptsec.as_bytes())
|
||||
.map_err(|e| format!("write portable backup file: {e}"))
|
||||
.write_all(contents)
|
||||
.map_err(|e| format!("write portable {label}: {e}"))
|
||||
.and_then(|()| {
|
||||
file.sync_all()
|
||||
.map_err(|e| format!("sync portable backup file: {e}"))
|
||||
.map_err(|e| format!("sync portable {label}: {e}"))
|
||||
});
|
||||
drop(file);
|
||||
|
||||
let result = write_result.and_then(|()| verify_backup_file(path, ncryptsec));
|
||||
let result = write_result.and_then(|()| verify_secret_file(path, contents, label));
|
||||
if result.is_err() {
|
||||
// This function created the destination exclusively, so cleanup cannot
|
||||
// clobber a backup that existed before the save attempt.
|
||||
@@ -225,6 +240,14 @@ pub fn write_portable_backup_file(path: &std::path::Path, ncryptsec: &str) -> Re
|
||||
result
|
||||
}
|
||||
|
||||
fn verify_secret_file(path: &std::path::Path, contents: &[u8], label: &str) -> Result<(), String> {
|
||||
let on_disk = std::fs::read(path).map_err(|e| format!("reread {label}: {e}"))?;
|
||||
if on_disk != contents {
|
||||
return Err(format!("{label} verification failed: on-disk bytes differ"));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn verify_backup_file(path: &std::path::Path, ncryptsec: &str) -> Result<(), String> {
|
||||
// Reread and byte-compare: only report success for bytes that are
|
||||
// actually on disk.
|
||||
|
||||
@@ -220,6 +220,23 @@ fn write_portable_backup_file_persists_0600_without_a_sibling() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_portable_secret_file_persists_binary_bytes_owner_only() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let path = dir.path().join("recovery-sheet.pdf");
|
||||
let contents = b"%PDF-1.7\0binary recovery sheet";
|
||||
|
||||
write_portable_secret_file(&path, contents).unwrap();
|
||||
|
||||
assert_eq!(std::fs::read(&path).unwrap(), contents);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = std::fs::metadata(&path).unwrap().permissions().mode();
|
||||
assert_eq!(mode & 0o777, 0o600, "recovery sheet must be owner-only");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_portable_backup_file_preserves_an_existing_backup() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
@@ -35,6 +35,7 @@ mod observed_unread;
|
||||
mod persona_catalog;
|
||||
mod prevent_sleep;
|
||||
mod ptt_shortcut;
|
||||
mod recovery_sheet;
|
||||
mod relay;
|
||||
mod relay_admission;
|
||||
mod reset;
|
||||
@@ -562,6 +563,7 @@ pub fn run() {
|
||||
verify_2skd_backup,
|
||||
save_ncryptsec_copy,
|
||||
save_2skd_backup_copy,
|
||||
save_2skd_recovery_sheet,
|
||||
import_identity,
|
||||
persist_current_identity,
|
||||
get_profile,
|
||||
|
||||
425
desktop/src-tauri/src/recovery_sheet.rs
Normal file
425
desktop/src-tauri/src/recovery_sheet.rs
Normal file
@@ -0,0 +1,425 @@
|
||||
//! Printable recovery-code sheet for a Buzz 2SKD recovery kit.
|
||||
//!
|
||||
//! The PDF deliberately contains only the recovery code and public identity.
|
||||
//! It never contains the password, private key, or encrypted backup artifact.
|
||||
|
||||
use printpdf::{
|
||||
BuiltinFont, Color, Mm, Op, PaintMode, PdfDocument, PdfFontHandle, PdfPage, PdfSaveOptions,
|
||||
Point, Pt, RawImage, Rect, Rgb, TextItem, WindingOrder, XObjectTransform,
|
||||
};
|
||||
use qrcode::{Color as QrColor, EcLevel, QrCode};
|
||||
|
||||
pub const RECOVERY_SHEET_FILE_NAME: &str = "buzz-recovery-sheet.pdf";
|
||||
|
||||
const PAGE_WIDTH_MM: f32 = 215.9;
|
||||
const PAGE_HEIGHT_MM: f32 = 279.4;
|
||||
const BUZZ_WORDMARK: &[u8] = include_bytes!("../../public/landing/buzz-wordmark.png");
|
||||
|
||||
fn charcoal() -> Color {
|
||||
Color::Rgb(Rgb::new(35.0 / 255.0, 30.0 / 255.0, 30.0 / 255.0, None))
|
||||
}
|
||||
|
||||
fn buzz_yellow() -> Color {
|
||||
Color::Rgb(Rgb::new(215.0 / 255.0, 215.0 / 255.0, 46.0 / 255.0, None))
|
||||
}
|
||||
|
||||
fn cream() -> Color {
|
||||
Color::Rgb(Rgb::new(247.0 / 255.0, 247.0 / 255.0, 235.0 / 255.0, None))
|
||||
}
|
||||
|
||||
fn muted() -> Color {
|
||||
Color::Rgb(Rgb::new(100.0 / 255.0, 94.0 / 255.0, 83.0 / 255.0, None))
|
||||
}
|
||||
|
||||
fn white() -> Color {
|
||||
Color::Rgb(Rgb::new(1.0, 1.0, 1.0, None))
|
||||
}
|
||||
|
||||
fn rect(x: f32, y: f32, width: f32, height: f32, mode: PaintMode) -> Rect {
|
||||
Rect {
|
||||
x: Mm(x).into(),
|
||||
y: Mm(y).into(),
|
||||
width: Mm(width).into(),
|
||||
height: Mm(height).into(),
|
||||
mode: Some(mode),
|
||||
winding_order: Some(WindingOrder::NonZero),
|
||||
}
|
||||
}
|
||||
|
||||
fn fill_rect(ops: &mut Vec<Op>, x: f32, y: f32, width: f32, height: f32, color: Color) {
|
||||
ops.push(Op::SetFillColor { col: color });
|
||||
ops.push(Op::DrawRectangle {
|
||||
rectangle: rect(x, y, width, height, PaintMode::Fill),
|
||||
});
|
||||
}
|
||||
|
||||
fn fill_stroke_rect(
|
||||
ops: &mut Vec<Op>,
|
||||
x: f32,
|
||||
y: f32,
|
||||
width: f32,
|
||||
height: f32,
|
||||
fill: Color,
|
||||
stroke: Color,
|
||||
) {
|
||||
ops.push(Op::SetFillColor { col: fill });
|
||||
ops.push(Op::SetOutlineColor { col: stroke });
|
||||
ops.push(Op::SetOutlineThickness { pt: Pt(0.8) });
|
||||
ops.push(Op::DrawRectangle {
|
||||
rectangle: rect(x, y, width, height, PaintMode::FillStroke),
|
||||
});
|
||||
}
|
||||
|
||||
fn text(
|
||||
ops: &mut Vec<Op>,
|
||||
x: f32,
|
||||
y: f32,
|
||||
size: f32,
|
||||
font: BuiltinFont,
|
||||
color: Color,
|
||||
value: impl Into<String>,
|
||||
) {
|
||||
ops.extend([
|
||||
Op::StartTextSection,
|
||||
Op::SetFillColor { col: color },
|
||||
Op::SetTextCursor {
|
||||
pos: Point::new(Mm(x), Mm(y)),
|
||||
},
|
||||
Op::SetFont {
|
||||
font: PdfFontHandle::Builtin(font),
|
||||
size: Pt(size),
|
||||
},
|
||||
Op::ShowText {
|
||||
items: vec![TextItem::Text(value.into())],
|
||||
},
|
||||
Op::EndTextSection,
|
||||
]);
|
||||
}
|
||||
|
||||
fn draw_dot_grid(ops: &mut Vec<Op>) {
|
||||
let dot = Color::Rgb(Rgb::new(211.0 / 255.0, 209.0 / 255.0, 198.0 / 255.0, None));
|
||||
let mut y = 215.0;
|
||||
let mut row = 0usize;
|
||||
while y < 274.0 {
|
||||
let offset = if row.is_multiple_of(2) { 0.0 } else { 3.5 };
|
||||
let mut x = 136.0 + offset;
|
||||
while x < 211.0 {
|
||||
fill_rect(ops, x, y, 0.4, 0.4, dot.clone());
|
||||
x += 7.0;
|
||||
}
|
||||
y += 7.0;
|
||||
row += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_qr(ops: &mut Vec<Op>, recovery_secret: &str) -> Result<(), String> {
|
||||
let code = QrCode::with_error_correction_level(recovery_secret.as_bytes(), EcLevel::Q)
|
||||
.map_err(|error| format!("encode recovery-code QR: {error}"))?;
|
||||
let quiet_modules = 4usize;
|
||||
let total_modules = code.width() + quiet_modules * 2;
|
||||
let qr_size_mm = 56.0f32;
|
||||
let module_mm = qr_size_mm / total_modules as f32;
|
||||
let qr_x = 136.0f32;
|
||||
let qr_y = 105.0f32;
|
||||
|
||||
fill_rect(ops, qr_x, qr_y, qr_size_mm, qr_size_mm, white());
|
||||
for y in 0..code.width() {
|
||||
for x in 0..code.width() {
|
||||
if code[(x, y)] == QrColor::Dark {
|
||||
let module_x = qr_x + (x + quiet_modules) as f32 * module_mm;
|
||||
let module_y = qr_y + (code.width() - 1 - y + quiet_modules) as f32 * module_mm;
|
||||
fill_rect(ops, module_x, module_y, module_mm, module_mm, charcoal());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn split_npub(npub: &str) -> (&str, &str) {
|
||||
let split_at = npub.len().min(32);
|
||||
npub.split_at(split_at)
|
||||
}
|
||||
|
||||
/// Render a one-page, print-ready recovery sheet.
|
||||
pub(crate) fn render_recovery_sheet(
|
||||
recovery_secret: &str,
|
||||
npub: &str,
|
||||
created_date: &str,
|
||||
) -> Result<Vec<u8>, String> {
|
||||
crate::two_skd::validate_recovery_secret(recovery_secret)?;
|
||||
if !npub.starts_with("npub1") || npub.len() < 20 {
|
||||
return Err("invalid recovery-sheet identity".to_string());
|
||||
}
|
||||
|
||||
let mut document = PdfDocument::new("Buzz recovery sheet");
|
||||
let mut ops = Vec::new();
|
||||
|
||||
fill_rect(&mut ops, 0.0, 0.0, PAGE_WIDTH_MM, PAGE_HEIGHT_MM, white());
|
||||
fill_rect(&mut ops, 0.0, 276.4, PAGE_WIDTH_MM, 3.0, buzz_yellow());
|
||||
draw_dot_grid(&mut ops);
|
||||
|
||||
let mut warnings = Vec::new();
|
||||
let wordmark = RawImage::decode_from_bytes(BUZZ_WORDMARK, &mut warnings)
|
||||
.map_err(|error| format!("decode Buzz wordmark: {error}"))?;
|
||||
let wordmark_id = document.add_image(&wordmark);
|
||||
ops.push(Op::UseXobject {
|
||||
id: wordmark_id,
|
||||
transform: XObjectTransform {
|
||||
translate_x: Some(Mm(18.0).into()),
|
||||
translate_y: Some(Mm(242.0).into()),
|
||||
scale_x: Some(0.82),
|
||||
scale_y: Some(0.82),
|
||||
dpi: Some(300.0),
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
text(
|
||||
&mut ops,
|
||||
18.0,
|
||||
231.0,
|
||||
8.0,
|
||||
BuiltinFont::HelveticaBold,
|
||||
charcoal(),
|
||||
"RECOVERY SHEET / KEEP OFFLINE",
|
||||
);
|
||||
text(
|
||||
&mut ops,
|
||||
18.0,
|
||||
211.0,
|
||||
27.0,
|
||||
BuiltinFont::HelveticaBold,
|
||||
charcoal(),
|
||||
"Keep this separate.",
|
||||
);
|
||||
text(
|
||||
&mut ops,
|
||||
18.0,
|
||||
200.5,
|
||||
11.0,
|
||||
BuiltinFont::Helvetica,
|
||||
charcoal(),
|
||||
"This paper is one of three things required to recover your identity.",
|
||||
);
|
||||
fill_rect(&mut ops, 18.0, 188.0, 24.0, 2.0, buzz_yellow());
|
||||
|
||||
fill_stroke_rect(&mut ops, 18.0, 78.0, 179.9, 104.0, white(), charcoal());
|
||||
text(
|
||||
&mut ops,
|
||||
28.0,
|
||||
165.0,
|
||||
8.0,
|
||||
BuiltinFont::HelveticaBold,
|
||||
muted(),
|
||||
"YOUR RECOVERY CODE",
|
||||
);
|
||||
text(
|
||||
&mut ops,
|
||||
28.0,
|
||||
151.0,
|
||||
19.0,
|
||||
BuiltinFont::HelveticaBold,
|
||||
charcoal(),
|
||||
"One code.",
|
||||
);
|
||||
text(
|
||||
&mut ops,
|
||||
28.0,
|
||||
142.5,
|
||||
19.0,
|
||||
BuiltinFont::HelveticaBold,
|
||||
charcoal(),
|
||||
"Store it offline.",
|
||||
);
|
||||
fill_stroke_rect(&mut ops, 28.0, 122.0, 96.0, 11.0, cream(), muted());
|
||||
fill_rect(&mut ops, 28.0, 122.0, 2.0, 11.0, buzz_yellow());
|
||||
text(
|
||||
&mut ops,
|
||||
32.0,
|
||||
126.0,
|
||||
8.0,
|
||||
BuiltinFont::CourierBold,
|
||||
charcoal(),
|
||||
recovery_secret,
|
||||
);
|
||||
|
||||
text(
|
||||
&mut ops,
|
||||
28.0,
|
||||
111.0,
|
||||
7.0,
|
||||
BuiltinFont::HelveticaBold,
|
||||
muted(),
|
||||
"BUZZ IDENTITY",
|
||||
);
|
||||
let (npub_first, npub_second) = split_npub(npub);
|
||||
text(
|
||||
&mut ops,
|
||||
28.0,
|
||||
104.5,
|
||||
7.5,
|
||||
BuiltinFont::Courier,
|
||||
charcoal(),
|
||||
npub_first,
|
||||
);
|
||||
text(
|
||||
&mut ops,
|
||||
28.0,
|
||||
99.5,
|
||||
7.5,
|
||||
BuiltinFont::Courier,
|
||||
charcoal(),
|
||||
npub_second,
|
||||
);
|
||||
text(
|
||||
&mut ops,
|
||||
28.0,
|
||||
89.0,
|
||||
7.0,
|
||||
BuiltinFont::HelveticaBold,
|
||||
muted(),
|
||||
format!("CREATED {created_date}"),
|
||||
);
|
||||
|
||||
draw_qr(&mut ops, recovery_secret)?;
|
||||
text(
|
||||
&mut ops,
|
||||
143.0,
|
||||
96.0,
|
||||
7.0,
|
||||
BuiltinFont::Helvetica,
|
||||
muted(),
|
||||
"QR CONTAINS THE RECOVERY CODE",
|
||||
);
|
||||
|
||||
text(
|
||||
&mut ops,
|
||||
18.0,
|
||||
67.5,
|
||||
8.0,
|
||||
BuiltinFont::HelveticaBold,
|
||||
muted(),
|
||||
"RECOVERY NEEDS ALL THREE",
|
||||
);
|
||||
let requirements = [
|
||||
("1", "Encrypted backup", "identity.buzzbackup"),
|
||||
("2", "Your password", "Known only to you"),
|
||||
("3", "This sheet", "Stored separately"),
|
||||
];
|
||||
for (index, (number, title, detail)) in requirements.iter().enumerate() {
|
||||
let x = 18.0 + index as f32 * 61.0;
|
||||
fill_stroke_rect(&mut ops, x, 39.0, 57.9, 23.0, white(), charcoal());
|
||||
fill_rect(&mut ops, x, 54.0, 8.0, 8.0, buzz_yellow());
|
||||
text(
|
||||
&mut ops,
|
||||
x + 2.55,
|
||||
56.2,
|
||||
8.0,
|
||||
BuiltinFont::HelveticaBold,
|
||||
charcoal(),
|
||||
*number,
|
||||
);
|
||||
text(
|
||||
&mut ops,
|
||||
x + 4.0,
|
||||
48.0,
|
||||
9.0,
|
||||
BuiltinFont::HelveticaBold,
|
||||
charcoal(),
|
||||
*title,
|
||||
);
|
||||
text(
|
||||
&mut ops,
|
||||
x + 4.0,
|
||||
42.5,
|
||||
7.0,
|
||||
BuiltinFont::Helvetica,
|
||||
muted(),
|
||||
*detail,
|
||||
);
|
||||
}
|
||||
|
||||
fill_stroke_rect(&mut ops, 18.0, 14.0, 179.9, 17.0, white(), charcoal());
|
||||
fill_rect(&mut ops, 18.0, 14.0, 3.0, 17.0, buzz_yellow());
|
||||
text(
|
||||
&mut ops,
|
||||
24.0,
|
||||
24.0,
|
||||
9.0,
|
||||
BuiltinFont::HelveticaBold,
|
||||
charcoal(),
|
||||
"DO NOT STORE THIS SHEET WITH YOUR ENCRYPTED BACKUP.",
|
||||
);
|
||||
text(
|
||||
&mut ops,
|
||||
24.0,
|
||||
18.5,
|
||||
8.0,
|
||||
BuiltinFont::Helvetica,
|
||||
muted(),
|
||||
"Buzz cannot recreate this code or reset your recovery password.",
|
||||
);
|
||||
text(
|
||||
&mut ops,
|
||||
18.0,
|
||||
6.0,
|
||||
7.0,
|
||||
BuiltinFont::HelveticaBold,
|
||||
muted(),
|
||||
"buzz.xyz / recovery sheet v1",
|
||||
);
|
||||
|
||||
let page = PdfPage::new(Mm(PAGE_WIDTH_MM), Mm(PAGE_HEIGHT_MM), ops);
|
||||
let bytes = document
|
||||
.with_pages(vec![page])
|
||||
.save(&PdfSaveOptions::default(), &mut warnings);
|
||||
if !bytes.starts_with(b"%PDF-") {
|
||||
return Err("render recovery sheet: invalid PDF output".to_string());
|
||||
}
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use printpdf::{PdfDocument, PdfParseOptions};
|
||||
|
||||
const RECOVERY_CODE: &str = "buzz-recovery-v1-00112233445566778899aabbccddeeff";
|
||||
const NPUB: &str = "npub1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq5hq3g5";
|
||||
|
||||
#[test]
|
||||
fn renders_a_single_page_with_recovery_material_and_no_backup() {
|
||||
let bytes = render_recovery_sheet(RECOVERY_CODE, NPUB, "August 21, 2026").unwrap();
|
||||
let mut warnings = Vec::new();
|
||||
let parsed = PdfDocument::parse(
|
||||
&bytes,
|
||||
&PdfParseOptions {
|
||||
fail_on_error: true,
|
||||
},
|
||||
&mut warnings,
|
||||
)
|
||||
.unwrap();
|
||||
let text = parsed
|
||||
.extract_text()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<String>();
|
||||
|
||||
assert_eq!(parsed.pages.len(), 1);
|
||||
assert!(text.contains(RECOVERY_CODE));
|
||||
assert!(text.contains("Keep this separate."));
|
||||
assert!(text.contains(NPUB));
|
||||
assert!(!text.contains("buzz2skd1:"));
|
||||
assert!(!text.contains("password="));
|
||||
|
||||
if let Ok(path) = std::env::var("BUZZ_RECOVERY_SHEET_PREVIEW") {
|
||||
std::fs::write(path, bytes).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_malformed_recovery_material() {
|
||||
assert!(render_recovery_sheet("not-a-code", NPUB, "August 21, 2026").is_err());
|
||||
assert!(render_recovery_sheet(RECOVERY_CODE, "not-an-npub", "August 21, 2026").is_err());
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ use base64::Engine;
|
||||
use chacha20poly1305::aead::{Aead, KeyInit, Payload};
|
||||
use chacha20poly1305::{ChaCha20Poly1305, Nonce};
|
||||
use hkdf::Hkdf;
|
||||
use nostr::{Keys, SecretKey};
|
||||
use nostr::{Keys, PublicKey, SecretKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2_legacy::Sha256;
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
@@ -240,6 +240,11 @@ pub(crate) fn validate_backup(input: &str) -> Result<(), String> {
|
||||
parse_backup(input).map(|_| ())
|
||||
}
|
||||
|
||||
pub(crate) fn backup_public_key(input: &str) -> Result<PublicKey, String> {
|
||||
let payload = parse_backup(input)?;
|
||||
PublicKey::parse(&payload.pubkey).map_err(|_| "invalid backup public key".to_string())
|
||||
}
|
||||
|
||||
fn validate_payload_shape(payload: &BackupPayload) -> Result<(), String> {
|
||||
validate_cost(payload.cost())?;
|
||||
decode_array::<SALT_BYTES>(&payload.salt, "backup salt")?;
|
||||
@@ -264,6 +269,10 @@ fn parse_recovery_secret(input: &str) -> Result<Zeroizing<[u8; RECOVERY_SECRET_B
|
||||
Ok(Zeroizing::new(secret))
|
||||
}
|
||||
|
||||
pub(crate) fn validate_recovery_secret(input: &str) -> Result<(), String> {
|
||||
parse_recovery_secret(input).map(|_| ())
|
||||
}
|
||||
|
||||
fn decode_array<const N: usize>(encoded: &str, label: &str) -> Result<[u8; N], String> {
|
||||
let bytes = URL_SAFE_NO_PAD
|
||||
.decode(encoded)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Check, Copy, Eye, EyeOff, ShieldCheck } from "lucide-react";
|
||||
import { Check, Copy, Eye, EyeOff, FileDown, ShieldCheck } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import {
|
||||
createTwoSkdBackup,
|
||||
type CreatedTwoSkdBackup,
|
||||
saveTwoSkdBackupCopy,
|
||||
saveTwoSkdRecoverySheet,
|
||||
} from "@/shared/api/tauriIdentity";
|
||||
import { writeTextToClipboard } from "@/shared/lib/clipboard";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
@@ -35,6 +36,9 @@ export function TwoSkdRecoveryCreator({
|
||||
null,
|
||||
);
|
||||
const [savedPath, setSavedPath] = React.useState<string | null>(null);
|
||||
const [recoverySheetPath, setRecoverySheetPath] = React.useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [pending, setPending] = React.useState(false);
|
||||
const [copied, setCopied] = React.useState(false);
|
||||
const [error, setError] = React.useState<string | null>(null);
|
||||
@@ -47,6 +51,7 @@ export function TwoSkdRecoveryCreator({
|
||||
setRevealed(false);
|
||||
setCreated(null);
|
||||
setSavedPath(null);
|
||||
setRecoverySheetPath(null);
|
||||
setPending(false);
|
||||
setCopied(false);
|
||||
setError(null);
|
||||
@@ -118,6 +123,30 @@ export function TwoSkdRecoveryCreator({
|
||||
}
|
||||
}, [created]);
|
||||
|
||||
const saveRecoverySheet = React.useCallback(async () => {
|
||||
if (!created || pending) return;
|
||||
const requestId = ++requestRef.current;
|
||||
setPending(true);
|
||||
setError(null);
|
||||
try {
|
||||
const path = await saveTwoSkdRecoverySheet(
|
||||
created.backup,
|
||||
created.recoverySecret,
|
||||
);
|
||||
if (requestId !== requestRef.current) return;
|
||||
if (path) setRecoverySheetPath(path);
|
||||
} catch (cause) {
|
||||
if (requestId !== requestRef.current) return;
|
||||
setError(
|
||||
cause instanceof Error
|
||||
? cause.message
|
||||
: "Could not save the recovery sheet.",
|
||||
);
|
||||
} finally {
|
||||
if (requestId === requestRef.current) setPending(false);
|
||||
}
|
||||
}, [created, pending]);
|
||||
|
||||
const retrySave = React.useCallback(async () => {
|
||||
if (!created || pending) return;
|
||||
const requestId = ++requestRef.current;
|
||||
@@ -227,10 +256,19 @@ export function TwoSkdRecoveryCreator({
|
||||
</div>
|
||||
<p className="text-xs leading-5 text-muted-foreground">
|
||||
The encrypted backup was saved to {savedPath}. Put this code in a
|
||||
different place, such as your password manager or a printed QR.
|
||||
Buzz cannot recover it later.
|
||||
different place. The printable PDF includes a QR code and recovery
|
||||
instructions, but never your password or encrypted backup. Buzz
|
||||
cannot recreate it later.
|
||||
</p>
|
||||
<div className="flex justify-end gap-2">
|
||||
{recoverySheetPath ? (
|
||||
<p
|
||||
className="rounded-lg bg-muted px-3 py-2 text-xs text-muted-foreground"
|
||||
data-testid="two-skd-recovery-sheet-path"
|
||||
>
|
||||
Recovery sheet saved to {recoverySheetPath}
|
||||
</p>
|
||||
) : null}
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<Button
|
||||
data-testid="two-skd-copy-code"
|
||||
onClick={() => void copyRecoveryCode()}
|
||||
@@ -244,7 +282,26 @@ export function TwoSkdRecoveryCreator({
|
||||
)}
|
||||
{copied ? "Copied" : "Copy code"}
|
||||
</Button>
|
||||
<Button onClick={() => handleOpenChange(false)} type="button">
|
||||
<Button
|
||||
data-testid="two-skd-save-recovery-sheet"
|
||||
disabled={pending}
|
||||
onClick={() => void saveRecoverySheet()}
|
||||
type="button"
|
||||
>
|
||||
{pending ? (
|
||||
<Spinner className="size-4 border-2" />
|
||||
) : recoverySheetPath ? (
|
||||
<Check aria-hidden="true" className="size-4" />
|
||||
) : (
|
||||
<FileDown aria-hidden="true" className="size-4" />
|
||||
)}
|
||||
{recoverySheetPath ? "Save another PDF" : "Save printable PDF"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleOpenChange(false)}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -104,6 +104,19 @@ export async function saveTwoSkdBackupCopy(
|
||||
);
|
||||
}
|
||||
|
||||
/** Save the recovery code and public identity as a printable PDF. */
|
||||
export async function saveTwoSkdRecoverySheet(
|
||||
backup: string,
|
||||
recoverySecret: string,
|
||||
): Promise<string | null> {
|
||||
return (
|
||||
(await invokeTauri<string | null>("save_2skd_recovery_sheet", {
|
||||
backup,
|
||||
recoverySecret,
|
||||
})) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/** Save a portable backup copy. Returns null when the native dialog is cancelled. */
|
||||
export async function saveNcryptsecCopy(
|
||||
ncryptsec: string,
|
||||
|
||||
@@ -568,6 +568,8 @@ type E2eConfig = {
|
||||
backupEncryptionDelayMs?: number;
|
||||
/** Native paths returned by successive backup saves. */
|
||||
backupSavePaths?: Array<string | null>;
|
||||
/** Native paths returned by successive recovery-sheet PDF saves. */
|
||||
recoverySheetSavePaths?: Array<string | null>;
|
||||
/**
|
||||
* When set, `get_nsec` throws with this message instead of returning the
|
||||
* mock nsec string. Use `nsecErrors` for sequenced failure/success.
|
||||
@@ -8168,6 +8170,7 @@ let mockGlobalAgentConfig: {
|
||||
let nsecCallCount = 0;
|
||||
let backupVerificationCallCount = 0;
|
||||
let backupSaveCallCount = 0;
|
||||
let recoverySheetSaveCallCount = 0;
|
||||
|
||||
const MOCK_NCRYPTSEC =
|
||||
"ncryptsec1qgg9947rlpvqu76pj5ecreduf9jxhselq2nae2kghhvd5g7dgjtcxfqtd67p9m0w57lspw8gsq6yphnm8623nsl8xn9j4jdzz84zm3frztj3z7s35vpzmqf6ksu8r89qk5z2zxfmu5gv8th8wclt0h4p";
|
||||
@@ -11769,6 +11772,24 @@ export function maybeInstallE2eTauriMocks() {
|
||||
backupSaveCallCount += 1;
|
||||
return paths[index];
|
||||
}
|
||||
case "save_2skd_recovery_sheet": {
|
||||
const request = payload as {
|
||||
backup?: string;
|
||||
recoverySecret?: string;
|
||||
} | null;
|
||||
if (
|
||||
request?.backup !== MOCK_TWO_SKD_BACKUP ||
|
||||
request?.recoverySecret !== MOCK_RECOVERY_SECRET
|
||||
) {
|
||||
throw new Error("invalid recovery sheet material");
|
||||
}
|
||||
const paths = activeConfig?.mock?.recoverySheetSavePaths ?? [
|
||||
"/tmp/buzz-recovery-sheet.pdf",
|
||||
];
|
||||
const index = Math.min(recoverySheetSaveCallCount, paths.length - 1);
|
||||
recoverySheetSaveCallCount += 1;
|
||||
return paths[index];
|
||||
}
|
||||
case "verify_ncryptsec_backup": {
|
||||
const request = payload as { password?: string } | null;
|
||||
const configuredErrors = activeConfig?.mock?.backupVerificationErrors;
|
||||
|
||||
@@ -130,6 +130,7 @@ test("2SKD recovery kit keeps the recovery code separate from the saved backup",
|
||||
}) => {
|
||||
await openBackupSettings(page, {
|
||||
backupSavePaths: ["/Users/test/Downloads/identity.buzzbackup"],
|
||||
recoverySheetSavePaths: ["/Users/test/Documents/buzz-recovery-sheet.pdf"],
|
||||
});
|
||||
const dialog = await openCreateRecoveryKit(page);
|
||||
const password = dialog.getByLabel("Recovery password");
|
||||
@@ -147,11 +148,16 @@ test("2SKD recovery kit keeps the recovery code separate from the saved backup",
|
||||
await expect(dialog).toContainText(
|
||||
"/Users/test/Downloads/identity.buzzbackup",
|
||||
);
|
||||
await dialog.getByTestId("two-skd-save-recovery-sheet").click();
|
||||
await expect(dialog.getByTestId("two-skd-recovery-sheet-path")).toContainText(
|
||||
"/Users/test/Documents/buzz-recovery-sheet.pdf",
|
||||
);
|
||||
const commands = await page.evaluate(
|
||||
() => window.__BUZZ_E2E_COMMANDS__ ?? [],
|
||||
);
|
||||
expect(commands).toContain("create_2skd_backup");
|
||||
expect(commands).toContain("save_2skd_backup_copy");
|
||||
expect(commands).toContain("save_2skd_recovery_sheet");
|
||||
});
|
||||
|
||||
test("2SKD recovery kit verification requires the code and password", async ({
|
||||
|
||||
Reference in New Issue
Block a user