diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fb0f71eb7..84d48d7dcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,7 +20,7 @@ during handling the JSON-RPC request. #4153 - Delete expired messages using multiple SQL requests. #4158 - Do not emit "Failed to run incremental vacuum" warnings on success. #4160 - +- Ability to send backup over network and QR code to setup second device #4007 ## 1.111.0 diff --git a/Cargo.lock b/Cargo.lock index 2919cc46c0..0e4e323fd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,19 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +[[package]] +name = "abao" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2daa0989489b05a455a9707adbbbc17443edf7bbc902ce499cd3b84148d68a40" +dependencies = [ + "arrayref", + "arrayvec", + "blake3", + "futures", + "tokio", +] + [[package]] name = "addr2line" version = "0.19.0" @@ -89,6 +102,21 @@ name = "anyhow" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" +dependencies = [ + "backtrace", +] + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] name = "ascii_utils" @@ -96,6 +124,45 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" +[[package]] +name = "asn1-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf6690c370453db30743b373a60ba498fc0d6d83b11f4abfd87a84a075db5dd4" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror", + "time 0.3.20", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", +] + [[package]] name = "async-channel" version = "1.8.0" @@ -198,9 +265,9 @@ version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -311,6 +378,18 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + [[package]] name = "base64" version = "0.11.0" @@ -341,6 +420,15 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitfield" version = "0.14.0" @@ -353,6 +441,20 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blake3" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ae2468a89544a466886840aa467a25b766499f4f04bf7d9fcd10ecee9fccef" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "digest 0.10.6", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -434,6 +536,37 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +[[package]] +name = "camino" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6031a462f977dd38968b6f23378356512feeace69cef817e1a4475108093cec3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] + [[package]] name = "cast" version = "0.3.0" @@ -490,7 +623,7 @@ dependencies = [ "js-sys", "num-integer", "num-traits", - "time", + "time 0.1.45", "wasm-bindgen", "winapi", ] @@ -539,11 +672,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "bitflags", - "clap_lex", + "clap_lex 0.2.4", "indexmap", "textwrap", ] +[[package]] +name = "clap" +version = "4.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d7ae14b20b94cb02149ed21a86c423859cbe18dc7ed69845cace50e52b40a5" +dependencies = [ + "bitflags", + "clap_derive", + "clap_lex 0.3.2", + "is-terminal", + "once_cell", + "strsim", + "termcolor", +] + +[[package]] +name = "clap_derive" +version = "4.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bec8e5c9d09e439c4335b1af0abaab56dcf3b94999a936e1bb47b9134288f0" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", +] + [[package]] name = "clap_lex" version = "0.2.4" @@ -553,6 +714,15 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "clap_lex" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" +dependencies = [ + "os_str_bytes", +] + [[package]] name = "clipboard-win" version = "4.5.0" @@ -564,6 +734,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -606,12 +782,57 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d79fbe8970a77e3e34151cc13d3b3e248aa0faaecb9f6091fa07ebefe5ad60" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width", + "windows-sys 0.42.0", +] + [[package]] name = "const-oid" version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" +[[package]] +name = "const_format" +version = "0.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7309d9b4d3d2c0641e018d449232f2e28f1b22933c137f157d3dbc14228b8c0e" +dependencies = [ + "const_format_proc_macros", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f47bf7270cf70d370f8f98c1abb6d2d4cf60a6845d30e05bfb90c6568650" +dependencies = [ + "proc-macro2 1.0.51", + "quote 1.0.23", + "unicode-xid 0.2.4", +] + +[[package]] +name = "constant_time_eq" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ad85c1f65dc7b37604eb0e89748faf0b9653065f2a8ef69f96a687ec1e9279" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "convert_case" version = "0.5.0" @@ -668,7 +889,7 @@ dependencies = [ "atty", "cast", "ciborium", - "clap", + "clap 3.2.23", "criterion-plot", "futures", "itertools", @@ -749,6 +970,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -793,10 +1026,10 @@ dependencies = [ "cc", "codespan-reporting", "once_cell", - "proc-macro2", - "quote", + "proc-macro2 1.0.51", + "quote 1.0.23", "scratch", - "syn", + "syn 1.0.109", ] [[package]] @@ -811,9 +1044,9 @@ version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "086c685979a698443656e5cf7856c95c642295a38599f12fb1ff76fb28d19892" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -844,10 +1077,10 @@ checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" dependencies = [ "fnv", "ident_case", - "proc-macro2", - "quote", + "proc-macro2 1.0.51", + "quote 1.0.23", "strsim", - "syn", + "syn 1.0.109", ] [[package]] @@ -858,10 +1091,10 @@ checksum = "001d80444f28e193f30c2f293455da62dcf9a6b29918a4253152ae2b1de592cb" dependencies = [ "fnv", "ident_case", - "proc-macro2", - "quote", + "proc-macro2 1.0.51", + "quote 1.0.23", "strsim", - "syn", + "syn 1.0.109", ] [[package]] @@ -871,8 +1104,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ "darling_core 0.13.4", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -882,8 +1115,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b36230598a2d5de7ec1c6f51f72d8a99a9208daff41de2084d06e3fd3ea56685" dependencies = [ "darling_core 0.14.3", - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -892,6 +1125,42 @@ version = "2.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb" +[[package]] +name = "data-encoding-macro" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86927b7cd2fe88fa698b87404b287ab98d1a0063a34071d92e575b72d3029aca" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5bbed42daaa95e780b60a50546aa345b8413a1e46f9a40a12907d3598f038db" +dependencies = [ + "data-encoding", + "syn 1.0.109", +] + +[[package]] +name = "default-net" +version = "0.13.1" +source = "git+https://github.com/dignifiedquire/default-net.git?branch=feat-android#7a257095bac009c4be0b93c2979801624fdd337b" +dependencies = [ + "dlopen", + "libc", + "memalloc", + "netlink-packet-core", + "netlink-packet-route", + "netlink-sys", + "once_cell", + "system-configuration", + "windows", +] + [[package]] name = "deltachat" version = "1.111.0" @@ -919,6 +1188,7 @@ dependencies = [ "hex", "humansize", "image", + "iroh", "kamadak-exif", "lettre_email", "libc", @@ -951,6 +1221,7 @@ dependencies = [ "strum_macros", "tagger", "tempfile", + "testdir", "textwrap", "thiserror", "tokio", @@ -1020,8 +1291,8 @@ dependencies = [ name = "deltachat_derive" version = "2.0.0" dependencies = [ - "quote", - "syn", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -1048,10 +1319,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" dependencies = [ "const-oid", + "der_derive", "pem-rfc7468", "zeroize", ] +[[package]] +name = "der-parser" +version = "8.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d4bc9b0db0a0df9ae64634ac5bdefb7afcb534e182275ca0beadbe486701c1" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "der_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ef71ddb5b3a1f53dee24817c8f70dfa1cb29e804c18d88c228d4bc9c86ee3b9" +dependencies = [ + "proc-macro-error", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", +] + [[package]] name = "derive_builder" version = "0.11.2" @@ -1068,9 +1366,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4" dependencies = [ "darling 0.14.3", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -1080,7 +1378,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68" dependencies = [ "derive_builder_core", - "syn", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2 1.0.51", + "quote 1.0.23", + "rustc_version", + "syn 1.0.109", ] [[package]] @@ -1110,6 +1421,7 @@ dependencies = [ "block-buffer 0.10.3", "const-oid", "crypto-common", + "subtle", ] [[package]] @@ -1153,12 +1465,59 @@ dependencies = [ "winapi", ] +[[package]] +name = "displaydoc" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +dependencies = [ + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", +] + +[[package]] +name = "dlopen" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e80ad39f814a9abe68583cd50a2d45c8a67561c3361ab8da240587dda80937" +dependencies = [ + "dlopen_derive", + "lazy_static", + "libc", + "winapi", +] + +[[package]] +name = "dlopen_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f236d9e1b1fbd81cea0f9cbdc8dcc7e8ebcd80e6659cd7cb2ad5f6c05946c581" +dependencies = [ + "libc", + "quote 0.6.13", + "syn 0.15.44", +] + +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature", +] + [[package]] name = "ed25519" version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" dependencies = [ + "serde", "signature", ] @@ -1172,16 +1531,48 @@ dependencies = [ "ed25519", "rand 0.7.3", "serde", + "serde_bytes", "sha2 0.9.9", "zeroize", ] +[[package]] +name = "educe" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0188e3c3ba8df5753894d54461f0e39bc91741dc5b22e1c46999ec2c71f4e4" +dependencies = [ + "enum-ordinalize", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", +] + [[package]] name = "either" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint", + "der", + "digest 0.10.6", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "email" version = "0.0.21" @@ -1193,10 +1584,16 @@ dependencies = [ "encoding", "lazy_static", "rand 0.7.3", - "time", + "time 0.1.45", "version_check 0.9.4", ] +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoded-words" version = "0.2.0" @@ -1303,9 +1700,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" dependencies = [ "heck", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", +] + +[[package]] +name = "enum-ordinalize" +version = "3.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bb1df8b45ecb7ffa78dca1c17a438fb193eb083db0b1b494d2a61bcb5096a" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2 1.0.51", + "quote 1.0.23", + "rustc_version", + "syn 1.0.109", ] [[package]] @@ -1434,6 +1845,16 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "filetime" version = "0.2.20" @@ -1456,6 +1877,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.10.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +dependencies = [ + "futures-core", + "futures-sink", + "nanorand", + "pin-project", + "spin 0.9.6", +] + [[package]] name = "fnv" version = "1.0.7" @@ -1559,9 +1993,9 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95a73af87da33b5acf53acfebdc339fe592ecf5357ac7c0a7734ab9d8c876a70" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -1622,8 +2056,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1642,6 +2078,17 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.3.16" @@ -1721,6 +2168,15 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.6", +] + [[package]] name = "hostname" version = "0.3.1" @@ -1935,6 +2391,19 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cef509aa9bc73864d6756f0d34d35504af3cf0844373afe9b8669a5b8005a729" +dependencies = [ + "console", + "number_prefix", + "portable-atomic 0.3.19", + "tokio", + "unicode-width", +] + [[package]] name = "inout" version = "0.1.3" @@ -1981,6 +2450,52 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30e22bd8629359895450b59ea7a776c850561b96a3b1d31321c1949d9e6c9146" +[[package]] +name = "iroh" +version = "0.3.0" +source = "git+https://github.com/n0-computer/iroh?branch=flub/ticket-multiple-addrs#aacd1d84e11b218e55e9622c4500973224904fe9" +dependencies = [ + "abao", + "anyhow", + "base64 0.21.0", + "blake3", + "bytes", + "clap 4.1.8", + "console", + "data-encoding", + "default-net", + "der", + "derive_more", + "dirs-next", + "ed25519-dalek", + "futures", + "hex", + "indicatif", + "multibase", + "num_cpus", + "portable-atomic 1.0.1", + "postcard", + "quic-rpc", + "quinn", + "rand 0.7.3", + "rcgen", + "ring", + "rustls", + "serde", + "serde-error", + "ssh-key", + "tempfile", + "thiserror", + "tokio", + "tokio-util", + "tracing", + "tracing-futures", + "tracing-subscriber", + "webpki", + "x509-parser", + "zeroize", +] + [[package]] name = "is-terminal" version = "0.4.4" @@ -2047,7 +2562,7 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" dependencies = [ - "spin", + "spin 0.5.2", ] [[package]] @@ -2070,7 +2585,7 @@ dependencies = [ "lettre", "mime", "regex", - "time", + "time 0.1.45", "uuid 0.8.2", ] @@ -2164,6 +2679,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + [[package]] name = "matches" version = "0.1.10" @@ -2185,6 +2709,12 @@ dependencies = [ "digest 0.10.6", ] +[[package]] +name = "memalloc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1" + [[package]] name = "memchr" version = "2.5.0" @@ -2233,12 +2763,32 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "multibase" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" +dependencies = [ + "base-x", + "data-encoding", + "data-encoding-macro", +] + [[package]] name = "mutate_once" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16cf681a23b4d0a43fc35024c176437f9dcd818db34e0f42ab456a0ee5ad497b" +[[package]] +name = "nanorand" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" +dependencies = [ + "getrandom 0.2.8", +] + [[package]] name = "native-tls" version = "0.2.11" @@ -2257,6 +2807,55 @@ dependencies = [ "tempfile", ] +[[package]] +name = "netlink-packet-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e5cf0b54effda4b91615c40ff0fd12d0d4c9a6e0f5116874f03941792ff535a" +dependencies = [ + "anyhow", + "byteorder", + "libc", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-route" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea993e32c77d87f01236c38f572ecb6c311d592e56a06262a007fd2a6e31253c" +dependencies = [ + "anyhow", + "bitflags", + "byteorder", + "libc", + "netlink-packet-core", + "netlink-packet-utils", +] + +[[package]] +name = "netlink-packet-utils" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ede8a08c71ad5a95cdd0e4e52facd37190977039a4704eb82a283f713747d34" +dependencies = [ + "anyhow", + "byteorder", + "paste", + "thiserror", +] + +[[package]] +name = "netlink-sys" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6471bf08e7ac0135876a9581bf3217ef0333c191c128d34878079f42ee150411" +dependencies = [ + "bytes", + "libc", + "log", +] + [[package]] name = "nibble_vec" version = "0.1.0" @@ -2298,6 +2897,36 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "ntapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc51db7b362b205941f71232e56c625156eb9a929f8cf74a428fd5bc094a4afc" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.2" @@ -2322,9 +2951,9 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -2379,6 +3008,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "object" version = "0.30.3" @@ -2388,6 +3023,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -2427,9 +3071,9 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -2496,9 +3140,37 @@ checksum = "5f7d21ccd03305a674437ee1248f3ab5d4b1db095cf1caf49f1713ddf61956b7" dependencies = [ "Inflector", "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", +] + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2 0.10.6", +] + +[[package]] +name = "p384" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2 0.10.6", ] [[package]] @@ -2530,6 +3202,21 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + [[package]] name = "pem-rfc7468" version = "0.6.0" @@ -2605,9 +3292,9 @@ version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -2690,6 +3377,41 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "portable-atomic" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f6a7b87c2e435a3241addceeeff740ff8b7e76b74c13bf9acb17fa454ea00b" + +[[package]] +name = "portable-atomic" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39c00c8683a03bd4fe7db7dd64ab4abee6b42166bc81231da983486ce96be51a" + +[[package]] +name = "postcard" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfa512cd0d087cc9f99ad30a1bf64795b67871edbead083ffc3a4dfafa59aa00" +dependencies = [ + "cobs", + "const_format", + "postcard-derive", + "serde", +] + +[[package]] +name = "postcard-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4b01218787dd4420daf63875163a787a78294ad48a24e9f6fa8c6507759a79" +dependencies = [ + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2713,9 +3435,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", "version_check 0.9.4", ] @@ -2725,11 +3447,20 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.51", + "quote 1.0.23", "version_check 0.9.4", ] +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +dependencies = [ + "unicode-xid 0.1.0", +] + [[package]] name = "proc-macro2" version = "1.0.51" @@ -2763,6 +3494,24 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4339fc7a1021c9c1621d87f5e3505f2805c8c105420ba2f2a4df86814590c142" +[[package]] +name = "quic-rpc" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d453504fc3e456160ae3b9ebe4d83c1f6477af167aa9b67e2d7bf11a096f179d" +dependencies = [ + "bincode", + "flume", + "futures", + "pin-project", + "quinn", + "serde", + "tokio", + "tokio-serde", + "tokio-util", + "tracing", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -2784,13 +3533,70 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445cbfe2382fa023c4f2f3c7e1c95c03dcc1df2bf23cebcb2b13e1402c4394d1" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "thiserror", + "tokio", + "tracing", + "webpki", +] + +[[package]] +name = "quinn-proto" +version = "0.9.2" +source = "git+https://github.com/quinn-rs/quinn?branch=main#11b34a7b2652010cdbbd08b5dfa407832baff927" +dependencies = [ + "bytes", + "rand 0.8.5", + "ring", + "rustc-hash", + "rustls", + "rustls-native-certs", + "slab", + "thiserror", + "tinyvec", + "tracing", + "webpki", +] + +[[package]] +name = "quinn-udp" +version = "0.3.2" +source = "git+https://github.com/quinn-rs/quinn?branch=main#11b34a7b2652010cdbbd08b5dfa407832baff927" +dependencies = [ + "libc", + "quinn-proto", + "socket2", + "tracing", + "windows-sys 0.45.0", +] + +[[package]] +name = "quote" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1" +dependencies = [ + "proc-macro2 0.4.30", +] + [[package]] name = "quote" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" dependencies = [ - "proc-macro2", + "proc-macro2 1.0.51", ] [[package]] @@ -2916,12 +3722,24 @@ dependencies = [ ] [[package]] -name = "redox_syscall" -version = "0.2.16" +name = "rcgen" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" dependencies = [ - "bitflags", + "pem", + "ring", + "time 0.3.20", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", ] [[package]] @@ -2946,6 +3764,15 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.6.28" @@ -2999,6 +3826,32 @@ dependencies = [ "quick-error 1.2.3", ] +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint", + "hmac", + "zeroize", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "ripemd" version = "0.1.3" @@ -3055,6 +3908,30 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom 7.1.3", +] + [[package]] name = "rustix" version = "0.36.8" @@ -3069,6 +3946,38 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "rustls" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +dependencies = [ + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0167bac7a9f490495f3c33013e7722b53cb087ecbe082fb0c6387c96f634ea50" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +dependencies = [ + "base64 0.21.0", +] + [[package]] name = "rustversion" version = "1.0.11" @@ -3150,6 +4059,30 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "2.8.2" @@ -3173,6 +4106,15 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" +dependencies = [ + "serde", +] + [[package]] name = "serde" version = "1.0.152" @@ -3182,15 +4124,33 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-error" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e988182713aeed6a619a88bca186f6d6407483485ffe44c869ee264f8eabd13f" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_bytes" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416bda436f9aab92e02c8e10d49a15ddd339cea90b6e340fe51ed97abb548294" +dependencies = [ + "serde", +] + [[package]] name = "serde_derive" version = "1.0.152" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -3290,6 +4250,15 @@ dependencies = [ "keccak", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -3346,6 +4315,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spin" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5d6e0250b93c8427a177b849d144a96d5acc57006149479403d7861ab721e34" +dependencies = [ + "lock_api", +] + [[package]] name = "spki" version = "0.6.0" @@ -3356,6 +4334,35 @@ dependencies = [ "der", ] +[[package]] +name = "ssh-encoding" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19cfdc32e0199062113edf41f344fbf784b8205a94600233c84eb838f45191e1" +dependencies = [ + "base64ct", + "pem-rfc7468", + "sha2 0.10.6", +] + +[[package]] +name = "ssh-key" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "288d8f5562af5a3be4bda308dd374b2c807b940ac370b5efa1c99311da91d9a1" +dependencies = [ + "ed25519-dalek", + "p256", + "p384", + "rand_core 0.6.4", + "rsa", + "sec1", + "sha2 0.10.6", + "signature", + "ssh-encoding", + "zeroize", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -3405,17 +4412,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck", - "proc-macro2", - "quote", + "proc-macro2 1.0.51", + "quote 1.0.23", "rustversion", - "syn", + "syn 1.0.109", ] [[package]] name = "subtle" -version = "2.5.0" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "0.15.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5" +dependencies = [ + "proc-macro2 0.4.30", + "quote 0.6.13", + "unicode-xid 0.1.0", +] [[package]] name = "syn" @@ -3423,8 +4441,8 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2", - "quote", + "proc-macro2 1.0.51", + "quote 1.0.23", "unicode-ident", ] @@ -3440,10 +4458,45 @@ version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ - "proc-macro2", - "quote", - "syn", - "unicode-xid", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", + "unicode-xid 0.2.4", +] + +[[package]] +name = "sysinfo" +version = "0.26.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c18a6156d1f27a9592ee18c1a846ca8dd5c258b7179fc193ae87c74ebb666f5" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "winapi", +] + +[[package]] +name = "system-configuration" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d75182f12f490e953596550b65ee31bda7c8e043d9386174b353bda50838c3fd" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -3474,6 +4527,20 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "testdir" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31eb500f7513b559ed7e0652894268dbe8ef27b6241b783ce274f4741eae137" +dependencies = [ + "anyhow", + "backtrace", + "cargo_metadata", + "once_cell", + "sysinfo", + "whoami", +] + [[package]] name = "textwrap" version = "0.16.0" @@ -3500,9 +4567,19 @@ version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", ] [[package]] @@ -3516,6 +4593,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -3577,9 +4681,9 @@ version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -3592,6 +4696,21 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911a61637386b789af998ee23f50aa30d5fd7edcec8d6d3dedae5e5815205466" +dependencies = [ + "bincode", + "bytes", + "educe", + "futures-core", + "futures-sink", + "pin-project", + "serde", +] + [[package]] name = "tokio-stream" version = "0.1.12" @@ -3744,9 +4863,9 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -3756,6 +4875,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -3862,9 +5021,9 @@ dependencies = [ "darling 0.13.4", "ident_case", "proc-macro-error", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -3916,12 +5075,24 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" + [[package]] name = "unicode-xid" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.3.1" @@ -3964,6 +5135,12 @@ dependencies = [ "serde", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -4046,9 +5223,9 @@ dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", "wasm-bindgen-shared", ] @@ -4070,7 +5247,7 @@ version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" dependencies = [ - "quote", + "quote 1.0.23", "wasm-bindgen-macro-support", ] @@ -4080,9 +5257,9 @@ version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4103,12 +5280,32 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "weezl" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" +[[package]] +name = "whoami" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45dbc71f0cdca27dc261a9bd37ddec174e4a0af2b900b890f378460f745426e3" +dependencies = [ + "wasm-bindgen", + "web-sys", +] + [[package]] name = "widestring" version = "0.5.1" @@ -4146,6 +5343,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbedf6db9096bc2364adce0ae0aa636dcd89f3c3f2cd67947062aaf0ca2a10ec" +dependencies = [ + "windows_aarch64_msvc 0.32.0", + "windows_i686_gnu 0.32.0", + "windows_i686_msvc 0.32.0", + "windows_x86_64_gnu 0.32.0", + "windows_x86_64_msvc 0.32.0", +] + [[package]] name = "windows-sys" version = "0.42.0" @@ -4153,12 +5363,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_x86_64_msvc 0.42.1", ] [[package]] @@ -4177,12 +5387,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_x86_64_msvc 0.42.1", ] [[package]] @@ -4191,24 +5401,48 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" + [[package]] name = "windows_aarch64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" + [[package]] name = "windows_i686_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + [[package]] name = "windows_i686_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + [[package]] name = "windows_x86_64_gnu" version = "0.42.1" @@ -4221,6 +5455,12 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" + [[package]] name = "windows_x86_64_msvc" version = "0.42.1" @@ -4256,6 +5496,24 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x509-parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8" +dependencies = [ + "asn1-rs", + "base64 0.13.1", + "data-encoding", + "der-parser", + "lazy_static", + "nom 7.1.3", + "oid-registry", + "rusticata-macros", + "thiserror", + "time 0.3.20", +] + [[package]] name = "xattr" version = "0.2.3" @@ -4265,6 +5523,15 @@ dependencies = [ "libc", ] +[[package]] +name = "yasna" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aed2e7a52e3744ab4d0c05c20aa065258e84c49fd4226f5191b2ed29712710b4" +dependencies = [ + "time 0.3.20", +] + [[package]] name = "yerpc" version = "0.4.3" @@ -4293,11 +5560,11 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6bd53ff9053698697b92c2535bf7ecb983fd5d546d690b7c725e5070d6d9a620" dependencies = [ - "convert_case", + "convert_case 0.5.0", "darling 0.14.3", - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", ] [[package]] @@ -4315,8 +5582,8 @@ version = "1.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" dependencies = [ - "proc-macro2", - "quote", - "syn", + "proc-macro2 1.0.51", + "quote 1.0.23", + "syn 1.0.109", "synstructure", ] diff --git a/Cargo.toml b/Cargo.toml index 3a91592d0d..b6ca6e84a1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,11 @@ lto = true panic = 'abort' opt-level = "z" +[patch.crates-io] +default-net = { git = "https://github.com/dignifiedquire/default-net.git", branch="feat-android" } +quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" } +quinn-proto = { git = "https://github.com/quinn-rs/quinn", branch="main" } + [dependencies] deltachat_derive = { path = "./deltachat_derive" } format-flowed = { path = "./format-flowed" } @@ -48,6 +53,8 @@ futures-lite = "1.12.0" hex = "0.4.0" humansize = "2" image = { version = "0.24.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] } +# iroh = { version = "0.3.0", default-features = false } +iroh = { git = 'https://github.com/n0-computer/iroh', branch = "flub/ticket-multiple-addrs" } kamadak-exif = "0.5" lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" } libc = "0.2" @@ -95,6 +102,7 @@ log = "0.4" pretty_env_logger = "0.4" proptest = { version = "1", default-features = false, features = ["std"] } tempfile = "3" +testdir = "0.7.2" tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] } [workspace] diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 086f2cd985..638ab74b71 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -24,6 +24,7 @@ typedef struct _dc_provider dc_provider_t; typedef struct _dc_event dc_event_t; typedef struct _dc_event_emitter dc_event_emitter_t; typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t; +typedef struct _dc_backup_provider dc_backup_provider_t; // Alias for backwards compatibility, use dc_event_emitter_t instead. typedef struct _dc_event_emitter dc_accounts_event_emitter_t; @@ -2294,6 +2295,7 @@ void dc_stop_ongoing_process (dc_context_t* context); #define DC_QR_FPR_MISMATCH 220 // id=contact #define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint #define DC_QR_ACCOUNT 250 // text1=domain +#define DC_QR_BACKUP 251 #define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern #define DC_QR_ADDR 320 // id=contact #define DC_QR_TEXT 330 // text1=text @@ -2319,7 +2321,7 @@ void dc_stop_ongoing_process (dc_context_t* context); * ask whether to verify the contact; * if so, start the protocol with dc_join_securejoin(). * - * - DC_QR_ASK_VERIFYGROUP withdc_lot_t::text1=Group name: + * - DC_QR_ASK_VERIFYGROUP with dc_lot_t::text1=Group name: * ask whether to join the group; * if so, start the protocol with dc_join_securejoin(). * @@ -2339,6 +2341,10 @@ void dc_stop_ongoing_process (dc_context_t* context); * ask the user if they want to create an account on the given domain, * if so, call dc_set_config_from_qr() and then dc_configure(). * + * - DC_QR_BACKUP: + * ask the user if they want to set up a new device. + * If so, pass the qr-code to dc_receive_backup(). + * * - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain: * ask the user if they want to use the given service for video chats; * if so, call dc_set_config_from_qr(). @@ -2629,6 +2635,123 @@ char* dc_get_last_error (dc_context_t* context); void dc_str_unref (char* str); +/** + * @class dc_backup_provider_t + * + * Set up another device. + */ + +/** + * Creates an object for sending a backup to another device. + * + * Before calling this function IO must be stopped using dc_accounts_stop_io() + * or dc_stop_io() so that no changes to the blobs or database are happening. + * IO should only be restarted once dc_backup_provider_wait() has returned. + * + * The backup is sent to through a peer-to-peer channel which is bootstrapped + * by a QR-code. The backup contains the entire state of the account + * including credentials. This can be used to setup a new device. + * + * This is a blocking call as some preparations are made like e.g. exporting + * the database. Once this function returns, the backup is being offered to + * remote devices. To wait until one device received the backup, use + * dc_backup_provider_wait(). Alternatively abort the operation using + * dc_stop_ongoing_process(). + * + * During execution of the job #DC_EVENT_IMEX_PROGRESS is sent out to indicate + * state and progress. + * + * @memberof dc_backup_provider_t + * @param context The context. + * @return Opaque object for sending the backup. + * On errors, NULL is returned and dc_get_last_error() returns an error that + * should be shown to the user. + */ +dc_backup_provider_t* dc_backup_provider_new (dc_context_t* context); + + +/** + * Returns the QR code text that will offer the backup to other devices. + * + * The QR code contains a ticket which will validate the backup and provide + * authentication for both the provider and the recipient. + * + * The scanning device should call the scanned text to dc_check_qr(). If + * dc_check_qr() returns DC_QR_BACKUP, the backup transfer can be started using + * dc_get_backup(). + * + * @memberof dc_backup_provider_t + * @param backup_provider The backup provider object as created by + * dc_backup_provider_new(). + * @return The text that should be put in the QR code. + * On errors an empty string is returned, NULL is never returned. + * the returned string must be released using dc_str_unref() after usage. + */ +char* dc_backup_provider_get_qr (const dc_backup_provider_t* backup_provider); + + +/** + * Returns the QR code SVG image that will offer the backup to other devices. + * + * This works like dc_backup_provider_qr() but returns the text of a rendered + * SVG image containing the QR code. + * + * @memberof dc_backup_provider_t + * @param backup_provider The backup provider object as created by + * dc_backup_provider_new(). + * @return The QR code rendered as SVG. + * On errors an empty string is returned, NULL is never returned. + * the returned string must be released using dc_str_unref() after usage. + */ +char* dc_backup_provider_get_qr_svg (const dc_backup_provider_t* backup_provider); + +/** + * Waits for the sending to finish. + * + * This is a blocking call and should only be called once. Once this function + * returns IO can be started again using dc_accounts_start_io() or + * dc_start_io(). + * + * @memberof dc_backup_provider_t + * @param backup_provider The backup provider object as created by + * dc_backup_provider_new(). If NULL is given nothing is done. + */ +void dc_backup_provider_wait (dc_backup_provider_t* backup_provider); + +/** + * Frees a dc_backup_provider_t object. + * + * @memberof dc_backup_provider_t + * @param backup_provider The backup provider object as created by + * dc_backup_provider_new(). + */ +void dc_backup_provider_unref (dc_backup_provider_t* backup_provider); + +/** + * Gets a backup offered by a dc_backup_provider_t object on another device. + * + * This function is called on a device that scanned the QR code offered by + * dc_backup_sender_qr() or dc_backup_sender_qr_svg(). Typically this is a + * different device than that which provides the backup. + * + * This call will block while the backup is being transferred and only + * complete on success or failure. Use dc_stop_ongoing_process() to abort it + * early. + * + * During execution of the job #DC_EVENT_IMEX_PROGRESS is sent out to indicate + * state and progress. The process is finished when the event emits either 0 + * or 1000, 0 means it failed and 1000 means it succeeded. These events are + * for showing progress and informational only, success and failure is also + * shown in the return code of this function. + * + * @memberof dc_context_t + * @param context The context. + * @param qr The qr code text, dc_check_qr() must have returned DC_QR_BACKUP + * on this text. + * @return 0=failure, 1=success. + */ +int dc_receive_backup (dc_context_t* context, const char* qr); + /** * @class dc_accounts_t * diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index a3eae996ca..8101eda93f 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -28,9 +28,10 @@ use deltachat::constants::DC_MSG_ID_LAST_SPECIAL; use deltachat::contact::{Contact, ContactId, Origin}; use deltachat::context::Context; use deltachat::ephemeral::Timer as EphemeralTimer; +use deltachat::imex::BackupProvider; use deltachat::key::DcKey; use deltachat::message::MsgId; -use deltachat::qr_code_generator::get_securejoin_qr_svg; +use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg}; use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions}; use deltachat::stock_str::StockMessage; use deltachat::stock_str::StockStrings; @@ -4142,6 +4143,105 @@ pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) { libc::free(s as *mut _) } +pub struct BackupProviderWrapper { + context: *const dc_context_t, + provider: BackupProvider, +} + +pub type dc_backup_provider_t = BackupProviderWrapper; + +#[no_mangle] +pub unsafe extern "C" fn dc_backup_provider_new( + context: *mut dc_context_t, +) -> *mut dc_backup_provider_t { + if context.is_null() { + eprintln!("ignoring careless call to dc_backup_provider_new()"); + return ptr::null_mut(); + } + let ctx = &*context; + block_on(BackupProvider::prepare(ctx)) + .map(|provider| BackupProviderWrapper { + context: ctx, + provider, + }) + .map(|ffi_provider| Box::into_raw(Box::new(ffi_provider))) + .log_err(ctx, "BackupProvider failed") + .unwrap_or(ptr::null_mut()) +} + +#[no_mangle] +pub unsafe extern "C" fn dc_backup_provider_get_qr( + provider: *const dc_backup_provider_t, +) -> *mut libc::c_char { + if provider.is_null() { + eprintln!("ignoring careless call to dc_backup_provider_qr"); + return "".strdup(); + } + let ffi_provider = &*provider; + deltachat::qr::format_backup(&ffi_provider.provider.qr()) + .unwrap_or_default() + .strdup() +} + +#[no_mangle] +pub unsafe extern "C" fn dc_backup_provider_get_qr_svg( + provider: *const dc_backup_provider_t, +) -> *mut libc::c_char { + if provider.is_null() { + eprintln!("ignoring careless call to dc_backup_provider_qr_svg()"); + return "".strdup(); + } + let ffi_provider = &*provider; + let ctx = &*ffi_provider.context; + let provider = &ffi_provider.provider; + block_on(generate_backup_qr(ctx, &provider.qr())) + .unwrap_or_default() + .strdup() +} + +#[no_mangle] +pub unsafe extern "C" fn dc_backup_provider_wait(provider: *mut dc_backup_provider_t) { + if provider.is_null() { + eprintln!("ignoring careless call to dc_backup_provider_wait()"); + return; + } + let ffi_provider = &mut *provider; + let ctx = &*ffi_provider.context; + let provider = &mut ffi_provider.provider; + block_on(provider) + .log_err(ctx, "Failed to join provider") + .ok(); +} + +#[no_mangle] +pub unsafe extern "C" fn dc_backup_provider_unref(provider: *mut dc_backup_provider_t) { + drop(Box::from_raw(provider)); +} + +#[no_mangle] +pub unsafe extern "C" fn dc_receive_backup( + context: *mut dc_context_t, + qr: *const libc::c_char, +) -> libc::c_int { + if context.is_null() { + eprintln!("ignoring careless call to dc_receive_backup()"); + return 0; + } + let ctx = &*context; + let qr_text = to_string_lossy(qr); + let qr = match block_on(qr::check_qr(ctx, &qr_text)).log_err(ctx, "Invalid QR code") { + Ok(qr) => qr, + Err(_) => return 0, + }; + spawn(async move { + imex::get_backup(ctx, qr) + .await + .log_err(ctx, "Get backup failed") + .ok(); + }); + 1 +} + trait ResultExt { /// Like `log_err()`, but: /// - returns the default value instead of an Err value. diff --git a/deltachat-ffi/src/lot.rs b/deltachat-ffi/src/lot.rs index 2e3d5e0c5b..52b5a1e445 100644 --- a/deltachat-ffi/src/lot.rs +++ b/deltachat-ffi/src/lot.rs @@ -14,6 +14,8 @@ use crate::summary::{Summary, SummaryPrefix}; /// eg. by chatlist.get_summary() or dc_msg_get_summary(). /// /// *Lot* is used in the meaning *heap* here. +// The QR code grew too large. So be it. +#[allow(clippy::large_enum_variant)] #[derive(Debug)] pub enum Lot { Summary(Summary), @@ -47,6 +49,7 @@ impl Lot { Qr::FprMismatch { .. } => None, Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint), Qr::Account { domain } => Some(domain), + Qr::Backup { .. } => None, Qr::WebrtcInstance { domain, .. } => Some(domain), Qr::Addr { draft, .. } => draft.as_deref(), Qr::Url { url } => Some(url), @@ -98,6 +101,7 @@ impl Lot { Qr::FprMismatch { .. } => LotState::QrFprMismatch, Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr, Qr::Account { .. } => LotState::QrAccount, + Qr::Backup { .. } => LotState::QrBackup, Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance, Qr::Addr { .. } => LotState::QrAddr, Qr::Url { .. } => LotState::QrUrl, @@ -122,6 +126,7 @@ impl Lot { Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(), Qr::FprWithoutAddr { .. } => Default::default(), Qr::Account { .. } => Default::default(), + Qr::Backup { .. } => Default::default(), Qr::WebrtcInstance { .. } => Default::default(), Qr::Addr { contact_id, .. } => contact_id.to_u32(), Qr::Url { .. } => Default::default(), @@ -170,6 +175,8 @@ pub enum LotState { /// text1=domain QrAccount = 250, + QrBackup = 251, + /// text1=domain, text2=instance pattern QrWebrtcInstance = 260, diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index cc0ad3b01a..2990dd363c 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -22,7 +22,7 @@ use deltachat::{ }, provider::get_provider_info, qr, - qr_code_generator::get_securejoin_qr_svg, + qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg}, reaction::send_reaction, securejoin, stock_str::StockMessage, @@ -1350,6 +1350,69 @@ impl CommandApi { .await } + /// Offers a backup for remote devices to retrieve. + /// + /// Can be cancelled by stopping the ongoing process. Success or failure can be tracked + /// via the `ImexProgress` event which should either reach `1000` for success or `0` for + /// failure. + /// + /// This **stops IO** while it is running. + /// + /// Returns once a remote device has retrieved the backup. + async fn provide_backup(&self, account_id: u32) -> Result<()> { + let ctx = self.get_context(account_id).await?; + ctx.stop_io().await; + let provider = match imex::BackupProvider::prepare(&ctx).await { + Ok(provider) => provider, + Err(err) => { + ctx.start_io().await; + return Err(err); + } + }; + let res = provider.await; + ctx.start_io().await; + res + } + + /// Returns the text of the QR code for the running [`CommandApi::provide_backup`]. + /// + /// This QR code text can be used in [`CommandApi::get_backup`] on a second device to + /// retrieve the backup and setup this second device. + async fn get_backup_qr(&self, account_id: u32) -> Result { + let ctx = self.get_context(account_id).await?; + let qr = ctx + .backup_export_qr() + .ok_or(anyhow!("no backup being exported"))?; + qr::format_backup(&qr) + } + + /// Returns the rendered QR code for the running [`CommandApi::provide_backup`]. + /// + /// This QR code can be used in [`CommandApi::get_backup`] on a second device to + /// retrieve the backup and setup this second device. + /// + /// Returns the QR code rendered as an SVG image. + async fn get_backup_qr_svg(&self, account_id: u32) -> Result { + let ctx = self.get_context(account_id).await?; + let qr = ctx + .backup_export_qr() + .ok_or(anyhow!("no backup being exported"))?; + generate_backup_qr(&ctx, &qr).await + } + + /// Gets a backup from a remote provider. + /// + /// This retrieves the backup from a remote device over the network and imports it into + /// the current device. + /// + /// Can be cancelled by stopping the ongoing process. + async fn get_backup(&self, account_id: u32, qr_text: String) -> Result<()> { + let ctx = self.get_context(account_id).await?; + let qr = qr::check_qr(&ctx, &qr_text).await?; + imex::get_backup(&ctx, qr).await?; + Ok(()) + } + // --------------------------------------------- // connectivity // --------------------------------------------- diff --git a/deltachat-jsonrpc/src/api/types/qr.rs b/deltachat-jsonrpc/src/api/types/qr.rs index 0a38d43cea..607b495ec9 100644 --- a/deltachat-jsonrpc/src/api/types/qr.rs +++ b/deltachat-jsonrpc/src/api/types/qr.rs @@ -32,6 +32,9 @@ pub enum QrObject { Account { domain: String, }, + Backup { + ticket: String, + }, WebrtcInstance { domain: String, instance_pattern: String, @@ -126,6 +129,9 @@ impl From for QrObject { } Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint }, Qr::Account { domain } => QrObject::Account { domain }, + Qr::Backup { ticket } => QrObject::Backup { + ticket: ticket.to_string(), + }, Qr::WebrtcInstance { domain, instance_pattern, diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs index e659ea7e86..9fdc8e91e9 100644 --- a/deltachat-repl/src/cmdline.rs +++ b/deltachat-repl/src/cmdline.rs @@ -336,6 +336,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu has-backup\n\ export-backup\n\ import-backup \n\ + send-backup\n\ + receive-backup \n\ export-keys\n\ import-keys\n\ export-setup\n\ @@ -486,6 +488,17 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu ) .await?; } + "send-backup" => { + let provider = BackupProvider::prepare(&context).await?; + let qr = provider.qr(); + println!("QR code: {}", format_backup(&qr)?); + provider.await?; + } + "receive-backup" => { + ensure!(!arg1.is_empty(), "Argument is missing."); + let qr = check_qr(&context, arg1).await?; + deltachat::imex::get_backup(&context, qr).await?; + } "export-keys" => { let dir = dirs::home_dir().unwrap_or_default(); imex(&context, ImexMode::ExportSelfKeys, dir.as_ref(), None).await?; diff --git a/deltachat-repl/src/main.rs b/deltachat-repl/src/main.rs index 80b1bd8be5..2670b311f1 100644 --- a/deltachat-repl/src/main.rs +++ b/deltachat-repl/src/main.rs @@ -152,13 +152,15 @@ impl Completer for DcHelper { } } -const IMEX_COMMANDS: [&str; 12] = [ +const IMEX_COMMANDS: [&str; 14] = [ "initiate-key-transfer", "get-setupcodebegin", "continue-key-transfer", "has-backup", "export-backup", "import-backup", + "send-backup", + "receive-backup", "export-keys", "import-keys", "export-setup", diff --git a/deny.toml b/deny.toml index 0d1e042594..c620e8db01 100644 --- a/deny.toml +++ b/deny.toml @@ -12,27 +12,42 @@ ignore = [ # becoming empty. Adding versions forces us to revisit this at least # when upgrading. skip = [ - { name = "windows-sys", version = "<0.45" }, - { name = "wasi", version = "<0.11" }, - { name = "version_check", version = "<0.9" }, - { name = "uuid", version = "<1.3" }, - { name = "sha2", version = "<0.10" }, - { name = "rand_core", version = "<0.6" }, - { name = "rand_chacha", version = "<0.3" }, - { name = "rand", version = "<0.8" }, - { name = "nom", version = "<7.1" }, - { name = "idna", version = "<0.3" }, - { name = "humantime", version = "<2.1" }, - { name = "hermit-abi", version = "<0.3" }, + { name = "base64", version = "<0.21" }, + { name = "block-buffer", version = "<0.10" }, + { name = "darling", version = "<0.14" }, + { name = "darling_core", version = "<0.14" }, + { name = "darling_macro", version = "<0.14" }, + { name = "digest", version = "<0.10" }, + { name = "env_logger", version = "<0.10" }, { name = "getrandom", version = "<0.2" }, + { name = "hermit-abi", version = "<0.3" }, + { name = "humantime", version = "<2.1" }, + { name = "idna", version = "<0.3" }, + { name = "nom", version = "<7.1" }, { name = "quick-error", version = "<2.0" }, - { name = "env_logger", version = "<0.10" }, - { name = "digest", version = "<0.10" }, - { name = "darling_macro", version = "<0.14" }, - { name = "darling_core", version = "<0.14" }, - { name = "darling", version = "<0.14" }, - { name = "block-buffer", version = "<0.10" }, - { name = "base64", version = "<0.21" }, + { name = "rand", version = "<0.8" }, + { name = "rand_chacha", version = "<0.3" }, + { name = "rand_core", version = "<0.6" }, + { name = "sha2", version = "<0.10" }, + { name = "time", version = "<0.3" }, + { name = "uuid", version = "<1.3" }, + { name = "version_check", version = "<0.9" }, + { name = "wasi", version = "<0.11" }, + { name = "windows-sys", version = "<0.45" }, + { name = "windows_x86_64_msvc", version = "<0.42" }, + { name = "windows_x86_64_gnu", version = "<0.42" }, + { name = "windows_i686_msvc", version = "<0.42" }, + { name = "windows_i686_gnu", version = "<0.42" }, + { name = "windows_aarch64_msvc", version = "<0.42" }, + { name = "unicode-xid", version = "<0.2.4" }, + { name = "syn", version = "<1.0" }, + { name = "quote", version = "<1.0" }, + { name = "proc-macro2", version = "<1.0" }, + { name = "portable-atomic", version = "<1.0" }, + { name = "spin", version = "<0.9.6" }, + { name = "convert_case", version = "0.4.0" }, + { name = "clap_lex", version = "0.2.4" }, + { name = "clap", version = "3.2.23" }, ] @@ -42,11 +57,21 @@ allow = [ "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", + "BSL-1.0", # Boost Software License 1.0 "CC0-1.0", + "ISC", "MIT", - "BSL-1.0", # Boost Software License 1.0 - "Unicode-DFS-2016", "MPL-2.0", + "OpenSSL", + "Unicode-DFS-2016", + "Zlib", +] + +[[licenses.clarify]] +name = "ring" +expression = "MIT AND ISC AND OpenSSL" +license-files = [ + { path = "LICENSE", hash = 0xbd0eed23 }, ] [sources.allow-org] @@ -54,4 +79,7 @@ allow = [ github = [ "async-email", "deltachat", + "n0-computer", + "quinn-rs", + "dignifiedquire", ] diff --git a/src/blob.rs b/src/blob.rs index ecca8129ca..cdc8ffc943 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -4,13 +4,16 @@ use core::cmp::max; use std::ffi::OsStr; use std::fmt; use std::io::Cursor; +use std::iter::FusedIterator; use std::path::{Path, PathBuf}; use anyhow::{format_err, Context as _, Result}; +use futures::StreamExt; use image::{DynamicImage, ImageFormat}; use num_traits::FromPrimitive; use tokio::io::AsyncWriteExt; use tokio::{fs, io}; +use tokio_stream::wrappers::ReadDirStream; use crate::config::Config; use crate::constants::{ @@ -160,9 +163,9 @@ impl<'a> BlobObject<'a> { pub fn from_path(context: &'a Context, path: &Path) -> Result> { let rel_path = path .strip_prefix(context.get_blobdir()) - .context("wrong blobdir")?; + .with_context(|| format!("wrong blobdir: {}", path.display()))?; if !BlobObject::is_acceptible_blob_name(rel_path) { - return Err(format_err!("wrong name")); + return Err(format_err!("bad blob name: {}", rel_path.display())); } let name = rel_path.to_str().context("wrong name")?; BlobObject::from_name(context, name.to_string()) @@ -468,6 +471,87 @@ impl<'a> fmt::Display for BlobObject<'a> { } } +/// All files in the blobdir. +/// +/// This exists so we can have a [`BlobDirIter`] which needs something to own the data of +/// it's `&Path`. Use [`BlobDirContents::iter`] to create the iterator. +/// +/// Additionally pre-allocating this means we get a length for progress report. +pub(crate) struct BlobDirContents<'a> { + inner: Vec, + context: &'a Context, +} + +impl<'a> BlobDirContents<'a> { + pub(crate) async fn new(context: &'a Context) -> Result> { + let readdir = fs::read_dir(context.get_blobdir()).await?; + let inner = ReadDirStream::new(readdir) + .filter_map(|entry| async move { + match entry { + Ok(entry) => Some(entry), + Err(err) => { + error!(context, "Failed to read blob file: {err}"); + None + } + } + }) + .filter_map(|entry| async move { + match entry.file_type().await.ok()?.is_file() { + true => Some(entry.path()), + false => { + warn!( + context, + "Export: Found blob dir entry {} that is not a file, ignoring", + entry.path().display() + ); + None + } + } + }) + .collect() + .await; + Ok(Self { inner, context }) + } + + pub(crate) fn iter(&self) -> BlobDirIter<'_> { + BlobDirIter::new(self.context, self.inner.iter()) + } + + pub(crate) fn len(&self) -> usize { + self.inner.len() + } +} + +/// A iterator over all the [`BlobObject`]s in the blobdir. +pub(crate) struct BlobDirIter<'a> { + iter: std::slice::Iter<'a, PathBuf>, + context: &'a Context, +} + +impl<'a> BlobDirIter<'a> { + fn new(context: &'a Context, iter: std::slice::Iter<'a, PathBuf>) -> BlobDirIter<'a> { + Self { iter, context } + } +} + +impl<'a> Iterator for BlobDirIter<'a> { + type Item = BlobObject<'a>; + + fn next(&mut self) -> Option { + for path in self.iter.by_ref() { + // In theory this can error but we'd have corrupted filenames in the blobdir, so + // silently skipping them is fine. + match BlobObject::from_path(self.context, path) { + Ok(blob) => return Some(blob), + Err(err) => warn!(self.context, "{err}"), + } + } + None + } +} + +impl FusedIterator for BlobDirIter<'_> {} + fn encode_img(img: &DynamicImage, encoded: &mut Vec) -> anyhow::Result<()> { encoded.clear(); let mut buf = Cursor::new(encoded); diff --git a/src/context.rs b/src/context.rs index 682d39f9c7..60740e9099 100644 --- a/src/context.rs +++ b/src/context.rs @@ -23,6 +23,7 @@ use crate::events::{Event, EventEmitter, EventType, Events}; use crate::key::{DcKey, SignedPublicKey}; use crate::login_param::LoginParam; use crate::message::{self, MessageState, MsgId}; +use crate::qr::Qr; use crate::quota::QuotaInfo; use crate::scheduler::SchedulerState; use crate::sql::Sql; @@ -191,6 +192,10 @@ pub struct InnerContext { pub(crate) blobdir: PathBuf, pub(crate) sql: Sql, pub(crate) smeared_timestamp: SmearedTimestamp, + /// The global "ongoing" process state. + /// + /// This is a global mutex-like state for operations which should be modal in the + /// clients. running_state: RwLock, /// Mutex to avoid generating the key for the user more than once. pub(crate) generating_key_mutex: Mutex<()>, @@ -236,6 +241,14 @@ pub struct InnerContext { /// If debug logging is enabled, this contains all necessary information pub(crate) debug_logging: RwLock>, + + /// QR code for currently running [`BackupProvider`]. + /// + /// This is only available if a backup export is currently running, it will also be + /// holding the ongoing process while running. + /// + /// [`BackupProvider`]: crate::imex::BackupProvider + pub(crate) export_provider: std::sync::Mutex>, } #[derive(Debug)] @@ -380,6 +393,7 @@ impl Context { last_full_folder_scan: Mutex::new(None), last_error: std::sync::RwLock::new("".to_string()), debug_logging: RwLock::new(None), + export_provider: std::sync::Mutex::new(None), }; let ctx = Context { @@ -502,6 +516,13 @@ impl Context { // Ongoing process allocation/free/check + /// Tries to acquire the global UI "ongoing" mutex. + /// + /// This is for modal operations during which no other user actions are allowed. Only + /// one such operation is allowed at any given time. + /// + /// The return value is a cancel token, which will release the ongoing mutex when + /// dropped. pub(crate) async fn alloc_ongoing(&self) -> Result> { let mut s = self.running_state.write().await; ensure!( @@ -547,6 +568,17 @@ impl Context { } } + /// Returns the QR-code of the currently running [`BackupProvider`]. + /// + /// [`BackupProvider`]: crate::imex::BackupProvider + pub fn backup_export_qr(&self) -> Option { + self.export_provider + .lock() + .expect("poisoned lock") + .as_ref() + .cloned() + } + /******************************************************************************* * UI chat/message related API ******************************************************************************/ diff --git a/src/imex.rs b/src/imex.rs index eeffbe2311..bd965fa3dc 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -5,18 +5,19 @@ use std::ffi::OsStr; use std::path::{Path, PathBuf}; use ::pgp::types::KeyTrait; -use anyhow::{bail, ensure, format_err, Context as _, Error, Result}; +use anyhow::{bail, ensure, format_err, Context as _, Result}; use futures::StreamExt; use futures_lite::FutureExt; use rand::{thread_rng, Rng}; use tokio::fs::{self, File}; use tokio_tar::Archive; -use crate::blob::BlobObject; +use crate::blob::{BlobDirContents, BlobObject}; use crate::chat::{self, delete_and_reset_all_device_msgs, ChatId}; use crate::config::Config; use crate::contact::ContactId; use crate::context::Context; +use crate::e2ee; use crate::events::EventType; use crate::key::{self, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey}; use crate::log::LogExt; @@ -30,7 +31,10 @@ use crate::tools::{ create_folder, delete_file, get_filesuffix_lc, open_file_std, read_file, time, write_file, EmailAddress, }; -use crate::{e2ee, tools}; + +mod transfer; + +pub use transfer::{get_backup, BackupProvider}; // Name of the database file in the backup. const DBFILE_BACKUP_NAME: &str = "dc_database_backup.sqlite"; @@ -520,16 +524,9 @@ async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Res let _d1 = DeleteOnDrop(temp_db_path.clone()); let _d2 = DeleteOnDrop(temp_path.clone()); - context - .sql - .set_raw_config_int("backup_time", now as i32) - .await?; - sql::housekeeping(context).await.ok_or_log(context); - - ensure!( - !context.scheduler.is_running().await, - "cannot export backup, IO is running" - ); + export_database(context, &temp_db_path, passphrase) + .await + .context("could not export database")?; info!( context, @@ -538,32 +535,6 @@ async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Res dest_path.display(), ); - let path_str = temp_db_path - .to_str() - .with_context(|| format!("path {temp_db_path:?} is not valid unicode"))?; - - context - .sql - .call_write(|conn| { - if let Err(err) = conn.execute("VACUUM", params![]) { - info!(context, "Vacuum failed, exporting anyway: {:#}.", err); - } - conn.execute( - "ATTACH DATABASE ? AS backup KEY ?", - paramsv![path_str, passphrase], - ) - .context("failed to attach backup database")?; - let res = conn - .query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(())) - .context("failed to export to attached backup database"); - conn.execute("DETACH DATABASE backup", []) - .context("failed to detach backup database")?; - res?; - - Ok::<_, Error>(()) - }) - .await?; - let res = export_backup_inner(context, &temp_db_path, &temp_path).await; match &res { @@ -601,29 +572,15 @@ async fn export_backup_inner( .append_path_with_name(temp_db_path, DBFILE_BACKUP_NAME) .await?; - let read_dir = tools::read_dir(context.get_blobdir()).await?; - let count = read_dir.len(); - let mut written_files = 0; - + let blobdir = BlobDirContents::new(context).await?; let mut last_progress = 0; - for entry in read_dir { - let name = entry.file_name(); - if !entry.file_type().await?.is_file() { - warn!( - context, - "Export: Found dir entry {} that is not a file, ignoring", - name.to_string_lossy() - ); - continue; - } - let mut file = File::open(entry.path()).await?; - let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(name); - builder.append_file(path_in_archive, &mut file).await?; - written_files += 1; - let progress = 1000 * written_files / count; + for (i, blob) in blobdir.iter().enumerate() { + let mut file = File::open(blob.to_abs_path()).await?; + let path_in_archive = PathBuf::from(BLOBS_BACKUP_NAME).join(blob.as_name()); + builder.append_file(path_in_archive, &mut file).await?; + let progress = 1000 * i / blobdir.len(); if progress != last_progress && progress > 10 && progress < 1000 { - // We already emitted ImexProgress(10) above context.emit_event(EventType::ImexProgress(progress)); last_progress = progress; } @@ -785,6 +742,48 @@ where Ok(()) } +/// Exports the database to *dest*, encrypted using *passphrase*. +/// +/// The directory of *dest* must already exist, if *dest* itself exists it will be +/// overwritten. +/// +/// This also verifies that IO is not running during the export. +async fn export_database(context: &Context, dest: &Path, passphrase: String) -> Result<()> { + ensure!( + !context.scheduler.is_running().await, + "cannot export backup, IO is running" + ); + let now = time().try_into().context("32-bit UNIX time overflow")?; + + // TODO: Maybe introduce camino crate for UTF-8 paths where we need them. + let dest = dest + .to_str() + .with_context(|| format!("path {} is not valid unicode", dest.display()))?; + + context.sql.set_raw_config_int("backup_time", now).await?; + sql::housekeeping(context).await.ok_or_log(context); + context + .sql + .call_write(|conn| { + conn.execute("VACUUM;", params![]) + .map_err(|err| warn!(context, "Vacuum failed, exporting anyway {err}")) + .ok(); + conn.execute( + "ATTACH DATABASE ? AS backup KEY ?", + paramsv![dest, passphrase], + ) + .context("failed to attach backup database")?; + let res = conn + .query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(())) + .context("failed to export to attached backup database"); + conn.execute("DETACH DATABASE backup", []) + .context("failed to detach backup database")?; + res?; + Ok(()) + }) + .await +} + #[cfg(test)] mod tests { use std::time::Duration; diff --git a/src/imex/transfer.rs b/src/imex/transfer.rs new file mode 100644 index 0000000000..17f5650cf6 --- /dev/null +++ b/src/imex/transfer.rs @@ -0,0 +1,699 @@ +//! Transfer a backup to an other device. +//! +//! This module provides support for using n0's iroh tool to initiate transfer of a backup +//! to another device using a QR code. +//! +//! Using the iroh terminology there are two parties to this: +//! +//! - The *Provider*, which starts a server and listens for connections. +//! - The *Getter*, which connects to the server and retrieves the data. +//! +//! Iroh is designed around the idea of verifying hashes, the downloads are verified as +//! they are retrieved. The entire transfer is initiated by requesting the data of a single +//! root hash. +//! +//! Both the provider and the getter are authenticated: +//! +//! - The provider is known by its *peer ID*. +//! - The provider needs an *authentication token* from the getter before it accepts a +//! connection. +//! +//! Both these are transferred in the QR code offered to the getter. This ensures that the +//! getter can not connect to an impersonated provider and the provider does not offer the +//! download to an impersonated getter. + +use std::future::Future; +use std::net::Ipv4Addr; +use std::ops::Deref; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::task::Poll; + +use anyhow::{anyhow, bail, ensure, format_err, Context as _, Result}; +use async_channel::Receiver; +use futures_lite::StreamExt; +use iroh::get::{DataStream, Options}; +use iroh::progress::ProgressEmitter; +use iroh::protocol::AuthToken; +use iroh::provider::{DataSource, Event, Provider, Ticket}; +use iroh::Hash; +use tokio::fs::{self, File}; +use tokio::io::{self, AsyncWriteExt, BufWriter}; +use tokio::sync::broadcast::error::RecvError; +use tokio::sync::{broadcast, Mutex}; +use tokio::task::{JoinHandle, JoinSet}; +use tokio_stream::wrappers::ReadDirStream; + +use crate::blob::BlobDirContents; +use crate::chat::delete_and_reset_all_device_msgs; +use crate::context::Context; +use crate::qr::Qr; +use crate::{e2ee, EventType}; + +use super::{export_database, DBFILE_BACKUP_NAME}; + +/// Provide or send a backup of this device. +/// +/// This creates a backup of the current device and starts a service which offers another +/// device to download this backup. +/// +/// This does not make a full backup on disk, only the SQLite database is created on disk, +/// the blobs in the blob directory are not copied. +/// +/// This starts a task which acquires the global "ongoing" mutex. If you need to stop the +/// task use the [`Context::stop_ongoing`] mechanism. +/// +/// The task implements [`Future`] and awaiting it will complete once a transfer has been +/// either completed or aborted. +#[derive(Debug)] +pub struct BackupProvider { + /// The supervisor task, run by [`BackupProvider::watch_provider`]. + handle: JoinHandle>, + /// The ticket to retrieve the backup collection. + ticket: Ticket, +} + +impl BackupProvider { + /// Prepares for sending a backup to a second device. + /// + /// Before calling this function all I/O must be stopped so that no changes to the blobs + /// or database are happening, this is done by calling the [`Accounts::stop_io`] or + /// [`Context::stop_io`] APIs first. + /// + /// This will acquire the global "ongoing process" mutex, which can be used to cancel + /// the process. + /// + /// [`Accounts::stop_io`]: crate::accounts::Accounts::stop_io + pub async fn prepare(context: &Context) -> Result { + e2ee::ensure_secret_key_exists(context) + .await + .context("Private key not available, aborting backup export")?; + + // Acquire global "ongoing" mutex. + let cancel_token = context.alloc_ongoing().await?; + let context_dir = context + .get_blobdir() + .parent() + .ok_or(anyhow!("Context dir not found"))?; + let dbfile = context_dir.join(DBFILE_BACKUP_NAME); + if fs::metadata(&dbfile).await.is_ok() { + fs::remove_file(&dbfile).await?; + warn!(context, "Previous database export deleted"); + } + let dbfile = TempPathGuard::new(dbfile); + let res = tokio::select! { + biased; + res = Self::prepare_inner(context, &dbfile) => { + match res { + Ok(slf) => Ok(slf), + Err(err) => { + error!(context, "Failed to set up second device setup: {:#}", err); + Err(err) + }, + } + }, + _ = cancel_token.recv() => Err(format_err!("cancelled")), + }; + let (provider, ticket) = match res { + Ok((provider, ticket)) => (provider, ticket), + Err(err) => { + context.free_ongoing().await; + return Err(err); + } + }; + let handle = tokio::spawn(Self::watch_provider( + context.clone(), + provider, + cancel_token, + dbfile, + )); + let slf = Self { handle, ticket }; + let qr = slf.qr(); + *context.export_provider.lock().expect("poisoned lock") = Some(qr); + Ok(slf) + } + + /// Creates the provider task. + /// + /// Having this as a function makes it easier to cancel it when needed. + async fn prepare_inner(context: &Context, dbfile: &Path) -> Result<(Provider, Ticket)> { + // Generate the token up front: we also use it to encrypt the database. + let token = AuthToken::generate(); + context.emit_event(SendProgress::Started.into()); + export_database(context, dbfile, token.to_string()) + .await + .context("Database export failed")?; + context.emit_event(SendProgress::DatabaseExported.into()); + + // Now we can be sure IO is not running. + let mut files = vec![DataSource::with_name( + dbfile.to_owned(), + format!("db/{DBFILE_BACKUP_NAME}"), + )]; + let blobdir = BlobDirContents::new(context).await?; + for blob in blobdir.iter() { + let path = blob.to_abs_path(); + let name = format!("blob/{}", blob.as_file_name()); + files.push(DataSource::with_name(path, name)); + } + + // Start listening. + let (db, hash) = iroh::provider::create_collection(files).await?; + context.emit_event(SendProgress::CollectionCreated.into()); + let provider = Provider::builder(db) + .bind_addr((Ipv4Addr::UNSPECIFIED, 0).into()) + .auth_token(token) + .spawn()?; + context.emit_event(SendProgress::ProviderListening.into()); + info!(context, "Waiting for remote to connect"); + let ticket = provider.ticket(hash); + Ok((provider, ticket)) + } + + /// Supervises the iroh [`Provider`], terminating it when needed. + /// + /// This will watch the provider and terminate it when: + /// + /// - A transfer is completed, successful or unsuccessful. + /// - An event could not be observed to protect against not knowing of a completed event. + /// - The ongoing process is cancelled. + /// + /// The *cancel_token* is the handle for the ongoing process mutex, when this completes + /// we must cancel this operation. + async fn watch_provider( + context: Context, + mut provider: Provider, + cancel_token: Receiver<()>, + _dbfile: TempPathGuard, + ) -> Result<()> { + // _dbfile exists so we can clean up the file once it is no longer needed + let mut events = provider.subscribe(); + let mut total_size = 0; + let mut current_size = 0; + let res = loop { + tokio::select! { + biased; + res = &mut provider => { + break res.context("BackupProvider failed"); + }, + maybe_event = events.recv() => { + match maybe_event { + Ok(event) => { + match event { + Event::ClientConnected { ..} => { + context.emit_event(SendProgress::ClientConnected.into()); + } + Event::RequestReceived { .. } => { + } + Event::TransferCollectionStarted { total_blobs_size, .. } => { + total_size = total_blobs_size; + context.emit_event(SendProgress::TransferInProgress { + current_size, + total_size, + }.into()); + } + Event::TransferBlobCompleted { size, .. } => { + current_size += size; + context.emit_event(SendProgress::TransferInProgress { + current_size, + total_size, + }.into()); + } + Event::TransferCollectionCompleted { .. } => { + context.emit_event(SendProgress::TransferInProgress { + current_size: total_size, + total_size + }.into()); + provider.shutdown(); + } + Event::TransferAborted { .. } => { + provider.shutdown(); + break Err(anyhow!("BackupProvider transfer aborted")); + } + } + } + Err(broadcast::error::RecvError::Closed) => { + // We should never see this, provider.join() should complete + // first. + } + Err(broadcast::error::RecvError::Lagged(_)) => { + // We really shouldn't be lagging, if we did we may have missed + // a completion event. + provider.shutdown(); + break Err(anyhow!("Missed events from BackupProvider")); + } + } + }, + _ = cancel_token.recv() => { + provider.shutdown(); + break Err(anyhow!("BackupSender cancelled")); + }, + } + }; + context + .export_provider + .lock() + .expect("poisoned lock") + .take(); + match &res { + Ok(_) => context.emit_event(SendProgress::Completed.into()), + Err(err) => { + error!(context, "Backup transfer failure: {err:#}"); + context.emit_event(SendProgress::Failed.into()) + } + } + context.free_ongoing().await; + res + } + + /// Returns a QR code that allows fetching this backup. + /// + /// This QR code can be passed to [`get_backup`] on a (different) device. + pub fn qr(&self) -> Qr { + Qr::Backup { + ticket: self.ticket.clone(), + } + } +} + +impl Future for BackupProvider { + type Output = Result<()>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut std::task::Context<'_>) -> Poll { + Pin::new(&mut self.handle).poll(cx)? + } +} + +/// A guard which will remove the path when dropped. +/// +/// It implements [`Deref`] it it can be used as a `&Path`. +#[derive(Debug)] +struct TempPathGuard { + path: PathBuf, +} + +impl TempPathGuard { + fn new(path: PathBuf) -> Self { + Self { path } + } +} + +impl Drop for TempPathGuard { + fn drop(&mut self) { + let path = self.path.clone(); + tokio::spawn(async move { + fs::remove_file(&path).await.ok(); + }); + } +} + +impl Deref for TempPathGuard { + type Target = Path; + + fn deref(&self) -> &Self::Target { + &self.path + } +} + +/// Create [`EventType::ImexProgress`] events using readable names. +/// +/// Plus you get warnings if you don't use all variants. +#[derive(Debug)] +enum SendProgress { + Failed, + Started, + DatabaseExported, + CollectionCreated, + ProviderListening, + ClientConnected, + TransferInProgress { current_size: u64, total_size: u64 }, + Completed, +} + +impl From for EventType { + fn from(source: SendProgress) -> Self { + use SendProgress::*; + let num: u16 = match source { + Failed => 0, + Started => 100, + DatabaseExported => 300, + CollectionCreated => 350, + ProviderListening => 400, + ClientConnected => 450, + TransferInProgress { + current_size, + total_size, + } => { + // the range is 450..=950 + 450 + ((current_size as f64 / total_size as f64) * 500.).floor() as u16 + } + Completed => 1000, + }; + Self::ImexProgress(num.into()) + } +} + +/// Contacts a backup provider and receives the backup from it. +/// +/// This uses a QR code to contact another instance of deltachat which is providing a backup +/// using the [`BackupProvider`]. Once connected it will authenticate using the secrets in +/// the QR code and retrieve the backup. +/// +/// This is a long running operation which will only when completed. +/// +/// Using [`Qr`] as argument is a bit odd as it only accepts one specific variant of it. It +/// does avoid having [`iroh::provider::Ticket`] in the primary API however, without +/// having to revert to untyped bytes. +pub async fn get_backup(context: &Context, qr: Qr) -> Result<()> { + ensure!( + matches!(qr, Qr::Backup { .. }), + "QR code for backup must be of type DCBACKUP" + ); + ensure!( + !context.is_configured().await?, + "Cannot import backups to accounts in use." + ); + ensure!( + !context.scheduler.is_running().await, + "cannot import backup, IO is running" + ); + + // Acquire global "ongoing" mutex. + let cancel_token = context.alloc_ongoing().await?; + tokio::select! { + biased; + res = get_backup_inner(context, qr) => { + context.free_ongoing().await; + res + } + _ = cancel_token.recv() => Err(format_err!("cancelled")), + } +} + +async fn get_backup_inner(context: &Context, qr: Qr) -> Result<()> { + let ticket = match qr { + Qr::Backup { ticket } => ticket, + _ => bail!("QR code for backup must be of type DCBACKUP"), + }; + if ticket.addrs.is_empty() { + bail!("ticket is missing addresses to dial"); + } + for addr in &ticket.addrs { + let opts = Options { + addr: *addr, + peer_id: Some(ticket.peer), + keylog: false, + }; + info!(context, "attempting to contact {}", addr); + match transfer_from_provider(context, &ticket, opts).await { + Ok(_) => { + delete_and_reset_all_device_msgs(context).await?; + context.emit_event(ReceiveProgress::Completed.into()); + return Ok(()); + } + Err(TransferError::ConnectionError(err)) => { + warn!(context, "Connection error: {err:#}."); + continue; + } + Err(TransferError::Other(err)) => { + // Clean up any blobs we already wrote. + let readdir = fs::read_dir(context.get_blobdir()).await?; + let mut readdir = ReadDirStream::new(readdir); + while let Some(dirent) = readdir.next().await { + if let Ok(dirent) = dirent { + fs::remove_file(dirent.path()).await.ok(); + } + } + context.emit_event(ReceiveProgress::Failed.into()); + return Err(err); + } + } + } + Err(anyhow!("failed to contact provider")) +} + +/// Error during a single transfer attempt. +/// +/// Mostly exists to distinguish between `ConnectionError` and any other errors. +#[derive(Debug, thiserror::Error)] +enum TransferError { + #[error("connection error")] + ConnectionError(#[source] anyhow::Error), + #[error("other")] + Other(#[source] anyhow::Error), +} + +async fn transfer_from_provider( + context: &Context, + ticket: &Ticket, + opts: Options, +) -> Result<(), TransferError> { + let progress = ProgressEmitter::new(0, ReceiveProgress::max_blob_progress()); + spawn_progress_proxy(context.clone(), progress.subscribe()); + let mut connected = false; + let on_connected = || { + context.emit_event(ReceiveProgress::Connected.into()); + connected = true; + async { Ok(()) } + }; + let jobs = Mutex::new(JoinSet::default()); + let on_blob = + |hash, reader, name| on_blob(context, &progress, &jobs, ticket, hash, reader, name); + let res = iroh::get::run( + ticket.hash, + ticket.token, + opts, + on_connected, + |collection| { + context.emit_event(ReceiveProgress::CollectionRecieved.into()); + progress.set_total(collection.total_blobs_size()); + async { Ok(()) } + }, + on_blob, + ) + .await; + + let mut jobs = jobs.lock().await; + while let Some(job) = jobs.join_next().await { + job.context("job failed").map_err(TransferError::Other)?; + } + + drop(progress); + match res { + Ok(stats) => { + info!( + context, + "Backup transfer finished, transfer rate is {} Mbps.", + stats.mbits() + ); + Ok(()) + } + Err(err) => match connected { + true => Err(TransferError::Other(err)), + false => Err(TransferError::ConnectionError(err)), + }, + } +} + +/// Get callback when a blob is received from the provider. +/// +/// This writes the blobs to the blobdir. If the blob is the database it will import it to +/// the database of the current [`Context`]. +async fn on_blob( + context: &Context, + progress: &ProgressEmitter, + jobs: &Mutex>, + ticket: &Ticket, + _hash: Hash, + mut reader: DataStream, + name: String, +) -> Result { + ensure!(!name.is_empty(), "Received a nameless blob"); + let path = if name.starts_with("db/") { + let context_dir = context + .get_blobdir() + .parent() + .ok_or(anyhow!("Context dir not found"))?; + let dbfile = context_dir.join(DBFILE_BACKUP_NAME); + if fs::metadata(&dbfile).await.is_ok() { + fs::remove_file(&dbfile).await?; + warn!(context, "Previous database export deleted"); + } + dbfile + } else { + ensure!(name.starts_with("blob/"), "malformatted blob name"); + let blobname = name.rsplit('/').next().context("malformatted blob name")?; + context.get_blobdir().join(blobname) + }; + + let mut wrapped_reader = progress.wrap_async_read(&mut reader); + let file = File::create(&path).await?; + let mut file = BufWriter::with_capacity(128 * 1024, file); + io::copy(&mut wrapped_reader, &mut file).await?; + file.flush().await?; + + if name.starts_with("db/") { + let context = context.clone(); + let token = ticket.token.to_string(); + jobs.lock().await.spawn(async move { + if let Err(err) = context.sql.import(&path, token).await { + error!(context, "cannot import database: {:#?}", err); + } + if let Err(err) = fs::remove_file(&path).await { + error!( + context, + "failed to delete database import file '{}': {:#?}", + path.display(), + err, + ); + } + }); + } + Ok(reader) +} + +/// Spawns a task proxying progress events. +/// +/// This spawns a tokio task which receives events from the [`ProgressEmitter`] and sends +/// them to the context. The task finishes when the emitter is dropped. +/// +/// This could be done directly in the emitter by making it less generic. +fn spawn_progress_proxy(context: Context, mut rx: broadcast::Receiver) { + tokio::spawn(async move { + loop { + match rx.recv().await { + Ok(step) => context.emit_event(ReceiveProgress::BlobProgress(step).into()), + Err(RecvError::Closed) => break, + Err(RecvError::Lagged(_)) => continue, + } + } + }); +} + +/// Create [`EventType::ImexProgress`] events using readable names. +/// +/// Plus you get warnings if you don't use all variants. +#[derive(Debug)] +enum ReceiveProgress { + Connected, + CollectionRecieved, + /// A value between 0 and 85 interpreted as a percentage. + /// + /// Other values are already used by the other variants of this enum. + BlobProgress(u16), + Completed, + Failed, +} + +impl ReceiveProgress { + /// The maximum value for [`ReceiveProgress::BlobProgress`]. + /// + /// This only exists to keep this magic value local in this type. + fn max_blob_progress() -> u16 { + 85 + } +} + +impl From for EventType { + fn from(source: ReceiveProgress) -> Self { + let val = match source { + ReceiveProgress::Connected => 50, + ReceiveProgress::CollectionRecieved => 100, + ReceiveProgress::BlobProgress(val) => 100 + 10 * val, + ReceiveProgress::Completed => 1000, + ReceiveProgress::Failed => 0, + }; + EventType::ImexProgress(val.into()) + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use crate::chat::{get_chat_msgs, send_msg, ChatItem}; + use crate::message::{Message, Viewtype}; + use crate::test_utils::TestContextManager; + + use super::*; + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_send_receive() { + let mut tcm = TestContextManager::new(); + + // Create first device. + let ctx0 = tcm.alice().await; + + // Write a message in the self chat + let self_chat = ctx0.get_self_chat().await; + let mut msg = Message::new(Viewtype::Text); + msg.set_text(Some("hi there".to_string())); + send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap(); + + // Send an attachment in the self chat + let file = ctx0.get_blobdir().join("hello.txt"); + fs::write(&file, "i am attachment").await.unwrap(); + let mut msg = Message::new(Viewtype::File); + msg.set_file(file.to_str().unwrap(), Some("text/plain")); + send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap(); + + // Prepare to transfer backup. + let provider = BackupProvider::prepare(&ctx0).await.unwrap(); + + // Set up second device. + let ctx1 = tcm.unconfigured().await; + get_backup(&ctx1, provider.qr()).await.unwrap(); + + // Make sure the provider finishes without an error. + tokio::time::timeout(Duration::from_secs(30), provider) + .await + .expect("timed out") + .expect("error in provider"); + + // Check that we have the self message. + let self_chat = ctx1.get_self_chat().await; + let msgs = get_chat_msgs(&ctx1, self_chat.id).await.unwrap(); + assert_eq!(msgs.len(), 2); + let msgid = match msgs.get(0).unwrap() { + ChatItem::Message { msg_id } => msg_id, + _ => panic!("wrong chat item"), + }; + let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap(); + let text = msg.get_text().unwrap(); + assert_eq!(text, "hi there"); + let msgid = match msgs.get(1).unwrap() { + ChatItem::Message { msg_id } => msg_id, + _ => panic!("wrong chat item"), + }; + let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap(); + let path = msg.get_file(&ctx1).unwrap(); + let text = fs::read_to_string(&path).await.unwrap(); + assert_eq!(text, "i am attachment"); + + // Check that both received the ImexProgress events. + ctx0.evtracker + .get_matching(|ev| matches!(ev, EventType::ImexProgress(1000))) + .await; + ctx1.evtracker + .get_matching(|ev| matches!(ev, EventType::ImexProgress(1000))) + .await; + } + + #[test] + fn test_send_progress() { + let cases = [ + ((0, 100), 450), + ((10, 100), 500), + ((50, 100), 700), + ((100, 100), 950), + ]; + + for ((current_size, total_size), progress) in cases { + let out = EventType::from(SendProgress::TransferInProgress { + current_size, + total_size, + }); + assert_eq!(out, EventType::ImexProgress(progress)); + } + } +} diff --git a/src/qr.rs b/src/qr.rs index b9ffbbad52..920776b5eb 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -34,6 +34,7 @@ const VCARD_SCHEME: &str = "BEGIN:VCARD"; const SMTP_SCHEME: &str = "SMTP:"; const HTTP_SCHEME: &str = "http://"; const HTTPS_SCHEME: &str = "https://"; +pub(crate) const DCBACKUP_SCHEME: &str = "DCBACKUP:"; /// Scanned QR code. #[derive(Debug, Clone, PartialEq, Eq)] @@ -102,6 +103,20 @@ pub enum Qr { domain: String, }, + /// Provides a backup that can be retrieve. + /// + /// This contains all the data needed to connect to a device and download a backup from + /// it to configure the receiving device with the same account. + Backup { + /// Printable version of the provider information. + /// + /// This is the printable version of a `sendme` ticket, which contains all the + /// information to connect to and authenticate a backup provider. + /// + /// The format is somewhat opaque, but `sendme` can deserialise this. + ticket: iroh::provider::Ticket, + }, + /// Ask the user if they want to use the given service for video chats. WebrtcInstance { /// Server domain name. @@ -244,6 +259,8 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result { dclogin_scheme::decode_login(qr)? } else if starts_with_ignore_case(qr, DCWEBRTC_SCHEME) { decode_webrtc_instance(context, qr)? + } else if starts_with_ignore_case(qr, DCBACKUP_SCHEME) { + decode_backup(qr)? } else if qr.starts_with(MAILTO_SCHEME) { decode_mailto(context, qr).await? } else if qr.starts_with(SMTP_SCHEME) { @@ -264,6 +281,19 @@ pub async fn check_qr(context: &Context, qr: &str) -> Result { Ok(qrcode) } +/// Formats the text of the [`Qr::Backup`] variant. +/// +/// This is the inverse of [`check_qr`] for that variant only. +/// +/// TODO: Refactor this so all variants have a correct [`Display`] and transform `check_qr` +/// into `FromStr`. +pub fn format_backup(qr: &Qr) -> Result { + match qr { + Qr::Backup { ref ticket } => Ok(format!("{DCBACKUP_SCHEME}{ticket}")), + _ => Err(anyhow!("Not a backup QR code")), + } +} + /// scheme: `OPENPGP4FPR:FINGERPRINT#a=ADDR&n=NAME&i=INVITENUMBER&s=AUTH` /// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR&g=GROUPNAME&x=GROUPID&i=INVITENUMBER&s=AUTH` /// or: `OPENPGP4FPR:FINGERPRINT#a=ADDR` @@ -471,6 +501,18 @@ fn decode_webrtc_instance(_context: &Context, qr: &str) -> Result { } } +/// Decodes a [`DCBACKUP_SCHEME`] QR code. +/// +/// The format of this scheme is `DCBACKUP:`. The encoding is the +/// [`iroh::provider::Ticket`]'s `Display` impl. +fn decode_backup(qr: &str) -> Result { + let payload = qr + .strip_prefix(DCBACKUP_SCHEME) + .ok_or(anyhow!("invalid DCBACKUP scheme"))?; + let ticket: iroh::provider::Ticket = payload.parse().context("invalid DCBACKUP payload")?; + Ok(Qr::Backup { ticket }) +} + #[derive(Debug, Deserialize)] struct CreateAccountSuccessResponse { /// Email address. diff --git a/src/qr_code_generator.rs b/src/qr_code_generator.rs index 63f0bc17f1..bfd2b6a39f 100644 --- a/src/qr_code_generator.rs +++ b/src/qr_code_generator.rs @@ -11,7 +11,9 @@ use crate::{ config::Config, contact::{Contact, ContactId}, context::Context, - securejoin, stock_str, + qr::{self, Qr}, + securejoin, + stock_str::{self, backup_transfer_qr}, }; /// Returns SVG of the QR code to join the group or verify contact. @@ -47,6 +49,34 @@ async fn generate_join_group_qr_code(context: &Context, chat_id: ChatId) -> Resu } async fn generate_verification_qr(context: &Context) -> Result { + let (avatar, displayname, addr, color) = self_info(context).await?; + + inner_generate_secure_join_qr_code( + &stock_str::setup_contact_qr_description(context, &displayname, &addr).await, + &securejoin::get_securejoin_qr(context, None).await?, + &color, + avatar, + displayname.chars().next().unwrap_or('#'), + ) +} + +/// Renders a [`Qr::Backup`] QR code as an SVG image. +pub async fn generate_backup_qr(context: &Context, qr: &Qr) -> Result { + let content = qr::format_backup(qr)?; + let (avatar, displayname, _addr, color) = self_info(context).await?; + let description = backup_transfer_qr(context).await?; + + inner_generate_secure_join_qr_code( + &description, + &content, + &color, + avatar, + displayname.chars().next().unwrap_or('#'), + ) +} + +/// Returns `(avatar, displayname, addr, color) of the configured account. +async fn self_info(context: &Context) -> Result<(Option>, String, String, String)> { let contact = Contact::get_by_id(context, ContactId::SELF).await?; let avatar = match contact.get_profile_image(context).await? { @@ -59,16 +89,11 @@ async fn generate_verification_qr(context: &Context) -> Result { let displayname = match context.get_config(Config::Displayname).await? { Some(name) => name, - None => contact.get_addr().to_owned(), + None => contact.get_addr().to_string(), }; - - inner_generate_secure_join_qr_code( - &stock_str::setup_contact_qr_description(context, &displayname, contact.get_addr()).await, - &securejoin::get_securejoin_qr(context, None).await?, - &color_int_to_hex_string(contact.get_color()), - avatar, - displayname.chars().next().unwrap_or('#'), - ) + let addr = contact.get_addr().to_string(); + let color = color_int_to_hex_string(contact.get_color()); + Ok((avatar, displayname, addr, color)) } fn inner_generate_secure_join_qr_code( @@ -272,6 +297,12 @@ fn inner_generate_secure_join_qr_code( #[cfg(test)] mod tests { + use testdir::testdir; + + use crate::imex::BackupProvider; + use crate::qr::format_backup; + use crate::test_utils::TestContextManager; + use super::*; #[tokio::test(flavor = "multi_thread", worker_threads = 2)] @@ -286,4 +317,20 @@ mod tests { .unwrap(); assert!(svg.contains("descr123 " < > &")) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_generate_backup_qr() { + let dir = testdir!(); + let mut tcm = TestContextManager::new(); + let ctx = tcm.alice().await; + let provider = BackupProvider::prepare(&ctx).await.unwrap(); + let qr = provider.qr(); + + println!("{}", format_backup(&qr).unwrap()); + let rendered = generate_backup_qr(&ctx, &qr).await.unwrap(); + tokio::fs::write(dir.join("qr.svg"), &rendered) + .await + .unwrap(); + assert_eq!(rendered.get(..4), Some(" String { - let name = &if display_name == addr { + let name = if display_name == addr { addr.to_owned() } else { format!("{display_name} ({addr})") }; translated(context, StockMessage::SetupContactQRDescription) .await - .replace1(name) + .replace1(&name) } /// Stock string: `Scan to join %1$s`. @@ -1240,6 +1243,24 @@ pub(crate) async fn aeap_explanation_and_link( .replace2(new_addr) } +/// Text to put in the [`Qr::Backup`] rendered SVG image. +/// +/// The default is "Scan to set up second device for ". The +/// account name and address are looked up from the context. +/// +/// [`Qr::Backup`]: crate::qr::Qr::Backup +pub(crate) async fn backup_transfer_qr(context: &Context) -> Result { + let contact = Contact::get_by_id(context, ContactId::SELF).await?; + let addr = contact.get_addr(); + let full_name = match context.get_config(Config::Displayname).await? { + Some(name) if name != addr => format!("{name} ({addr})"), + _ => addr.to_string(), + }; + Ok(translated(context, StockMessage::BackupTransferQr) + .await + .replace1(&full_name)) +} + impl Context { /// Set the stock string for the [StockMessage]. /// diff --git a/src/test_utils.rs b/src/test_utils.rs index 5006f31c68..07fbef649c 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -40,6 +40,11 @@ pub const AVATAR_900x900_BYTES: &[u8] = include_bytes!("../test-data/image/avata static CONTEXT_NAMES: Lazy>> = Lazy::new(|| std::sync::RwLock::new(BTreeMap::new())); +/// Manage multiple [`TestContext`]s in one place. +/// +/// The main advantage is that the log records of the contexts will appear in the order they +/// occurred rather than grouped by context like would happen when you use separate +/// [`TestContext`]s without managing your own [`LogSink`]. pub struct TestContextManager { log_tx: Sender, _log_sink: LogSink,