diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index caf4d0d3c..1bb6b4af7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,6 +98,8 @@ jobs: uses: swatinem/rust-cache@v2 - name: Tests + env: + RUST_BACKTRACE: 1 run: cargo test --workspace - name: Test cargo vendor diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5bb9ea6f6..0285b2697 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -76,6 +76,29 @@ If you have multiple changes in one PR, create multiple conventional commits, an [Conventional Commits]: https://www.conventionalcommits.org/ [git-cliff]: https://git-cliff.org/ +### Errors + +Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors. +When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html), +capitalize it but do not add a full stop as the contexts will be separated by `:`. +For example: +``` +.with_context(|| format!("Unable to trash message {msg_id}")) +``` + +### Logging + +For logging, use `info!`, `warn!` and `error!` macros. +Log messages should be capitalized and have a full stop in the end. For example: +``` +info!(context, "Ignoring addition of {added_addr:?} to {chat_id}."); +``` + +Format anyhow errors with `{:#}` to print all the contexts like this: +``` +error!(context, "Failed to set selfavatar timestamp: {err:#}."); +``` + ### Reviewing Once a PR has an approval and passes CI, it can be merged. diff --git a/Cargo.lock b/Cargo.lock index 08a25afa4..b7493ce1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,9 +17,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -79,9 +79,9 @@ dependencies = [ [[package]] name = "allocator-api2" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56fc6cf8dc8c4158eed8649f9b8b0ea1518eb62b544fe9490d66fa0b349eafe9" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" [[package]] name = "android-tzdata" @@ -121,9 +121,9 @@ checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" [[package]] name = "anyhow" -version = "1.0.71" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c7d0618f0e0b7e8ff11427422b64564d5fb0be1940354bfe2e0529b18a9d9b8" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" dependencies = [ "backtrace", ] @@ -159,7 +159,7 @@ dependencies = [ "num-traits", "rusticata-macros", "thiserror", - "time 0.3.22", + "time 0.3.24", ] [[package]] @@ -187,9 +187,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf46fee83e5ccffc220104713af3292ff9bc7c64c7de289f66dae8e38d826833" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", "event-listener", @@ -216,7 +216,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "936c1b580be4373b48c9c687e0c79285441664398354df28d0860087cac0c069" dependencies = [ "async-channel", - "base64 0.21.2", + "base64 0.21.3", "bytes", "chrono", "futures", @@ -271,13 +271,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.70" +version = "0.1.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79fa67157abdfd688a259b6648808757db9347af834624f27ec646da976aee5d" +checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -308,7 +308,7 @@ checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", "axum-core", - "base64 0.21.2", + "base64 0.21.3", "bitflags 1.3.2", "bytes", "futures-util", @@ -354,9 +354,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -399,9 +399,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" [[package]] name = "base64ct" @@ -438,9 +438,9 @@ checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" [[package]] name = "blake3" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "729b71f35bd3fa1a4c86b85d32c8b9069ea7fe14f7a53cfabb65f62d4265b888" +checksum = "199c42ab6972d92c9f8995f086273d25c42fc0f7b2a1fcefba465c1352d25ba5" dependencies = [ "arrayref", "arrayvec", @@ -489,9 +489,9 @@ dependencies = [ [[package]] name = "brotli" -version = "3.3.4" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -500,9 +500,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.3.4" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" +checksum = "da74e2b81409b1b743f8f0c62cc6254afefb8b8e50bbfe3735550f7aeefa3448" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -510,9 +510,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a246e68bb43f6cd9db24bea052a53e40405417c5fb372e3d1a8a7f770a564ef5" +checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" dependencies = [ "memchr", "serde", @@ -564,18 +564,18 @@ dependencies = [ [[package]] name = "camino" -version = "1.1.4" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c530edf18f37068ac2d977409ed5cd50d53d73bc653c7647b48eb78976ac9ae2" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" dependencies = [ "serde", ] [[package]] name = "cargo-platform" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbdb825da8a5df079a43676dbe042702f1707b1109f713a01420fbb4cc71fa27" +checksum = "2cfa25e60aea747ec7e1124f238816749faa93759c6ff5b31f1ccdda137f4479" dependencies = [ "serde", ] @@ -610,9 +610,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.79" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "51f1226cd9da55587234753d1245dd5b132343ea240f26b6a9003d68706141ba" +dependencies = [ + "libc", +] [[package]] name = "cfb-mode" @@ -641,9 +644,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "95ed24df0632f708f5f6d8082675bef2596f7084dee3dd55f632290bf35bfe0f" dependencies = [ "android-tzdata", "iana-time-zone", @@ -651,7 +654,7 @@ dependencies = [ "num-traits", "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets 0.48.1", ] [[package]] @@ -693,18 +696,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.10" +version = "4.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384e169cc618c613d5e3ca6404dda77a8685a63e08660dcc64abaf7da7cb0c7a" +checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.3.10" +version = "4.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef137bbe35aab78bdb468ccfba75a5f4d8321ae011d34063770780545176af2d" +checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" dependencies = [ "anstyle", "clap_lex", @@ -750,9 +753,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6340df57935414636969091153f35f68d9f00bbc8fb4a9c6054706c213e6c6bc" +checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747" [[package]] name = "const_format" @@ -776,9 +779,9 @@ dependencies = [ [[package]] name = "constant_time_eq" -version = "0.2.6" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" +checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2" [[package]] name = "convert_case" @@ -810,9 +813,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" +checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1" dependencies = [ "libc", ] @@ -947,16 +950,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "ctor" -version = "0.1.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" -dependencies = [ - "quote", - "syn 1.0.109", -] - [[package]] name = "curve25519-dalek" version = "3.2.0" @@ -995,7 +988,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -1103,7 +1096,7 @@ dependencies = [ "async-smtp", "async_zip", "backtrace", - "base64 0.21.2", + "base64 0.21.3", "brotli", "chrono", "criterion", @@ -1112,6 +1105,7 @@ dependencies = [ "encoded-words", "escaper", "fast-socks5", + "fd-lock", "format-flowed", "futures", "futures-lite", @@ -1126,7 +1120,7 @@ dependencies = [ "log", "mailparse", "mime", - "num-derive 0.4.0", + "num-derive", "num-traits", "num_cpus", "once_cell", @@ -1148,7 +1142,7 @@ dependencies = [ "serde", "serde_json", "sha-1", - "sha2 0.10.7", + "sha2 0.10.8", "smallvec", "strum", "strum_macros", @@ -1174,7 +1168,7 @@ dependencies = [ "anyhow", "async-channel", "axum", - "base64 0.21.2", + "base64 0.21.3", "deltachat", "env_logger", "futures", @@ -1228,7 +1222,7 @@ name = "deltachat_derive" version = "2.0.0" dependencies = [ "quote", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -1298,6 +1292,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "deranged" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8810e7e2cf385b1e9b50d68264908ec367ba642c96d02edfe61c39e88e2a3c01" + [[package]] name = "derive_builder" version = "0.12.0" @@ -1428,7 +1428,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -1456,9 +1456,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" +checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" [[package]] name = "ecdsa" @@ -1474,9 +1474,9 @@ dependencies = [ [[package]] name = "ecdsa" -version = "0.16.7" +version = "0.16.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0997c976637b606099b9985693efa3581e84e41f5c11ba5255f88711058ad428" +checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" dependencies = [ "der 0.7.7", "digest 0.10.7", @@ -1530,7 +1530,7 @@ dependencies = [ "curve25519-dalek 4.0.0-rc.3", "ed25519 2.2.1", "serde", - "sha2 0.10.7", + "sha2 0.10.8", "zeroize", ] @@ -1548,9 +1548,9 @@ dependencies = [ [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" @@ -1587,7 +1587,7 @@ dependencies = [ "pem-rfc7468 0.7.0", "pkcs8 0.10.2", "rand_core 0.6.4", - "sec1 0.7.2", + "sec1 0.7.3", "subtle", "zeroize", ] @@ -1715,7 +1715,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -1728,7 +1728,7 @@ dependencies = [ "num-traits", "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -1746,15 +1746,15 @@ dependencies = [ [[package]] name = "equivalent" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" dependencies = [ "errno-dragonfly", "libc", @@ -1839,6 +1839,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + [[package]] name = "fd-lock" version = "3.0.13" @@ -1846,7 +1852,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" dependencies = [ "cfg-if", - "rustix 0.38.2", + "rustix", "windows-sys 0.48.0", ] @@ -2008,7 +2014,7 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -2025,7 +2031,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -2105,9 +2111,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "group" @@ -2432,9 +2438,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" dependencies = [ "bytemuck", "byteorder", @@ -2493,17 +2499,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "io-lifetimes" -version = "1.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.48.0", -] - [[package]] name = "ipconfig" version = "0.3.2" @@ -2513,7 +2508,7 @@ dependencies = [ "socket2 0.5.3", "widestring", "windows-sys 0.48.0", - "winreg 0.50.0", + "winreg", ] [[package]] @@ -2530,7 +2525,7 @@ checksum = "e4fb9858c8cd3dd924a5da5bc511363845a9bcfdfac066bb2ef8454eb6111546" dependencies = [ "abao", "anyhow", - "base64 0.21.2", + "base64 0.21.3", "blake3", "bytes", "default-net", @@ -2568,12 +2563,12 @@ dependencies = [ [[package]] name = "is-terminal" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24fddda5af7e54bf7da53067d6e802dbcc381d0a8eef629df528e3ebf68755cb" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", - "rustix 0.38.2", + "rustix", "windows-sys 0.48.0", ] @@ -2588,9 +2583,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b02a5381cc465bd3041d84623d0fa3b66738b52b8e2fc3bab8ad63ab032f4a" +checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" [[package]] name = "jpeg-decoder" @@ -2690,15 +2685,9 @@ checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" [[package]] name = "linux-raw-sys" -version = "0.3.8" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" - -[[package]] -name = "linux-raw-sys" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "lock_api" @@ -2712,9 +2701,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" [[package]] name = "lru-cache" @@ -2748,14 +2737,14 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] [[package]] name = "matchit" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b87248edafb776e59e6ee64a79086f65890d3510f2c656c000bf2a7e8a0aea40" +checksum = "67827e6ea8ee8a7c4a72227ef4fc08957040acffdb5f122733b24fa12daff41b" [[package]] name = "md-5" @@ -2774,9 +2763,9 @@ checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memoffset" @@ -2981,17 +2970,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "num-derive" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "num-derive" version = "0.4.0" @@ -3000,7 +2978,7 @@ checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -3037,9 +3015,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", "libm", @@ -3057,9 +3035,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" dependencies = [ "memchr", ] @@ -3114,7 +3092,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -3162,15 +3140,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "output_vt100" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628223faebab4e3e40667ee0b2336d34a5b960ff60ea743ddfdbcf7770bcfb66" -dependencies = [ - "winapi", -] - [[package]] name = "overload" version = "0.1.1" @@ -3185,7 +3154,7 @@ checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ "ecdsa 0.14.8", "elliptic-curve 0.12.3", - "sha2 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -3194,10 +3163,10 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" dependencies = [ - "ecdsa 0.16.7", + "ecdsa 0.16.8", "elliptic-curve 0.13.5", "primeorder", - "sha2 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -3208,7 +3177,7 @@ checksum = "dfc8c5bf642dde52bb9e87c0ecd8ca5a76faac2eeed98dedb7c717997e1080aa" dependencies = [ "ecdsa 0.14.8", "elliptic-curve 0.12.3", - "sha2 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -3217,10 +3186,10 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" dependencies = [ - "ecdsa 0.16.7", + "ecdsa 0.16.8", "elliptic-curve 0.13.5", "primeorder", - "sha2 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -3254,9 +3223,9 @@ dependencies = [ [[package]] name = "paste" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4b27ab7be369122c218afc2079489cdcb4b517c0a3fc386ff11e1fedfcc2b35" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pem" @@ -3293,12 +3262,12 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pgp" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a79d6411154d1a9908e7a2c4bac60a5742f6125823c2c30780c7039aef02f0" +checksum = "27e1f8e085bfa9b85763fe3ddaacbe90a09cd847b3833129153a6cb063bbe132" dependencies = [ "aes", - "base64 0.21.2", + "base64 0.21.3", "bitfield", "block-padding", "blowfish", @@ -3311,6 +3280,7 @@ dependencies = [ "chrono", "cipher", "crc24", + "curve25519-dalek 4.0.0-rc.3", "derive_builder", "des", "digest 0.10.7", @@ -3324,15 +3294,15 @@ dependencies = [ "md-5", "nom", "num-bigint-dig", - "num-derive 0.3.3", + "num-derive", "num-traits", "p256 0.13.2", "p384 0.13.0", "rand 0.8.5", "ripemd", - "rsa 0.9.0-pre.2", + "rsa 0.9.2", "sha1", - "sha2 0.10.7", + "sha2 0.10.8", "sha3", "signature 2.1.0", "smallvec", @@ -3359,7 +3329,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -3472,15 +3442,15 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.3.3" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "767eb9f07d4a5ebcb39bbf2d452058a93c011373abf6832e24194a1c3f004794" +checksum = "f32154ba0af3a075eefa1eda8bb414ee928f62303a54ea85b8d6638ff1a6ee9e" [[package]] name = "postcard" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfa512cd0d087cc9f99ad30a1bf64795b67871edbead083ffc3a4dfafa59aa00" +checksum = "c9ee729232311d3cd113749948b689627618133b1c5012b77342c1950b25eaeb" dependencies = [ "cobs", "const_format", @@ -3507,13 +3477,11 @@ checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "pretty_assertions" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" dependencies = [ - "ctor", "diff", - "output_vt100", "yansi", ] @@ -3562,27 +3530,26 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.63" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] [[package]] name = "proptest" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e35c06b98bf36aba164cc17cb25f7e232f5c4aeea73baa14b8a9f0d92dbfa65" +checksum = "7c003ac8c77cb07bb74f5f198bce836a689bcd5a42574612bf14d17bfd08c20e" dependencies = [ - "bitflags 1.3.2", - "byteorder", + "bitflags 2.3.3", "lazy_static", "num-traits", "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax 0.6.29", + "regex-syntax 0.7.5", "unarray", ] @@ -3618,9 +3585,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-xml" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b9228215d82c7b61490fec1de287136b5de6f5700f6e58ea9ad61a7964ca51" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" dependencies = [ "memchr", ] @@ -3675,9 +3642,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.29" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -3812,7 +3779,7 @@ checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" dependencies = [ "pem", "ring", - "time 0.3.22", + "time 0.3.24", "yasna", ] @@ -3847,13 +3814,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.4" +version = "1.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.2", + "regex-automata 0.3.9", + "regex-syntax 0.7.5", ] [[package]] @@ -3865,6 +3833,17 @@ dependencies = [ "regex-syntax 0.6.29", ] +[[package]] +name = "regex-automata" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", +] + [[package]] name = "regex-syntax" version = "0.6.29" @@ -3873,17 +3852,17 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ - "base64 0.21.2", + "base64 0.21.3", "bytes", "encoding_rs", "futures-core", @@ -3911,7 +3890,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg 0.10.1", + "winreg", ] [[package]] @@ -3992,9 +3971,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.9.0-pre.2" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65db0998ad35adcaca498b7358992e088ee16cc783fe6fb899da203e113a63e5" +checksum = "6ab43bb47d23c1a631b4b680199a45255dce26fa9ab2fa902581f624ff13e6a8" dependencies = [ "byteorder", "const-oid", @@ -4007,6 +3986,7 @@ dependencies = [ "pkcs8 0.10.2", "rand_core 0.6.4", "signature 2.1.0", + "spki 0.7.2", "subtle", "zeroize", ] @@ -4063,28 +4043,14 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.22" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8818fa822adcc98b18fedbb3632a6a33213c070556b5aa7c4c8cc21cff565c4c" -dependencies = [ - "bitflags 1.3.2", - "errno", - "io-lifetimes", - "libc", - "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", -] - -[[package]] -name = "rustix" -version = "0.38.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aabcb0461ebd01d6b79945797c27f8529082226cb630a9865a71870ff63532a4" +checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" dependencies = [ "bitflags 2.3.3", "errno", "libc", - "linux-raw-sys 0.4.3", + "linux-raw-sys", "windows-sys 0.48.0", ] @@ -4117,14 +4083,14 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" dependencies = [ - "base64 0.21.2", + "base64 0.21.3", ] [[package]] name = "rustversion" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" [[package]] name = "rustyline" @@ -4151,9 +4117,9 @@ dependencies = [ [[package]] name = "ryu" -version = "1.0.14" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe232bdf6be8c8de797b22184ee71118d63780ea42ac85b61d1baa6d3b782ae9" +checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" [[package]] name = "safemem" @@ -4172,9 +4138,9 @@ dependencies = [ [[package]] name = "sanitize-filename" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c502bdb638f1396509467cb0580ef3b29aa2a45c5d43e5d84928241280296c" +checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603" dependencies = [ "lazy_static", "regex", @@ -4191,9 +4157,9 @@ dependencies = [ [[package]] name = "schemars" -version = "0.8.12" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" +checksum = "763f8cd0d4c71ed8389c90cb8100cba87e763bd01a8e614d4f0af97bcd50a161" dependencies = [ "dyn-clone", "schemars_derive", @@ -4203,9 +4169,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.12" +version = "0.8.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" +checksum = "ec0f696e21e10fa546b7ffb1c9672c6de8fbc7a81acf59524386d8639bf12737" dependencies = [ "proc-macro2", "quote", @@ -4215,9 +4181,9 @@ dependencies = [ [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sct" @@ -4245,9 +4211,9 @@ dependencies = [ [[package]] name = "sec1" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0aec48e813d6b90b15f0b8948af3c63483992dee44c03e9930b3eebdabe046e" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct 0.2.0", "der 0.7.7", @@ -4259,9 +4225,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -4272,9 +4238,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", @@ -4288,18 +4254,18 @@ checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6" [[package]] name = "semver" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" dependencies = [ "serde", ] [[package]] name = "serde" -version = "1.0.166" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d01b7404f9d441d3ad40e6a636a7782c377d2abdbe4fa2440e2edcc2f4f10db8" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] @@ -4315,22 +4281,22 @@ dependencies = [ [[package]] name = "serde_bytes" -version = "0.11.10" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c5113243e4a3a1c96587342d067f3e6b0f50790b6cf40d2868eb647a3eef0e" +checksum = "ab33ec92f677585af6d88c65593ae2375adde54efdbf16d597f2cbc7a6d368ff" dependencies = [ "serde", ] [[package]] name = "serde_derive" -version = "1.0.166" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd83d6dde2b6b2d466e14d9d1acce8816dedee94f735eac6395808b3483c6d6" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -4346,9 +4312,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.100" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f1e14e89be7aa4c4b78bdbdc9eb5bf8517829a600ae8eaa39a6e1d960b5185c" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", @@ -4357,9 +4323,9 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.12" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b1b6471d7496b051e03f1958802a73f88b947866f5146f329e47e36554f4e55" +checksum = "4beec8bce849d58d06238cb50db2e1c417cfeafa4c63f692b15c82b7c80f8335" dependencies = [ "itoa", "serde", @@ -4423,9 +4389,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.7" +version = "0.10.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" dependencies = [ "cfg-if", "cpufeatures", @@ -4482,9 +4448,9 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.5" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "238abfbb77c1915110ad968465608b68e869e0772622c9656714e73e5a1a522f" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "slab" @@ -4497,9 +4463,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.10.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "smawk" @@ -4570,7 +4536,7 @@ checksum = "19cfdc32e0199062113edf41f344fbf784b8205a94600233c84eb838f45191e1" dependencies = [ "base64ct", "pem-rfc7468 0.6.0", - "sha2 0.10.7", + "sha2 0.10.8", ] [[package]] @@ -4585,7 +4551,7 @@ dependencies = [ "rand_core 0.6.4", "rsa 0.7.2", "sec1 0.3.0", - "sha2 0.10.7", + "sha2 0.10.8", "signature 1.6.4", "ssh-encoding", "zeroize", @@ -4637,7 +4603,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -4659,9 +4625,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.23" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fb7d6d8281a51045d62b8eb3a7d1ce347b76f312af50cd3dc0af39c87c1737" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -4723,21 +4689,20 @@ dependencies = [ [[package]] name = "tagger" -version = "4.3.4" +version = "4.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aaa6f5d645d1dae4cd0286e9f8bf15b75a31656348e5e106eb1a940abd34b63" +checksum = "094c9f64d6de9a8506b1e49b63a29333b37ed9e821ee04be694d431b3264c3c5" [[package]] name = "tempfile" -version = "3.6.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ - "autocfg", "cfg-if", - "fastrand", + "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix 0.37.22", + "rustix", "windows-sys 0.48.0", ] @@ -4777,22 +4742,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.41" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c16a64ba9387ef3fdae4f9c1a7f07a0997fce91985c0336f1ddc1822b3b37802" +checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.41" +version = "1.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d14928354b01c4d6a4f0e549069adef399a284e7995c7ccca94e8a07a5346c59" +checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -4818,10 +4783,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.22" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +checksum = "b79eabcd964882a646b3584543ccabeae7869e9ac32a46f6f22b7a5bd405308b" dependencies = [ + "deranged", "itoa", "serde", "time-core", @@ -4836,9 +4802,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.9" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +checksum = "eb71511c991639bb078fd5bf97757e03914361c48100d52878b8e52b46fb92cd" dependencies = [ "time-core", ] @@ -4905,7 +4871,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -4973,9 +4939,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes", "futures-core", @@ -4987,9 +4953,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebafdf5ad1220cb59e7d17cf4d2c72015297b75b19a10472f99b89225089240" +checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" dependencies = [ "serde", "serde_spanned", @@ -5008,9 +4974,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.11" +version = "0.19.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" +checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" dependencies = [ "indexmap 2.0.0", "serde", @@ -5068,7 +5034,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -5162,9 +5128,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "typescript-type-def" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e6b74ffbd5684d318252bb7182051df8c4ecc098b542f63fddf792e7f42aa02" +checksum = "356e00027bd9ef773605a353070dc87684b25561a59087ea3ee3dd5fe8854e83" dependencies = [ "serde_json", "typescript-type-def-derive", @@ -5172,9 +5138,9 @@ dependencies = [ [[package]] name = "typescript-type-def-derive" -version = "0.5.6" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b10a4f5dd87c279f90beef31edb7055bfd1ceb66e73148de107a5c9005e9f864" +checksum = "c4e696c28431595138cc53892104528152cbcf26653ae0aa655e4eaede5b9f69" dependencies = [ "darling 0.13.4", "ident_case", @@ -5198,9 +5164,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22049a19f4a68748a168c0fc439f9516686aa045927ff767eca0a85101fb6e73" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" [[package]] name = "unicode-linebreak" @@ -5243,9 +5209,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", @@ -5266,9 +5232,9 @@ checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" [[package]] name = "uuid" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d023da39d1fde5a8a3fe1f3e01ca9632ada0a63e9797de55a879d6e2236277be" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" dependencies = [ "getrandom 0.2.10", "serde", @@ -5356,7 +5322,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -5390,7 +5356,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5660,22 +5626,13 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.7" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" +checksum = "f46aab759304e4d7b2075a9aecba26228bb073ee8c50db796b2c72c676b5d807" dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - [[package]] name = "winreg" version = "0.50.0" @@ -5688,12 +5645,13 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "2.0.0-pre.1" +version = "2.0.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5da623d8af10a62342bcbbb230e33e58a63255a58012f8653c578e54bab48df" +checksum = "ec7fae07da688e17059d5886712c933bb0520f15eff2e09cfa18e30968f4e63a" dependencies = [ - "curve25519-dalek 3.2.0", + "curve25519-dalek 4.0.0-rc.3", "rand_core 0.6.4", + "serde", "zeroize", ] @@ -5712,7 +5670,7 @@ dependencies = [ "oid-registry", "rusticata-macros", "thiserror", - "time 0.3.22", + "time 0.3.24", ] [[package]] @@ -5736,14 +5694,14 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" dependencies = [ - "time 0.3.22", + "time 0.3.24", ] [[package]] name = "yerpc" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30fc983d32883ecb563227a2dcdcbe8567decd9c533b5ecca7e3099e2f7d4c96" +checksum = "75b5547af776328f66a5476ea3b7c0789e6fed164eb32d1a2122cfb39ffa505d" dependencies = [ "anyhow", "async-channel", @@ -5764,9 +5722,9 @@ dependencies = [ [[package]] name = "yerpc_derive" -version = "0.5.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d6b8ce490e8719fe84d7d80ad4d58572b2ea9d7a83d156f6afd6ab3ad5cfb94" +checksum = "f321bb5f728fb066af06c5a994e4375f1f8b054ee6d650766f0bd68dfa4faefe" dependencies = [ "convert_case 0.5.0", "darling 0.14.4", @@ -5792,7 +5750,7 @@ checksum = "9731702e2f0617ad526794ae28fbc6f6ca8849b5ba729666c2a5bc4b6ddee2cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", ] [[package]] @@ -5812,5 +5770,5 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.23", + "syn 2.0.29", ] diff --git a/Cargo.toml b/Cargo.toml index 5863cc9e9..e71720265 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,18 +43,19 @@ async-smtp = { version = "0.9", default-features = false, features = ["runtime-t async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] } backtrace = "0.3" base64 = "0.21" -brotli = { version = "3.3", default-features=false, features = ["std"] } +brotli = { version = "3.4", default-features=false, features = ["std"] } chrono = { version = "0.4", default-features=false, features = ["clock", "std"] } email = { git = "https://github.com/deltachat/rust-email", branch = "master" } encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" } escaper = "0.1" fast-socks5 = "0.8" +fd-lock = "3.0.11" futures = "0.3" futures-lite = "1.13.0" hex = "0.4.0" hickory-resolver = "0.24" humansize = "2" -image = { version = "0.24.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] } +image = { version = "0.24.7", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] } iroh = { version = "0.4.1", default-features = false } kamadak-exif = "0.5" lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" } @@ -70,13 +71,13 @@ parking_lot = "0.12" pgp = { version = "0.10", default-features = false } pretty_env_logger = { version = "0.5", optional = true } qrcodegen = "1.7.0" -quick-xml = "0.29" +quick-xml = "0.30" rand = "0.8" -regex = "1.8" -reqwest = { version = "0.11.18", features = ["json"] } +regex = "1.9" +reqwest = { version = "0.11.20", features = ["json"] } rusqlite = { version = "0.29", features = ["sqlcipher"] } rust-hsluv = "0.1" -sanitize-filename = "0.4" +sanitize-filename = "0.5" serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } sha-1 = "0.10" @@ -91,7 +92,7 @@ tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] } tokio-io-timeout = "1.2.0" tokio-stream = { version = "0.1.14", features = ["fs"] } tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar -tokio-util = "0.7.8" +tokio-util = "0.7.9" toml = "0.7" url = "2" uuid = { version = "1", features = ["serde", "v4"] } @@ -119,11 +120,6 @@ members = [ "format-flowed", ] -[[example]] -name = "simple" -path = "examples/simple.rs" - - [[bench]] name = "create_account" harness = false diff --git a/README.md b/README.md index 9dec8d468..96ff5b485 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,16 @@ -# Delta Chat Rust +

+ Delta Chat Logo +

-> Deltachat-core written in Rust +

+ + Rust CI + +

-[![Rust CI](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml) +

+The core library for Delta Chat, written in Rust +

## Installing Rust and Cargo diff --git a/benches/create_account.rs b/benches/create_account.rs index 5e1ae8561..c487004ac 100644 --- a/benches/create_account.rs +++ b/benches/create_account.rs @@ -8,7 +8,8 @@ async fn create_accounts(n: u32) { let dir = tempdir().unwrap(); let p: PathBuf = dir.path().join("accounts"); - let mut accounts = Accounts::new(p.clone()).await.unwrap(); + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await.unwrap(); for expected_id in 2..n { let id = accounts.add_account().await.unwrap(); diff --git a/cliff.toml b/cliff.toml index 86a2303e5..98a959c28 100644 --- a/cliff.toml +++ b/cliff.toml @@ -54,7 +54,7 @@ header = """ # Changelog\n """ # template for the changelog body -# https://tera.netlify.app/docs/#introduction +# https://keats.github.io/tera/docs/#templates body = """ {% if version %}\ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} diff --git a/deltachat-ffi/Doxyfile b/deltachat-ffi/Doxyfile index 03365989d..5c60f15e3 100644 --- a/deltachat-ffi/Doxyfile +++ b/deltachat-ffi/Doxyfile @@ -846,7 +846,7 @@ EXCLUDE_PATTERNS = # exclude all test directories use the pattern */test/* ###################################################### -EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_keyring_t dc_loginparam_t dc_mime*_t +EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_loginparam_t dc_mime*_t EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t EXCLUDE_SYMBOLS += _dc_* jsmn* ###################################################### diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index a1c45214f..ab4d746d4 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -511,6 +511,13 @@ char* dc_get_blobdir (const dc_context_t* context); * - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in * seconds. 2 days by default. * This is not supposed to be changed by UIs and only used for testing. + * - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it + * to 1 if it supports verified 1:1 chats. + * Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified, + * and when the key changes, an info message is posted into the chat. + * 0=Nothing else happens when the key changes. + * 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true + * until `dc_accept_chat()` is called. * - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes. * The prefix should be followed by the system and maybe subsystem, * e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`. @@ -1130,7 +1137,7 @@ dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id); * * In JS land, that would be mapped to something as: * ``` - * success = window.webxdc.sendUpdate('{"action":"move","src":"A3","dest":"B4"}', 'move A3 B4'); + * success = window.webxdc.sendUpdate('{payload: {"action":"move","src":"A3","dest":"B4"}}', 'move A3 B4'); * ``` * `context` and `msg_id` are not needed in JS as those are unique within a webxdc instance. * See dc_get_webxdc_status_updates() for the receiving counterpart. @@ -1512,24 +1519,6 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3); -/** - * Enable or disable protection against active attacks. - * To enable protection, it is needed that all members are verified; - * if this condition is met, end-to-end-encryption is always enabled - * and only the verified keys are used. - * - * Sends out #DC_EVENT_CHAT_MODIFIED on changes - * and #DC_EVENT_MSGS_CHANGED if a status message was sent. - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id The ID of the chat to change the protection for. - * @param protect 1=protect chat, 0=unprotect chat - * @return 1=success, 0=error, e.g. some members may be unverified - */ -int dc_set_chat_protection (dc_context_t* context, uint32_t chat_id, int protect); - - /** * Set chat visibility to pinned, archived or normal. * @@ -2956,12 +2945,15 @@ int dc_receive_backup (dc_context_t* context, const char* qr); * @param dir The directory to create the context-databases in. * If the directory does not exist, * dc_accounts_new() will try to create it. + * @param writable Whether the returned account manager is writable, i.e. calling these functions on + * it is possible: dc_accounts_add_account(), dc_accounts_add_closed_account(), + * dc_accounts_migrate_account(), dc_accounts_remove_account(), dc_accounts_select_account(). * @return An account manager object. * The object must be passed to the other account manager functions * and must be freed using dc_accounts_unref() after usage. * On errors, NULL is returned. */ -dc_accounts_t* dc_accounts_new (const char* os_name, const char* dir); +dc_accounts_t* dc_accounts_new (const char* dir, int writable); /** @@ -3742,7 +3734,6 @@ int dc_chat_can_send (const dc_chat_t* chat); * Check if a chat is protected. * Protected chats contain only verified members and encryption is always enabled. * Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1. - * The status can be changed using dc_set_chat_protection(). * * @memberof dc_chat_t * @param chat The chat object. @@ -3751,6 +3742,26 @@ int dc_chat_can_send (const dc_chat_t* chat); int dc_chat_is_protected (const dc_chat_t* chat); +/** + * Checks if the chat was protected, and then an incoming message broke this protection. + * + * This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag, + * otherwise it will return false for all chats. + * + * 1:1 chats are automatically set as protected when a contact is verified. + * When a message comes in that is not encrypted / signed correctly, + * the chat is automatically set as unprotected again. + * dc_chat_is_protection_broken() will return true until dc_accept_chat() is called. + * + * The UI should let the user confirm that this is OK with a message like + * `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat(). + * @memberof dc_chat_t + * @param chat The chat object. + * @return 1=chat protection broken, 0=otherwise. + */ +int dc_chat_is_protection_broken (const dc_chat_t* chat); + + /** * Check if locations are sent to the chat * at the time the object was created using dc_get_chat(). @@ -4345,7 +4356,7 @@ int dc_msg_is_forwarded (const dc_msg_t* msg); * Check if the message is an informational message, created by the * device or by another users. Such messages are not "typed" by the user but * created due to other actions, - * e.g. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection() + * e.g. dc_set_chat_name(), dc_set_chat_profile_image(), * or dc_add_contact_to_chat(). * * These messages are typically shown in the center of the chat view, @@ -5039,7 +5050,12 @@ int dc_contact_is_verified (dc_contact_t* contact); /** * Return the address that verified a contact * - * The UI may use this in addition to a checkmark showing the verification status + * The UI may use this in addition to a checkmark showing the verification status. + * In case of verification chains, + * the last contact in the chain is shown. + * This is because of privacy reasons, but also as it would not help the user + * to see a unknown name here - where one can mostly always ask the shown name + * as it is directly known. * * @memberof dc_contact_t * @param contact The contact object. @@ -6780,15 +6796,6 @@ void dc_event_unref(dc_event_t* event); /// Used in error strings. #define DC_STR_ERROR_NO_NETWORK 87 -/// "Chat protection enabled." -/// - -/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_ENABLED_PROTECTION and DC_STR_MSG_PROTECTION_ENABLED_BY. -#define DC_STR_PROTECTION_ENABLED 88 - -/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_DISABLED_PROTECTION and DC_STR_MSG_PROTECTION_DISABLED_BY. -#define DC_STR_PROTECTION_DISABLED 89 - /// "Reply" /// /// Used in summaries. @@ -7233,26 +7240,6 @@ void dc_event_unref(dc_event_t* event); /// `%2$s` will be replaced by name and address of the contact. #define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157 -/// "You enabled chat protection." -/// -/// Used in status messages. -#define DC_STR_PROTECTION_ENABLED_BY_YOU 158 - -/// "Chat protection enabled by %1$s." -/// -/// `%1$s` will be replaced by name and address of the contact. -/// -/// Used in status messages. -#define DC_STR_PROTECTION_ENABLED_BY_OTHER 159 - -/// "You disabled chat protection." -#define DC_STR_PROTECTION_DISABLED_BY_YOU 160 - -/// "Chat protection disabled by %1$s." -/// -/// `%1$s` will be replaced by name and address of the contact. -#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161 - /// "Scan to set up second device for %1$s" /// /// `%1$s` will be replaced by name and address of the account. @@ -7263,6 +7250,16 @@ void dc_event_unref(dc_event_t* event); /// Used as a device message after a successful backup transfer. #define DC_STR_BACKUP_TRANSFER_MSG_BODY 163 +/// "Messages are guaranteed to be end-to-end encrypted from now on." +/// +/// Used in info messages. +#define DC_STR_CHAT_PROTECTION_ENABLED 170 + +/// "%1$s sent a message from another device." +/// +/// Used in info messages. +#define DC_STR_CHAT_PROTECTION_DISABLED 171 + /** * @} */ diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 86d142355..025c3f7ce 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -29,7 +29,7 @@ use deltachat::contact::{Contact, ContactId, Origin}; use deltachat::context::Context; use deltachat::ephemeral::Timer as EphemeralTimer; use deltachat::imex::BackupProvider; -use deltachat::key::{DcKey, DcSecretKey}; +use deltachat::key::preconfigure_keypair; use deltachat::message::MsgId; use deltachat::net::read_url_blob; use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg}; @@ -813,21 +813,12 @@ pub unsafe extern "C" fn dc_preconfigure_keypair( return 0; } let ctx = &*context; - block_on(async move { - let addr = tools::EmailAddress::new(&to_string_lossy(addr))?; - let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0; - let public = secret.split_public_key()?; - let keypair = key::KeyPair { - addr, - public, - secret, - }; - key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default).await?; - Ok::<_, anyhow::Error>(1) - }) - .context("Failed to save keypair") - .log_err(ctx) - .unwrap_or(0) + let addr = to_string_lossy(addr); + let secret_data = to_string_lossy(secret_data); + block_on(preconfigure_keypair(ctx, &addr, &secret_data)) + .context("Failed to save keypair") + .log_err(ctx) + .is_ok() as libc::c_int } #[no_mangle] @@ -1472,32 +1463,6 @@ pub unsafe extern "C" fn dc_get_next_media( }) } -#[no_mangle] -pub unsafe extern "C" fn dc_set_chat_protection( - context: *mut dc_context_t, - chat_id: u32, - protect: libc::c_int, -) -> libc::c_int { - if context.is_null() { - eprintln!("ignoring careless call to dc_set_chat_protection()"); - return 0; - } - let ctx = &*context; - let protect = if let Some(s) = ProtectionStatus::from_i32(protect) { - s - } else { - warn!(ctx, "bad protect-value for dc_set_chat_protection()"); - return 0; - }; - - block_on(async move { - match ChatId::new(chat_id).set_protection(ctx, protect).await { - Ok(()) => 1, - Err(_) => 0, - } - }) -} - #[no_mangle] pub unsafe extern "C" fn dc_set_chat_visibility( context: *mut dc_context_t, @@ -3132,6 +3097,16 @@ pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_i ffi_chat.chat.is_protected() as libc::c_int } +#[no_mangle] +pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int { + if chat.is_null() { + eprintln!("ignoring careless call to dc_chat_is_protection_broken()"); + return 0; + } + let ffi_chat = &*chat; + ffi_chat.chat.is_protection_broken() as libc::c_int +} + #[no_mangle] pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int { if chat.is_null() { @@ -4750,17 +4725,17 @@ pub type dc_accounts_t = AccountsWrapper; #[no_mangle] pub unsafe extern "C" fn dc_accounts_new( - _os_name: *const libc::c_char, - dbfile: *const libc::c_char, + dir: *const libc::c_char, + writable: libc::c_int, ) -> *mut dc_accounts_t { setup_panic!(); - if dbfile.is_null() { + if dir.is_null() { eprintln!("ignoring careless call to dc_accounts_new()"); return ptr::null_mut(); } - let accs = block_on(Accounts::new(as_path(dbfile).into())); + let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0)); match accs { Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))), diff --git a/deltachat-jsonrpc/Cargo.toml b/deltachat-jsonrpc/Cargo.toml index 2080f0dfa..16b7b2827 100644 --- a/deltachat-jsonrpc/Cargo.toml +++ b/deltachat-jsonrpc/Cargo.toml @@ -15,22 +15,22 @@ required-features = ["webserver"] anyhow = "1" deltachat = { path = ".." } num-traits = "0.2" -schemars = "0.8.11" +schemars = "0.8.13" serde = { version = "1.0", features = ["derive"] } -tempfile = "3.6.0" +tempfile = "3.8.0" log = "0.4" async-channel = { version = "1.8.0" } futures = { version = "0.3.28" } -serde_json = "1.0.99" -yerpc = { version = "0.5.1", features = ["anyhow_expose", "openrpc"] } -typescript-type-def = { version = "0.5.5", features = ["json_value"] } +serde_json = "1.0.105" +yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] } +typescript-type-def = { version = "0.5.8", features = ["json_value"] } tokio = { version = "1.33.0" } -sanitize-filename = "0.4" +sanitize-filename = "0.5" walkdir = "2.3.3" base64 = "0.21" # optional dependencies -axum = { version = "0.6.18", optional = true, features = ["ws"] } +axum = { version = "0.6.20", optional = true, features = ["ws"] } env_logger = { version = "0.10.0", optional = true } [dev-dependencies] diff --git a/deltachat-jsonrpc/src/api/mod.rs b/deltachat-jsonrpc/src/api/mod.rs index 580634cf5..b67c8e595 100644 --- a/deltachat-jsonrpc/src/api/mod.rs +++ b/deltachat-jsonrpc/src/api/mod.rs @@ -4,30 +4,30 @@ use std::{collections::HashMap, str::FromStr}; use anyhow::{anyhow, bail, ensure, Context, Result}; pub use deltachat::accounts::Accounts; -use deltachat::message::get_msg_read_receipts; -use deltachat::qr::Qr; -use deltachat::{ - chat::{ - self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex, - marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions, - ProtectionStatus, - }, - chatlist::Chatlist, - config::Config, - constants::DC_MSG_ID_DAYMARKER, - contact::{may_be_valid_addr, Contact, ContactId, Origin}, - context::get_info, - ephemeral::Timer, - imex, location, - message::{self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype}, - provider::get_provider_info, - qr, - qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg}, - reaction::{get_msg_reactions, send_reaction}, - securejoin, - stock_str::StockMessage, - webxdc::StatusUpdateSerial, +use deltachat::chat::{ + self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex, + marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions, + ProtectionStatus, }; +use deltachat::chatlist::Chatlist; +use deltachat::config::Config; +use deltachat::constants::DC_MSG_ID_DAYMARKER; +use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin}; +use deltachat::context::get_info; +use deltachat::ephemeral::Timer; +use deltachat::imex; +use deltachat::location; +use deltachat::message::get_msg_read_receipts; +use deltachat::message::{ + self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype, +}; +use deltachat::provider::get_provider_info; +use deltachat::qr::{self, Qr}; +use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg}; +use deltachat::reaction::{get_msg_reactions, send_reaction}; +use deltachat::securejoin; +use deltachat::stock_str::StockMessage; +use deltachat::webxdc::StatusUpdateSerial; use sanitize_filename::is_sanitized; use tokio::fs; use tokio::sync::{watch, Mutex, RwLock}; @@ -142,11 +142,7 @@ impl CommandApi { } } -#[rpc( - all_positional, - ts_outdir = "typescript/generated", - openrpc_outdir = "openrpc" -)] +#[rpc(all_positional, ts_outdir = "typescript/generated")] impl CommandApi { /// Test function. async fn sleep(&self, delay: f64) { @@ -157,12 +153,12 @@ impl CommandApi { // Misc top level functions // --------------------------------------------- - /// Check if an email address is valid. + /// Checks if an email address is valid. async fn check_email_validity(&self, email: String) -> bool { may_be_valid_addr(&email) } - /// Get general system info. + /// Returns general system info. async fn get_system_info(&self) -> BTreeMap<&'static str, String> { get_info() } @@ -223,11 +219,13 @@ impl CommandApi { Ok(accounts) } + /// Starts background tasks for all accounts. async fn start_io_for_all_accounts(&self) -> Result<()> { self.accounts.read().await.start_io().await; Ok(()) } + /// Stops background tasks for all accounts. async fn stop_io_for_all_accounts(&self) -> Result<()> { self.accounts.read().await.stop_io().await; Ok(()) @@ -237,14 +235,16 @@ impl CommandApi { // Methods that work on individual accounts // --------------------------------------------- - async fn start_io(&self, id: u32) -> Result<()> { - let ctx = self.get_context(id).await?; + /// Starts background tasks for a single account. + async fn start_io(&self, account_id: u32) -> Result<()> { + let ctx = self.get_context(account_id).await?; ctx.start_io().await; Ok(()) } - async fn stop_io(&self, id: u32) -> Result<()> { - let ctx = self.get_context(id).await?; + /// Stops background tasks for a single account. + async fn stop_io(&self, account_id: u32) -> Result<()> { + let ctx = self.get_context(account_id).await?; ctx.stop_io().await; Ok(()) } @@ -311,11 +311,13 @@ impl CommandApi { ctx.get_info().await } + /// Sets the given configuration key. async fn set_config(&self, account_id: u32, key: String, value: Option) -> Result<()> { let ctx = self.get_context(account_id).await?; set_config(&ctx, &key, value.as_deref()).await } + /// Updates a batch of configuration values. async fn batch_set_config( &self, account_id: u32, @@ -347,6 +349,7 @@ impl CommandApi { Ok(qr_object) } + /// Returns configuration value for the given key. async fn get_config(&self, account_id: u32, key: String) -> Result> { let ctx = self.get_context(account_id).await?; get_config(&ctx, &key).await @@ -1741,6 +1744,9 @@ impl CommandApi { let mut msg = Message::new(Viewtype::Sticker); msg.set_file(&sticker_path, None); + // JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image] + msg.force_sticker(); + let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?; Ok(message_id.to_u32()) } @@ -1874,7 +1880,7 @@ impl CommandApi { .context("path conversion to string failed") } - /// save a sticker to a collection/folder in the account's sticker folder + /// Saves a sticker to a collection/folder in the account's sticker folder. async fn misc_save_sticker( &self, account_id: u32, diff --git a/deltachat-jsonrpc/src/api/types/account.rs b/deltachat-jsonrpc/src/api/types/account.rs index d27a53723..b2909109d 100644 --- a/deltachat-jsonrpc/src/api/types/account.rs +++ b/deltachat-jsonrpc/src/api/types/account.rs @@ -7,7 +7,7 @@ use typescript_type_def::TypeDef; use super::color_int_to_hex_string; #[derive(Serialize, TypeDef, schemars::JsonSchema)] -#[serde(tag = "type")] +#[serde(tag = "kind")] pub enum Account { #[serde(rename_all = "camelCase")] Configured { diff --git a/deltachat-jsonrpc/src/api/types/chat.rs b/deltachat-jsonrpc/src/api/types/chat.rs index c8d7d867f..e2146c8c1 100644 --- a/deltachat-jsonrpc/src/api/types/chat.rs +++ b/deltachat-jsonrpc/src/api/types/chat.rs @@ -167,10 +167,11 @@ impl BasicChat { } #[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)] +#[serde(tag = "kind")] pub enum MuteDuration { NotMuted, Forever, - Until(i64), + Until { duration: i64 }, } impl MuteDuration { @@ -178,13 +179,13 @@ impl MuteDuration { match self { MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted), MuteDuration::Forever => Ok(chat::MuteDuration::Forever), - MuteDuration::Until(n) => { - if n <= 0 { + MuteDuration::Until { duration } => { + if duration <= 0 { bail!("failed to read mute duration") } Ok(SystemTime::now() - .checked_add(Duration::from_secs(n as u64)) + .checked_add(Duration::from_secs(duration as u64)) .map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until)) } } diff --git a/deltachat-jsonrpc/src/api/types/chat_list.rs b/deltachat-jsonrpc/src/api/types/chat_list.rs index fac1fb039..b47f7a728 100644 --- a/deltachat-jsonrpc/src/api/types/chat_list.rs +++ b/deltachat-jsonrpc/src/api/types/chat_list.rs @@ -15,7 +15,7 @@ use super::color_int_to_hex_string; use super::message::MessageViewtype; #[derive(Serialize, TypeDef, schemars::JsonSchema)] -#[serde(tag = "type")] +#[serde(tag = "kind")] pub enum ChatListItemFetchResult { #[serde(rename_all = "camelCase")] ChatListItem { diff --git a/deltachat-jsonrpc/src/api/types/events.rs b/deltachat-jsonrpc/src/api/types/events.rs index 0cfb02a84..dd03358bc 100644 --- a/deltachat-jsonrpc/src/api/types/events.rs +++ b/deltachat-jsonrpc/src/api/types/events.rs @@ -22,7 +22,7 @@ impl From for Event { } #[derive(Serialize, TypeDef, schemars::JsonSchema)] -#[serde(tag = "type")] +#[serde(tag = "kind")] pub enum EventType { /// The library-user may write an informational string to the log. /// diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index 5132e5231..4bd691e3a 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -19,7 +19,7 @@ use super::reactions::JSONRPCReactions; use super::webxdc::WebxdcMessageInfo; #[derive(Serialize, TypeDef, schemars::JsonSchema)] -#[serde(rename_all = "camelCase", tag = "variant")] +#[serde(rename_all = "camelCase", tag = "kind")] pub enum MessageLoadResult { Message(MessageObject), LoadingError { error: String }, diff --git a/deltachat-jsonrpc/src/api/types/qr.rs b/deltachat-jsonrpc/src/api/types/qr.rs index d53ebeab5..0f6d79c8c 100644 --- a/deltachat-jsonrpc/src/api/types/qr.rs +++ b/deltachat-jsonrpc/src/api/types/qr.rs @@ -4,7 +4,7 @@ use typescript_type_def::TypeDef; #[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename = "Qr", rename_all = "camelCase")] -#[serde(tag = "type")] +#[serde(tag = "kind")] pub enum QrObject { AskVerifyContact { contact_id: u32, diff --git a/deltachat-jsonrpc/src/lib.rs b/deltachat-jsonrpc/src/lib.rs index 16592bd88..10ec39ea4 100644 --- a/deltachat-jsonrpc/src/lib.rs +++ b/deltachat-jsonrpc/src/lib.rs @@ -13,7 +13,8 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn basic_json_rpc_functionality() -> anyhow::Result<()> { let tmp_dir = TempDir::new().unwrap().path().into(); - let accounts = Accounts::new(tmp_dir).await?; + let writable = true; + let accounts = Accounts::new(tmp_dir, writable).await?; let api = CommandApi::new(accounts); let (sender, mut receiver) = unbounded::(); @@ -54,7 +55,8 @@ mod tests { #[tokio::test(flavor = "multi_thread")] async fn test_batch_set_config() -> anyhow::Result<()> { let tmp_dir = TempDir::new().unwrap().path().into(); - let accounts = Accounts::new(tmp_dir).await?; + let writable = true; + let accounts = Accounts::new(tmp_dir, writable).await?; let api = CommandApi::new(accounts); let (sender, mut receiver) = unbounded::(); diff --git a/deltachat-jsonrpc/src/webserver.rs b/deltachat-jsonrpc/src/webserver.rs index df8f92135..f4b6f38af 100644 --- a/deltachat-jsonrpc/src/webserver.rs +++ b/deltachat-jsonrpc/src/webserver.rs @@ -19,7 +19,8 @@ async fn main() -> Result<(), std::io::Error> { .map(|port| port.parse::().expect("DC_PORT must be a number")) .unwrap_or(DEFAULT_PORT); log::info!("Starting with accounts directory `{path}`."); - let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap(); + let writable = true; + let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap(); let state = CommandApi::new(accounts); let app = Router::new() diff --git a/deltachat-jsonrpc/typescript/example/example.ts b/deltachat-jsonrpc/typescript/example/example.ts index d7b038e0c..e45bc18cc 100644 --- a/deltachat-jsonrpc/typescript/example/example.ts +++ b/deltachat-jsonrpc/typescript/example/example.ts @@ -35,7 +35,7 @@ async function run() { const accounts = await client.rpc.getAllAccounts(); console.log("accounts loaded", accounts); for (const account of accounts) { - if (account.type === "Configured") { + if (account.kind === "Configured") { write( $head, ` @@ -57,7 +57,7 @@ async function run() { clear($main); const selectedAccount = SELECTED_ACCOUNT; const info = await client.rpc.getAccountInfo(selectedAccount); - if (info.type !== "Configured") { + if (info.kind !== "Configured") { return write($main, "Account is not configured"); } write($main, `

${info.addr!}

`); @@ -81,8 +81,7 @@ async function run() { messageIds ); for (const [_messageId, message] of Object.entries(messages)) { - if (message.variant === "message") - write($main, `

${message.text}

`); + if (message.kind === "message") write($main, `

${message.text}

`); else write($main, `

loading error: ${message.error}

`); } } @@ -93,9 +92,9 @@ async function run() { $side, `

- [${event.type} on account ${accountId}]
+ [${event.kind} on account ${accountId}]
f1: ${JSON.stringify( - Object.assign({}, event, { type: undefined }) + Object.assign({}, event, { kind: undefined }) )}

` ); diff --git a/deltachat-jsonrpc/typescript/src/client.ts b/deltachat-jsonrpc/typescript/src/client.ts index 1a51b5e04..83cc2f7e7 100644 --- a/deltachat-jsonrpc/typescript/src/client.ts +++ b/deltachat-jsonrpc/typescript/src/client.ts @@ -6,22 +6,22 @@ import { WebsocketTransport, BaseTransport, Request } from "yerpc"; import { TinyEmitter } from "@deltachat/tiny-emitter"; type Events = { ALL: (accountId: number, event: EventType) => void } & { - [Property in EventType["type"]]: ( + [Property in EventType["kind"]]: ( accountId: number, - event: Extract + event: Extract ) => void; }; type ContextEvents = { ALL: (event: EventType) => void } & { - [Property in EventType["type"]]: ( - event: Extract + [Property in EventType["kind"]]: ( + event: Extract ) => void; }; export type DcEvent = EventType; -export type DcEventType = Extract< +export type DcEventType = Extract< EventType, - { type: T } + { kind: T } >; export class BaseDeltaChat< @@ -46,12 +46,12 @@ export class BaseDeltaChat< while (true) { const event = await this.rpc.getNextEvent(); //@ts-ignore - this.emit(event.event.type, event.contextId, event.event); + this.emit(event.event.kind, event.contextId, event.event); this.emit("ALL", event.contextId, event.event); if (this.contextEmitters[event.contextId]) { this.contextEmitters[event.contextId].emit( - event.event.type, + event.event.kind, //@ts-ignore event.event as any ); diff --git a/deltachat-jsonrpc/typescript/test/online.ts b/deltachat-jsonrpc/typescript/test/online.ts index c9fa4077a..c85376b09 100644 --- a/deltachat-jsonrpc/typescript/test/online.ts +++ b/deltachat-jsonrpc/typescript/test/online.ts @@ -29,8 +29,8 @@ describe("online tests", function () { serverHandle = await startServer(); dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true); - dc.on("ALL", (contextId, { type }) => { - if (type !== "Info") console.log(contextId, type); + dc.on("ALL", (contextId, { kind }) => { + if (kind !== "Info") console.log(contextId, kind); }); account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL); @@ -177,12 +177,12 @@ describe("online tests", function () { }); }); -async function waitForEvent( +async function waitForEvent( dc: DeltaChat, eventType: T, accountId: number, timeout: number = EVENT_TIMEOUT -): Promise> { +): Promise> { return new Promise((resolve, reject) => { const rejectTimeout = setTimeout( () => reject(new Error("Timeout reached before event came in")), diff --git a/deltachat-repl/Cargo.toml b/deltachat-repl/Cargo.toml index 2a2d1cd2e..dd5cb0a67 100644 --- a/deltachat-repl/Cargo.toml +++ b/deltachat-repl/Cargo.toml @@ -9,7 +9,7 @@ ansi_term = "0.12.1" anyhow = "1" deltachat = { path = "..", features = ["internals"]} dirs = "5" -log = "0.4.19" +log = "0.4.20" pretty_env_logger = "0.5" rusqlite = "0.29" rustyline = "12" diff --git a/deltachat-repl/src/cmdline.rs b/deltachat-repl/src/cmdline.rs index 9011813c5..40fa149fa 100644 --- a/deltachat-repl/src/cmdline.rs +++ b/deltachat-repl/src/cmdline.rs @@ -18,6 +18,7 @@ use deltachat::imex::*; use deltachat::location; use deltachat::log::LogExt; use deltachat::message::{self, Message, MessageState, MsgId, Viewtype}; +use deltachat::mimeparser::SystemMessage; use deltachat::peerstate::*; use deltachat::qr::*; use deltachat::reaction::send_reaction; @@ -210,7 +211,17 @@ async fn log_msg(context: &Context, prefix: impl AsRef, msg: &Message) { } else { "[FRESH]" }, - if msg.is_info() { "[INFO]" } else { "" }, + if msg.is_info() { + if msg.get_info_type() == SystemMessage::ChatProtectionEnabled { + "[INFO πŸ›‘οΈ]" + } else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled { + "[INFO πŸ›‘οΈβŒ]" + } else { + "[INFO]" + } + } else { + "" + }, if msg.get_viewtype() == Viewtype::VideochatInvitation { format!( "[VIDEOCHAT-INVITATION: {}, type={}]", @@ -395,8 +406,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu unpin \n\ mute []\n\ unmute \n\ - protect \n\ - unprotect \n\ delchat \n\ accept \n\ decline \n\ @@ -1071,20 +1080,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu }; chat::set_muted(&context, chat_id, duration).await?; } - "protect" | "unprotect" => { - ensure!(!arg1.is_empty(), "Argument missing."); - let chat_id = ChatId::new(arg1.parse()?); - chat_id - .set_protection( - &context, - match arg0 { - "protect" => ProtectionStatus::Protected, - "unprotect" => ProtectionStatus::Unprotected, - _ => unreachable!("arg0={:?}", arg0), - }, - ) - .await?; - } "delchat" => { ensure!(!arg1.is_empty(), "Argument missing."); let chat_id = ChatId::new(arg1.parse()?); diff --git a/deltachat-rpc-client/examples/echobot_advanced.py b/deltachat-rpc-client/examples/echobot_advanced.py index 0c315ac5d..221f69dda 100644 --- a/deltachat-rpc-client/examples/echobot_advanced.py +++ b/deltachat-rpc-client/examples/echobot_advanced.py @@ -14,9 +14,9 @@ hooks = events.HookCollection() @hooks.on(events.RawEvent) def log_event(event): - if event.type == EventType.INFO: + if event.kind == EventType.INFO: logging.info(event.msg) - elif event.type == EventType.WARNING: + elif event.kind == EventType.WARNING: logging.warning(event.msg) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py index 68e310811..20fc11b36 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/chat.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/chat.py @@ -54,14 +54,14 @@ class Chat: """ if duration is not None: assert duration > 0, "Invalid duration" - dur: Union[str, dict] = {"Until": duration} + dur: dict = {"kind": "Until", "duration": duration} else: - dur = "Forever" + dur = {"kind": "Forever"} self._rpc.set_chat_mute_duration(self.account.id, self.id, dur) def unmute(self) -> None: """Unmute this chat.""" - self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted") + self._rpc.set_chat_mute_duration(self.account.id, self.id, {"kind": "NotMuted"}) def pin(self) -> None: """Pin this chat.""" diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/client.py b/deltachat-rpc-client/src/deltachat_rpc_client/client.py index e27c35a1f..31b628ffa 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/client.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/client.py @@ -104,10 +104,10 @@ class Client: self._process_messages() # Process old messages. while True: event = self.account.wait_for_event() - event["type"] = EventType(event.type) + event["kind"] = EventType(event.kind) event["account"] = self.account self._on_event(event) - if event.type == EventType.INCOMING_MSG: + if event.kind == EventType.INCOMING_MSG: self._process_messages() stop = func(event) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/events.py b/deltachat-rpc-client/src/deltachat_rpc_client/events.py index f83c06e83..b90b6e045 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/events.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/events.py @@ -79,7 +79,7 @@ class RawEvent(EventFilter): return False def filter(self, event: "AttrDict") -> bool: - if self.types and event.type not in self.types: + if self.types and event.kind not in self.types: return False return self._call_func(event) diff --git a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py index 9fb1422e7..4d569d3ea 100644 --- a/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py +++ b/deltachat-rpc-client/src/deltachat_rpc_client/pytestplugin.py @@ -54,7 +54,7 @@ class ACFactory: account.start_io() while True: event = account.wait_for_event() - if event.type == EventType.IMAP_INBOX_IDLE: + if event.kind == EventType.IMAP_INBOX_IDLE: break return account @@ -95,7 +95,7 @@ class ACFactory: group=group, ) - return to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG) + return to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG) @pytest.fixture() diff --git a/deltachat-rpc-client/tests/test_something.py b/deltachat-rpc-client/tests/test_something.py index b667d5c00..f91f9844e 100644 --- a/deltachat-rpc-client/tests/test_something.py +++ b/deltachat-rpc-client/tests/test_something.py @@ -1,4 +1,6 @@ import concurrent.futures +import json +import subprocess from unittest.mock import MagicMock import pytest @@ -42,7 +44,7 @@ def test_acfactory(acfactory) -> None: account = acfactory.new_configured_account() while True: event = account.wait_for_event() - if event.type == EventType.CONFIGURE_PROGRESS: + if event.kind == EventType.CONFIGURE_PROGRESS: assert event.progress != 0 # Progress 0 indicates error. if event.progress == 1000: # Success break @@ -71,7 +73,7 @@ def test_account(acfactory) -> None: while True: event = bob.wait_for_event() - if event.type == EventType.INCOMING_MSG: + if event.kind == EventType.INCOMING_MSG: chat_id = event.chat_id msg_id = event.msg_id break @@ -140,7 +142,7 @@ def test_chat(acfactory) -> None: while True: event = bob.wait_for_event() - if event.type == EventType.INCOMING_MSG: + if event.kind == EventType.INCOMING_MSG: chat_id = event.chat_id msg_id = event.msg_id break @@ -224,7 +226,7 @@ def test_message(acfactory) -> None: while True: event = bob.wait_for_event() - if event.type == EventType.INCOMING_MSG: + if event.kind == EventType.INCOMING_MSG: chat_id = event.chat_id msg_id = event.msg_id break @@ -263,7 +265,7 @@ def test_is_bot(acfactory) -> None: while True: event = bob.wait_for_event() - if event.type == EventType.INCOMING_MSG: + if event.kind == EventType.INCOMING_MSG: msg_id = event.msg_id message = bob.get_message_by_id(msg_id) snapshot = message.get_snapshot() @@ -346,3 +348,13 @@ def test_import_export(acfactory, tmp_path) -> None: files = list(tmp_path.glob("*.tar")) alice2 = acfactory.get_unconfigured_account() alice2.import_backup(files[0]) + + assert alice2.manager.get_system_info() + + +def test_openrpc_command_line() -> None: + """Test that "deltachat-rpc-server --openrpc" command returns an OpenRPC specification.""" + out = subprocess.run(["deltachat-rpc-server", "--openrpc"], capture_output=True, check=True).stdout + openrpc = json.loads(out) + assert "openrpc" in openrpc + assert "methods" in openrpc diff --git a/deltachat-rpc-client/tests/test_webxdc.py b/deltachat-rpc-client/tests/test_webxdc.py index cb62566e4..8f3d8edcb 100644 --- a/deltachat-rpc-client/tests/test_webxdc.py +++ b/deltachat-rpc-client/tests/test_webxdc.py @@ -11,7 +11,7 @@ def test_webxdc(acfactory) -> None: while True: event = bob.wait_for_event() - if event.type == EventType.INCOMING_MSG: + if event.kind == EventType.INCOMING_MSG: bob_chat_alice = bob.get_chat_by_id(event.chat_id) message = bob.get_message_by_id(event.msg_id) break diff --git a/deltachat-rpc-server/Cargo.toml b/deltachat-rpc-server/Cargo.toml index b30e67b83..6315c5b89 100644 --- a/deltachat-rpc-server/Cargo.toml +++ b/deltachat-rpc-server/Cargo.toml @@ -17,11 +17,11 @@ anyhow = "1" env_logger = { version = "0.10.0" } futures-lite = "1.13.0" log = "0.4" -serde_json = "1.0.99" +serde_json = "1.0.105" serde = { version = "1.0", features = ["derive"] } tokio = { version = "1.33.0", features = ["io-std"] } -tokio-util = "0.7.8" -yerpc = { version = "0.5.1", features = ["anyhow_expose"] } +tokio-util = "0.7.9" +yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] } [features] default = ["vendored"] diff --git a/deltachat-rpc-server/README.md b/deltachat-rpc-server/README.md index 2027072a5..be8c0c562 100644 --- a/deltachat-rpc-server/README.md +++ b/deltachat-rpc-server/README.md @@ -32,3 +32,6 @@ languages other than Rust, for example: 1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/ 2. Go: https://github.com/deltachat/deltachat-rpc-client-go/ + +Run `deltachat-rpc-server --version` to check the version of the server. +Run `deltachat-rpc-server --openrpc` to get [OpenRPC](https://open-rpc.org/) specification of the provided JSON-RPC API. diff --git a/deltachat-rpc-server/src/main.rs b/deltachat-rpc-server/src/main.rs index e3d0b0b20..4be58760b 100644 --- a/deltachat-rpc-server/src/main.rs +++ b/deltachat-rpc-server/src/main.rs @@ -10,6 +10,7 @@ use deltachat::constants::DC_VERSION_STR; use deltachat_jsonrpc::api::{Accounts, CommandApi}; use futures_lite::stream::StreamExt; use tokio::io::{self, AsyncBufReadExt, BufReader}; +use yerpc::RpcServer as _; #[cfg(target_family = "unix")] use tokio::signal::unix as signal_unix; @@ -39,6 +40,12 @@ async fn main_impl() -> Result<()> { } eprintln!("{}", &*DC_VERSION_STR); return Ok(()); + } else if first_arg.to_str() == Some("--openrpc") { + if let Some(arg) = args.next() { + return Err(anyhow!("Unrecognized argument {:?}", arg)); + } + println!("{}", CommandApi::openrpc_specification()?); + return Ok(()); } else { return Err(anyhow!("Unrecognized option {:?}", first_arg)); } @@ -56,7 +63,8 @@ async fn main_impl() -> Result<()> { let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string()); log::info!("Starting with accounts directory `{}`.", path); - let accounts = Accounts::new(PathBuf::from(&path)).await?; + let writable = true; + let accounts = Accounts::new(PathBuf::from(&path), writable).await?; log::info!("Creating JSON-RPC API."); let accounts = Arc::new(RwLock::new(accounts)); diff --git a/deny.toml b/deny.toml index 2231b94c1..22d06bab4 100644 --- a/deny.toml +++ b/deny.toml @@ -24,11 +24,10 @@ skip = [ { name = "digest", version = "<0.10" }, { name = "ed25519-dalek", version = "1.0.1" }, { name = "ed25519", version = "1.5.3" }, + { name = "fastrand", version = "1.9.0" }, { name = "getrandom", version = "<0.2" }, { name = "hashbrown", version = "<0.14.0" }, { name = "indexmap", version = "<2.0.0" }, - { name = "linux-raw-sys", version = "0.3.8" }, - { name = "num-derive", version = "0.3.3" }, { name = "pem-rfc7468", version = "0.6.0" }, { name = "pkcs8", version = "0.9.0" }, { name = "quick-error", version = "<2.0" }, @@ -36,8 +35,8 @@ skip = [ { name = "rand_core", version = "<0.6" }, { name = "rand", version = "<0.8" }, { name = "redox_syscall", version = "0.2.16" }, + { name = "regex-automata", version = "0.1.10" }, { name = "regex-syntax", version = "0.6.29" }, - { name = "rustix", version = "0.37.21" }, { name = "sec1", version = "0.3.0" }, { name = "sha2", version = "<0.10" }, { name = "signature", version = "1.6.4" }, @@ -53,11 +52,10 @@ skip = [ { name = "windows_i686_msvc", version = "<0.48" }, { name = "windows-sys", version = "<0.48" }, { name = "windows-targets", version = "<0.48" }, - { name = "windows_x86_64_gnullvm", version = "<0.48" }, { name = "windows", version = "0.32.0" }, + { name = "windows_x86_64_gnullvm", version = "<0.48" }, { name = "windows_x86_64_gnu", version = "<0.48" }, { name = "windows_x86_64_msvc", version = "<0.48" }, - { name = "winreg", version = "0.10.1" }, ] diff --git a/examples/simple.rs b/examples/simple.rs deleted file mode 100644 index cca50bb27..000000000 --- a/examples/simple.rs +++ /dev/null @@ -1,100 +0,0 @@ -use deltachat::chat::{self, ChatId}; -use deltachat::chatlist::*; -use deltachat::config; -use deltachat::contact::*; -use deltachat::context::*; -use deltachat::message::Message; -use deltachat::stock_str::StockStrings; -use deltachat::{EventType, Events}; -use tempfile::tempdir; - -fn cb(event: EventType) { - match event { - EventType::ConfigureProgress { progress, .. } => { - log::info!("progress: {}", progress); - } - EventType::Info(msg) => { - log::info!("{}", msg); - } - EventType::Warning(msg) => { - log::warn!("{}", msg); - } - EventType::Error(msg) => { - log::error!("{}", msg); - } - event => { - log::info!("{:?}", event); - } - } -} - -/// Run with `RUST_LOG=simple=info cargo run --release --example simple -- email pw`. -#[tokio::main] -async fn main() { - pretty_env_logger::try_init_timed().ok(); - - let dir = tempdir().unwrap(); - let dbfile = dir.path().join("db.sqlite"); - log::info!("creating database {:?}", dbfile); - let ctx = Context::new(&dbfile, 0, Events::new(), StockStrings::new()) - .await - .expect("Failed to create context"); - let info = ctx.get_info().await; - log::info!("info: {:#?}", info); - - let events = ctx.get_event_emitter(); - let events_spawn = tokio::task::spawn(async move { - while let Some(event) = events.recv().await { - cb(event.typ); - } - }); - - log::info!("configuring"); - let args = std::env::args().collect::>(); - assert_eq!(args.len(), 3, "requires email password"); - let email = args[1].clone(); - let pw = args[2].clone(); - ctx.set_config(config::Config::Addr, Some(&email)) - .await - .unwrap(); - ctx.set_config(config::Config::MailPw, Some(&pw)) - .await - .unwrap(); - - ctx.configure().await.unwrap(); - - log::info!("------ RUN ------"); - ctx.start_io().await; - log::info!("--- SENDING A MESSAGE ---"); - - let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com") - .await - .unwrap(); - let chat_id = ChatId::create_for_contact(&ctx, contact_id).await.unwrap(); - - for i in 0..1 { - log::info!("sending message {}", i); - chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {i}nth message!")) - .await - .unwrap(); - } - - // wait for the message to be sent out - tokio::time::sleep(std::time::Duration::from_secs(1)).await; - - log::info!("fetching chats.."); - let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap(); - - for i in 0..chats.len() { - let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap().unwrap()) - .await - .unwrap(); - log::info!("[{}] msg: {:?}", i, msg); - } - - log::info!("stopping"); - ctx.stop_io().await; - log::info!("closing"); - drop(ctx); - events_spawn.await.unwrap(); -} diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index 097b7ac9b..feb204237 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -177,13 +177,13 @@ dependencies = [ [[package]] name = "async-imap" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da93622739d458dd9a6abc1abf0e38e81965a5824a3b37f9500437c82a8bb572" +checksum = "b538b767cbf9c162a6c5795d4b932bd2c20ba10b5a91a94d2b2b6886c1dce6a8" dependencies = [ "async-channel", "base64 0.21.0", - "byte-pool", + "bytes", "chrono", "futures", "imap-proto", @@ -337,9 +337,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.0.2" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "487f1e0fcbe47deb8b0574e646def1c903389d95241dd1bbcc6ce4a715dfc0c1" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" [[package]] name = "blake3" @@ -529,16 +529,6 @@ version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" -[[package]] -name = "byte-pool" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2f1b21189f50b5625efa6227cf45e9d4cfdc2e73582df2b879e9689e78a7158" -dependencies = [ - "crossbeam-queue", - "stable_deref_trait", -] - [[package]] name = "bytemuck" version = "1.12.3" @@ -741,16 +731,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-queue" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - [[package]] name = "crossbeam-utils" version = "0.8.14" @@ -926,7 +906,7 @@ dependencies = [ [[package]] name = "deltachat" -version = "1.117.0" +version = "1.123.0" dependencies = [ "anyhow", "async-channel", @@ -943,6 +923,7 @@ dependencies = [ "encoded-words", "escaper", "fast-socks5", + "fd-lock", "format-flowed", "futures", "futures-lite", @@ -955,7 +936,7 @@ dependencies = [ "libc", "mailparse 0.14.0", "mime", - "num-derive", + "num-derive 0.4.0", "num-traits", "num_cpus", "once_cell", @@ -1429,14 +1410,14 @@ checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" [[package]] name = "enum-as-inner" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" dependencies = [ "heck", "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.15", ] [[package]] @@ -1464,6 +1445,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "errno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "errno-dragonfly" version = "0.1.2" @@ -1532,6 +1524,17 @@ dependencies = [ "instant", ] +[[package]] +name = "fd-lock" +version = "3.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef033ed5e9bad94e55838ca0ca906db0e043f517adda0c8b79c7a8c66c93c1b5" +dependencies = [ + "cfg-if", + "rustix 0.38.14", + "windows-sys 0.48.0", +] + [[package]] name = "ff" version = "0.12.1" @@ -1616,9 +1619,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -1843,18 +1846,15 @@ dependencies = [ [[package]] name = "heck" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.2.6" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -1951,7 +1951,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.7", "tokio", "tower-service", "tracing", @@ -2012,20 +2012,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.2.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "idna" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -2033,9 +2022,9 @@ dependencies = [ [[package]] name = "image" -version = "0.24.6" +version = "0.24.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527909aa81e20ac3a44803521443a765550f09b5130c2c2fa1ea59c2f8f50a3a" +checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" dependencies = [ "bytemuck", "byteorder", @@ -2100,10 +2089,10 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd302af1b90f2463a98fa5ad469fc212c8e3175a41c3068601bfa2727591c5be" dependencies = [ - "socket2", + "socket2 0.4.7", "widestring", "winapi", - "winreg", + "winreg 0.10.1", ] [[package]] @@ -2230,9 +2219,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.139" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libm" @@ -2279,6 +2268,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" +[[package]] +name = "linux-raw-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" + [[package]] name = "lock_api" version = "0.4.9" @@ -2341,15 +2336,9 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" dependencies = [ - "regex-automata", + "regex-automata 0.1.10", ] -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "md-5" version = "0.10.5" @@ -2367,9 +2356,9 @@ checksum = "df39d232f5c40b0891c10216992c2f250c054105cb1e56f0fc9032db6203ecc1" [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "mime" @@ -2394,14 +2383,13 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.5" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d732bc30207a6423068df043e3d02e0735b155ad7ce1a6f76fe2baa5b158de" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] @@ -2555,6 +2543,17 @@ dependencies = [ "syn 1.0.107", ] +[[package]] +name = "num-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e6a0fd4f737c707bd9086cc16c925f294943eb62eb71499e9fd4cf71f8b9f4e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.15", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -2599,9 +2598,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ "hermit-abi", "libc", @@ -2854,7 +2853,7 @@ dependencies = [ "md-5", "nom", "num-bigint-dig", - "num-derive", + "num-derive 0.3.3", "num-traits", "p256 0.13.1", "p384 0.13.0", @@ -2894,9 +2893,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.9" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -3087,9 +3086,9 @@ checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] name = "quick-xml" -version = "0.28.2" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce5e73202a820a31f8a0ee32ada5e21029c81fd9e3ebf668a40832e4219d9d1" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" dependencies = [ "memchr", ] @@ -3114,9 +3113,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.9.2" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72ef4ced82a24bb281af338b9e8f94429b6eca01b4e66d899f40031f074e74c9" +checksum = "c956be1b23f4261676aed05a0046e204e8a6836e50203902683a718af0797989" dependencies = [ "bytes", "rand 0.8.5", @@ -3139,7 +3138,7 @@ checksum = "641538578b21f5e5c8ea733b736895576d0fe329bb883b937db6f4d163dbaaf4" dependencies = [ "libc", "quinn-proto", - "socket2", + "socket2 0.4.7", "tracing", "windows-sys 0.42.0", ] @@ -3268,13 +3267,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.2", + "regex-automata 0.3.8", + "regex-syntax 0.7.5", ] [[package]] @@ -3286,6 +3286,17 @@ dependencies = [ "regex-syntax 0.6.28", ] +[[package]] +name = "regex-automata" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.7.5", +] + [[package]] name = "regex-syntax" version = "0.6.28" @@ -3294,15 +3305,15 @@ checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "regex-syntax" -version = "0.7.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ "base64 0.21.0", "bytes", @@ -3332,7 +3343,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", + "winreg 0.50.0", ] [[package]] @@ -3438,7 +3449,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "549b9d036d571d42e6e85d1c1425e2ac83491075078ca9a15be021c56b1641f2" dependencies = [ - "bitflags 2.0.2", + "bitflags 2.4.0", "fallible-iterator", "fallible-streaming-iterator", "hashlink", @@ -3489,13 +3500,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" dependencies = [ "bitflags 1.3.2", - "errno", + "errno 0.2.8", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.1.4", "windows-sys 0.42.0", ] +[[package]] +name = "rustix" +version = "0.38.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" +dependencies = [ + "bitflags 2.4.0", + "errno 0.3.3", + "libc", + "linux-raw-sys 0.4.7", + "windows-sys 0.48.0", +] + [[package]] name = "rustls" version = "0.20.8" @@ -3557,9 +3581,9 @@ dependencies = [ [[package]] name = "sanitize-filename" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c502bdb638f1396509467cb0580ef3b29aa2a45c5d43e5d84928241280296c" +checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603" dependencies = [ "lazy_static", "regex", @@ -3855,6 +3879,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "spin" version = "0.5.2" @@ -3919,12 +3953,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - [[package]] name = "stop-token" version = "0.7.0" @@ -3945,21 +3973,21 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" -version = "0.24.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" [[package]] name = "strum_macros" -version = "0.24.3" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +checksum = "ad8d03b598d3d0fff69bf533ee3ef19b8eeb342729596df84bcc7e1f96ec4059" dependencies = [ "heck", "proc-macro2", "quote", "rustversion", - "syn 1.0.107", + "syn 2.0.15", ] [[package]] @@ -4038,7 +4066,7 @@ dependencies = [ "cfg-if", "fastrand", "redox_syscall", - "rustix", + "rustix 0.36.7", "windows-sys 0.42.0", ] @@ -4147,22 +4175,21 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.25.0" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", + "backtrace", "bytes", "libc", - "memchr", "mio", "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.4", "tokio-macros", - "windows-sys 0.42.0", + "windows-sys 0.48.0", ] [[package]] @@ -4177,13 +4204,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.8.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 1.0.107", + "syn 2.0.15", ] [[package]] @@ -4239,9 +4266,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes", "futures-core", @@ -4365,9 +4392,9 @@ dependencies = [ [[package]] name = "trust-dns-proto" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" +checksum = "0dc775440033cb114085f6f2437682b194fa7546466024b1037e82a48a052a69" dependencies = [ "async-trait", "cfg-if", @@ -4376,9 +4403,9 @@ dependencies = [ "futures-channel", "futures-io", "futures-util", - "idna 0.2.3", + "idna", "ipnet", - "lazy_static", + "once_cell", "rand 0.8.5", "smallvec", "thiserror", @@ -4390,16 +4417,17 @@ dependencies = [ [[package]] name = "trust-dns-resolver" -version = "0.22.0" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" +checksum = "2dff7aed33ef3e8bf2c9966fccdfed93f93d46f432282ea875cd66faabc6ef2f" dependencies = [ "cfg-if", "futures-util", "ipconfig", - "lazy_static", "lru-cache", + "once_cell", "parking_lot", + "rand 0.8.5", "resolv-conf", "smallvec", "thiserror", @@ -4431,9 +4459,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicode-bidi" -version = "0.3.8" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" @@ -4480,12 +4508,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.3.1" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", - "idna 0.3.0", + "idna", "percent-encoding", ] @@ -4648,9 +4676,9 @@ dependencies = [ [[package]] name = "webpki" -version = "0.22.0" +version = "0.22.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f" dependencies = [ "ring", "untrusted", @@ -4731,21 +4759,51 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm", + "windows_aarch64_gnullvm 0.42.0", "windows_aarch64_msvc 0.42.0", "windows_i686_gnu 0.42.0", "windows_i686_msvc 0.42.0", "windows_x86_64_gnu 0.42.0", - "windows_x86_64_gnullvm", + "windows_x86_64_gnullvm 0.42.0", "windows_x86_64_msvc 0.42.0", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_msvc" version = "0.32.0" @@ -4764,6 +4822,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_i686_gnu" version = "0.32.0" @@ -4782,6 +4846,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_msvc" version = "0.32.0" @@ -4800,6 +4870,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_x86_64_gnu" version = "0.32.0" @@ -4818,12 +4894,24 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_msvc" version = "0.32.0" @@ -4842,6 +4930,12 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "winreg" version = "0.10.1" @@ -4851,6 +4945,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "x25519-dalek" version = "2.0.0-pre.1" diff --git a/node/constants.js b/node/constants.js index dcbe52daf..b9c990c77 100644 --- a/node/constants.js +++ b/node/constants.js @@ -159,6 +159,8 @@ module.exports = { DC_STR_BROADCAST_LIST: 115, DC_STR_CANNOT_LOGIN: 60, DC_STR_CANTDECRYPT_MSG_BODY: 29, + DC_STR_CHAT_PROTECTION_DISABLED: 171, + DC_STR_CHAT_PROTECTION_ENABLED: 170, DC_STR_CONFIGURATION_FAILED: 84, DC_STR_CONNECTED: 107, DC_STR_CONNTECTING: 108, @@ -244,12 +246,6 @@ module.exports = { DC_STR_OUTGOING_MESSAGES: 104, DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99, DC_STR_PART_OF_TOTAL_USED: 116, - DC_STR_PROTECTION_DISABLED: 89, - DC_STR_PROTECTION_DISABLED_BY_OTHER: 161, - DC_STR_PROTECTION_DISABLED_BY_YOU: 160, - DC_STR_PROTECTION_ENABLED: 88, - DC_STR_PROTECTION_ENABLED_BY_OTHER: 159, - DC_STR_PROTECTION_ENABLED_BY_YOU: 158, DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98, DC_STR_READRCPT: 31, DC_STR_READRCPT_MAILBODY: 32, diff --git a/node/lib/constants.ts b/node/lib/constants.ts index f97e44426..6cc08f144 100644 --- a/node/lib/constants.ts +++ b/node/lib/constants.ts @@ -159,6 +159,8 @@ export enum C { DC_STR_BROADCAST_LIST = 115, DC_STR_CANNOT_LOGIN = 60, DC_STR_CANTDECRYPT_MSG_BODY = 29, + DC_STR_CHAT_PROTECTION_DISABLED = 171, + DC_STR_CHAT_PROTECTION_ENABLED = 170, DC_STR_CONFIGURATION_FAILED = 84, DC_STR_CONNECTED = 107, DC_STR_CONNTECTING = 108, @@ -244,12 +246,6 @@ export enum C { DC_STR_OUTGOING_MESSAGES = 104, DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99, DC_STR_PART_OF_TOTAL_USED = 116, - DC_STR_PROTECTION_DISABLED = 89, - DC_STR_PROTECTION_DISABLED_BY_OTHER = 161, - DC_STR_PROTECTION_DISABLED_BY_YOU = 160, - DC_STR_PROTECTION_ENABLED = 88, - DC_STR_PROTECTION_ENABLED_BY_OTHER = 159, - DC_STR_PROTECTION_ENABLED_BY_YOU = 158, DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98, DC_STR_READRCPT = 31, DC_STR_READRCPT_MAILBODY = 32, diff --git a/node/lib/context.ts b/node/lib/context.ts index a9cf9c687..b292e9e35 100644 --- a/node/lib/context.ts +++ b/node/lib/context.ts @@ -699,23 +699,6 @@ export class Context extends EventEmitter { ) } - /** - * - * @param chatId - * @param protect - * @returns success boolean - */ - setChatProtection(chatId: number, protect: boolean) { - debug(`setChatProtection ${chatId} ${protect}`) - return Boolean( - binding.dcn_set_chat_protection( - this.dcn_context, - Number(chatId), - protect ? 1 : 0 - ) - ) - } - getChatEphemeralTimer(chatId: number): number { debug(`getChatEphemeralTimer ${chatId}`) return binding.dcn_get_chat_ephemeral_timer( diff --git a/node/lib/deltachat.ts b/node/lib/deltachat.ts index 2526b9378..dbf002e46 100644 --- a/node/lib/deltachat.ts +++ b/node/lib/deltachat.ts @@ -21,12 +21,15 @@ export class AccountManager extends EventEmitter { accountDir: string jsonRpcStarted = false - constructor(cwd: string, os = 'deltachat-node') { + constructor(cwd: string, writable = true) { super() debug('DeltaChat constructor') this.accountDir = cwd - this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir) + this.dcn_accounts = binding.dcn_accounts_new( + this.accountDir, + writable ? 1 : 0 + ) } getAllAccountIds() { diff --git a/node/src/module.c b/node/src/module.c index 3d3045017..5c675020e 100644 --- a/node/src/module.c +++ b/node/src/module.c @@ -1399,18 +1399,6 @@ NAPI_METHOD(dcn_set_chat_name) { NAPI_RETURN_INT32(result); } -NAPI_METHOD(dcn_set_chat_protection) { - NAPI_ARGV(3); - NAPI_DCN_CONTEXT(); - NAPI_ARGV_UINT32(chat_id, 1); - NAPI_ARGV_INT32(protect, 1); - - int result = dc_set_chat_protection(dcn_context->dc_context, - chat_id, - protect); - NAPI_RETURN_INT32(result); -} - NAPI_METHOD(dcn_get_chat_ephemeral_timer) { NAPI_ARGV(2); NAPI_DCN_CONTEXT(); @@ -2915,8 +2903,8 @@ NAPI_METHOD(dcn_msg_get_webxdc_blob){ NAPI_METHOD(dcn_accounts_new) { NAPI_ARGV(2); - NAPI_ARGV_UTF8_MALLOC(os_name, 0); - NAPI_ARGV_UTF8_MALLOC(dir, 1); + NAPI_ARGV_UTF8_MALLOC(dir, 0); + NAPI_ARGV_INT32(writable, 1); TRACE("calling.."); dcn_accounts_t* dcn_accounts = calloc(1, sizeof(dcn_accounts_t)); @@ -2925,7 +2913,7 @@ NAPI_METHOD(dcn_accounts_new) { } - dcn_accounts->dc_accounts = dc_accounts_new(os_name, dir); + dcn_accounts->dc_accounts = dc_accounts_new(dir, writable); napi_value result; NAPI_STATUS_THROWS(napi_create_external(env, dcn_accounts, @@ -3491,7 +3479,6 @@ NAPI_INIT() { NAPI_EXPORT_FUNCTION(dcn_send_msg); NAPI_EXPORT_FUNCTION(dcn_send_videochat_invitation); NAPI_EXPORT_FUNCTION(dcn_set_chat_name); - NAPI_EXPORT_FUNCTION(dcn_set_chat_protection); NAPI_EXPORT_FUNCTION(dcn_get_chat_ephemeral_timer); NAPI_EXPORT_FUNCTION(dcn_set_chat_ephemeral_timer); NAPI_EXPORT_FUNCTION(dcn_set_chat_profile_image); diff --git a/python/tests/test_0_complex_or_slow.py b/python/tests/test_0_complex_or_slow.py index 857e1645d..75c554a6d 100644 --- a/python/tests/test_0_complex_or_slow.py +++ b/python/tests/test_0_complex_or_slow.py @@ -137,6 +137,7 @@ def test_qr_verified_group_and_chatting(acfactory, lp): lp.sec("ac2: read member added message") msg = ac2._evtracker.wait_next_incoming_message() assert msg.is_encrypted() + assert msg.is_system_message() assert "added" in msg.text.lower() lp.sec("ac1: send message") @@ -182,8 +183,9 @@ def test_qr_verified_group_and_chatting(acfactory, lp): lp.sec("ac2: send message and let ac3 read it") chat2.send_text("hi") - # Skip system message about added member - ac3._evtracker.wait_next_incoming_message() + # System message about the added member. + msg = ac3._evtracker.wait_next_incoming_message() + assert msg.is_system_message() msg = ac3._evtracker.wait_next_incoming_message() assert msg.text == "hi" assert msg.is_encrypted() @@ -524,7 +526,8 @@ def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp): lp.sec("ac2: sending message") # Message can be sent only after a receipt of "vg-member-added" message. Just wait for # "Member Me () added by ." message. - ac2._evtracker.wait_next_incoming_message() + msg_in = ac2._evtracker.wait_next_incoming_message() + assert msg_in.is_system_message() msg_out = chat2.send_text("hello") lp.sec("ac1: receiving message") @@ -633,7 +636,7 @@ def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp): ac2_offl.start_io() msg_in = ac2_offl._evtracker.wait_next_incoming_message() assert not msg_in.is_system_message() - assert msg_in.text.startswith("[Sender of this message is not verified:") + assert msg_in.text.startswith("[The message was sent with non-verified encryption") ac2_offl_ac1_contact = msg_in.get_sender_contact() assert ac2_offl_ac1_contact.addr == ac1.get_config("addr") assert not ac2_offl_ac1_contact.is_verified() diff --git a/python/tests/test_1_online.py b/python/tests/test_1_online.py index bf5914a77..431463952 100644 --- a/python/tests/test_1_online.py +++ b/python/tests/test_1_online.py @@ -9,7 +9,7 @@ import pytest from imap_tools import AND, U import deltachat as dc -from deltachat import account_hookimpl, Message +from deltachat import account_hookimpl, Message, Chat from deltachat.tracker import ImexTracker @@ -1467,13 +1467,18 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path): path = tmp_path / "large" path.write_bytes(os.urandom(download_limit + 1)) msgs.append(chat.send_file(str(path))) - - lp.sec("sending a reaction to the large message from ac1 to ac2") - react_str = "\N{THUMBS UP SIGN}" - msgs.append(msgs[-1].send_reaction(react_str)) - for m in msgs: ac1._evtracker.wait_msg_delivered(m) + + lp.sec("sending a reaction to the large message from ac1 to ac2") + # TODO: Find the reason of an occasional message reordering on the server (so that the reaction + # has a lower UID than the previous message). W/a is to sleep for some time to let the reaction + # have a later INTERNALDATE. + time.sleep(1.1) + react_str = "\N{THUMBS UP SIGN}" + msgs.append(msgs[-1].send_reaction(react_str)) + ac1._evtracker.wait_msg_delivered(msgs[-1]) + ac2.start_io() lp.sec("wait for ac2 to receive a reaction") @@ -1505,7 +1510,7 @@ def test_reactions_for_a_reordering_move(acfactory, lp): ac1._evtracker.wait_msg_delivered(msg1) # It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct # order by DC, and most (if not all) mail servers provide only seconds precision. - time.sleep(2) + time.sleep(1.1) react_str = "\N{THUMBS UP SIGN}" ac1._evtracker.wait_msg_delivered(msg1.send_reaction(react_str)) @@ -1664,8 +1669,12 @@ def test_qr_setup_contact(acfactory, lp): ac1._evtracker.wait_securejoin_inviter_progress(1000) -def test_qr_join_chat(acfactory, lp): +@pytest.mark.parametrize("verified_one_on_one_chats", [0, 1]) +def test_qr_join_chat(acfactory, lp, verified_one_on_one_chats): ac1, ac2 = acfactory.get_online_accounts(2) + ac1.set_config("verified_one_on_one_chats", verified_one_on_one_chats) + ac2.set_config("verified_one_on_one_chats", verified_one_on_one_chats) + lp.sec("ac1: create QR code and let ac2 scan it, starting the securejoin") chat = ac1.create_group_chat("hello") qr = chat.get_join_qr() @@ -1678,6 +1687,17 @@ def test_qr_join_chat(acfactory, lp): ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED") ac1._evtracker.wait_securejoin_inviter_progress(1000) + msg = ac2._evtracker.wait_next_incoming_message() + assert msg.text == "Member Me ({}) added by {}.".format(ac2.get_config("addr"), ac1.get_config("addr")) + + # ac1 reloads the chat. + chat = Chat(chat.account, chat.id) + assert not chat.is_protected() + + # ac2 reloads the chat. + ch = Chat(ch.account, ch.id) + assert not ch.is_protected() + def test_qr_new_group_unblocked(acfactory, lp): """Regression test for a bug intoduced in core v1.113.0. diff --git a/python/tests/test_4_lowlevel.py b/python/tests/test_4_lowlevel.py index 1695f943d..23ef3dbe3 100644 --- a/python/tests/test_4_lowlevel.py +++ b/python/tests/test_4_lowlevel.py @@ -222,8 +222,9 @@ def test_logged_ac_process_ffi_failure(acfactory): def test_jsonrpc_blocking_call(tmp_path): accounts_fname = tmp_path / "accounts" + writable = True accounts = ffi.gc( - lib.dc_accounts_new(ffi.NULL, str(accounts_fname).encode("ascii")), + lib.dc_accounts_new(str(accounts_fname).encode("ascii"), writable), lib.dc_accounts_unref, ) jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref) diff --git a/python/tox.ini b/python/tox.ini index a68b4e729..ed6069dec 100644 --- a/python/tox.ini +++ b/python/tox.ini @@ -62,7 +62,8 @@ commands = [testenv:doc] changedir=doc deps = - sphinx +# Pinned due to incompatibility of breathe with sphinx 7.2: + sphinx<=7.1.2 breathe commands = sphinx-build -Q -w toxdoc-warnings.log -b html . _build/html diff --git a/scripts/coredeps/Dockerfile b/scripts/coredeps/Dockerfile index a6ceed532..55b90d9f6 100644 --- a/scripts/coredeps/Dockerfile +++ b/scripts/coredeps/Dockerfile @@ -6,3 +6,4 @@ FROM $BASEIMAGE RUN pipx install tox COPY install-rust.sh /scripts/ RUN /scripts/install-rust.sh +RUN if command -v yum; then yum install -y perl-IPC-Cmd; fi diff --git a/src/accounts.rs b/src/accounts.rs index 77770049f..7cbe1cea4 100644 --- a/src/accounts.rs +++ b/src/accounts.rs @@ -7,6 +7,9 @@ use anyhow::{ensure, Context as _, Result}; use serde::{Deserialize, Serialize}; use tokio::fs; use tokio::io::AsyncWriteExt; +use tokio::sync::oneshot; +use tokio::task::JoinHandle; +use tokio::time::{sleep, Duration}; use uuid::Uuid; use crate::context::Context; @@ -33,16 +36,16 @@ pub struct Accounts { impl Accounts { /// Loads or creates an accounts folder at the given `dir`. - pub async fn new(dir: PathBuf) -> Result { - if !dir.exists() { + pub async fn new(dir: PathBuf, writable: bool) -> Result { + if writable && !dir.exists() { Accounts::create(&dir).await?; } - Accounts::open(dir).await + Accounts::open(dir, writable).await } /// Creates a new default structure. - pub async fn create(dir: &Path) -> Result<()> { + async fn create(dir: &Path) -> Result<()> { fs::create_dir_all(dir) .await .context("failed to create folder")?; @@ -54,13 +57,13 @@ impl Accounts { /// Opens an existing accounts structure. Will error if the folder doesn't exist, /// no account exists and no config exists. - pub async fn open(dir: PathBuf) -> Result { + async fn open(dir: PathBuf, writable: bool) -> Result { ensure!(dir.exists(), "directory does not exist"); let config_file = dir.join(CONFIG_NAME); ensure!(config_file.exists(), "{:?} does not exist", config_file); - let config = Config::from_file(config_file) + let config = Config::from_file(config_file, writable) .await .context("failed to load accounts config")?; let events = Events::new(); @@ -298,14 +301,20 @@ impl Accounts { /// Configuration file name. const CONFIG_NAME: &str = "accounts.toml"; +/// Lockfile name. +const LOCKFILE_NAME: &str = "accounts.lock"; + /// Database file name. const DB_NAME: &str = "dc.db"; /// Account manager configuration file. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug)] struct Config { file: PathBuf, inner: InnerConfig, + // We lock the lockfile in the Config constructors to protect also from having multiple Config + // objects for the same config file. + lock_task: Option>>, } /// Account manager configuration file contents. @@ -319,17 +328,74 @@ struct InnerConfig { pub accounts: Vec, } +impl Drop for Config { + fn drop(&mut self) { + if let Some(lock_task) = self.lock_task.take() { + lock_task.abort(); + } + } +} + impl Config { - /// Creates a new configuration file in the given account manager directory. - pub async fn new(dir: &Path) -> Result { + /// Creates a new Config for `file`, but doesn't open/sync it. + async fn new_nosync(file: PathBuf, lock: bool) -> Result { + let dir = file.parent().context("Cannot get config file directory")?; let inner = InnerConfig { accounts: Vec::new(), selected_account: 0, next_id: 1, }; - let file = dir.join(CONFIG_NAME); - let mut cfg = Self { file, inner }; + if !lock { + let cfg = Self { + file, + inner, + lock_task: None, + }; + return Ok(cfg); + } + let lockfile = dir.join(LOCKFILE_NAME); + let mut lock = fd_lock::RwLock::new(fs::File::create(lockfile).await?); + let (locked_tx, locked_rx) = oneshot::channel(); + let lock_task: JoinHandle> = tokio::spawn(async move { + let mut timeout = Duration::from_millis(100); + let _guard = loop { + match lock.try_write() { + Ok(guard) => break Ok(guard), + Err(err) => { + if timeout.as_millis() > 1600 { + break Err(err); + } + // We need to wait for the previous lock_task to be aborted thus unlocking + // the lockfile. We don't open configs for writing often outside of the + // tests, so this adds delays to the tests, but otherwise ok. + sleep(timeout).await; + if err.kind() == std::io::ErrorKind::WouldBlock { + timeout *= 2; + } + } + } + }?; + locked_tx + .send(()) + .ok() + .context("Cannot notify about lockfile locking")?; + let (_tx, rx) = oneshot::channel(); + rx.await?; + Ok(()) + }); + let cfg = Self { + file, + inner, + lock_task: Some(lock_task), + }; + locked_rx.await?; + Ok(cfg) + } + /// Creates a new configuration file in the given account manager directory. + pub async fn new(dir: &Path) -> Result { + let lock = true; + let mut cfg = Self::new_nosync(dir.join(CONFIG_NAME), lock).await?; cfg.sync().await?; Ok(cfg) @@ -339,6 +405,11 @@ impl Config { /// Takes a mutable reference because the saved file is a part of the `Config` state. This /// protects from parallel calls resulting to a wrong file contents. async fn sync(&mut self) -> Result<()> { + ensure!(!self + .lock_task + .as_ref() + .context("Config is read-only")? + .is_finished()); let tmp_path = self.file.with_extension("toml.tmp"); let mut file = fs::File::create(&tmp_path) .await @@ -357,24 +428,28 @@ impl Config { } /// Read a configuration from the given file into memory. - pub async fn from_file(file: PathBuf) -> Result { - let dir = file.parent().context("can't get config file directory")?; - let bytes = fs::read(&file).await.context("failed to read file")?; + pub async fn from_file(file: PathBuf, writable: bool) -> Result { + let dir = file + .parent() + .context("Cannot get config file directory")? + .to_path_buf(); + let mut config = Self::new_nosync(file, writable).await?; + let bytes = fs::read(&config.file) + .await + .context("Failed to read file")?; let s = std::str::from_utf8(&bytes)?; - let mut inner: InnerConfig = toml::from_str(s).context("failed to parse config")?; + config.inner = toml::from_str(s).context("Failed to parse config")?; // Previous versions of the core stored absolute paths in account config. // Convert them to relative paths. let mut modified = false; - for account in &mut inner.accounts { - if let Ok(new_dir) = account.dir.strip_prefix(dir) { + for account in &mut config.inner.accounts { + if let Ok(new_dir) = account.dir.strip_prefix(&dir) { account.dir = new_dir.to_path_buf(); modified = true; } } - - let mut config = Self { file, inner }; - if modified { + if modified && writable { config.sync().await?; } @@ -518,26 +593,44 @@ mod tests { let p: PathBuf = dir.path().join("accounts1"); { - let mut accounts = Accounts::new(p.clone()).await.unwrap(); + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await.unwrap(); accounts.add_account().await.unwrap(); assert_eq!(accounts.accounts.len(), 1); assert_eq!(accounts.config.get_selected_account(), 1); } - { - let accounts = Accounts::open(p).await.unwrap(); + for writable in [true, false] { + let accounts = Accounts::new(p.clone(), writable).await.unwrap(); assert_eq!(accounts.accounts.len(), 1); assert_eq!(accounts.config.get_selected_account(), 1); } } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_account_new_open_conflict() { + let dir = tempfile::tempdir().unwrap(); + let p: PathBuf = dir.path().join("accounts"); + let writable = true; + let _accounts = Accounts::new(p.clone(), writable).await.unwrap(); + + let writable = true; + assert!(Accounts::new(p.clone(), writable).await.is_err()); + + let writable = false; + let accounts = Accounts::new(p, writable).await.unwrap(); + assert_eq!(accounts.accounts.len(), 0); + assert_eq!(accounts.config.get_selected_account(), 0); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_account_new_add_remove() { let dir = tempfile::tempdir().unwrap(); let p: PathBuf = dir.path().join("accounts"); - let mut accounts = Accounts::new(p.clone()).await.unwrap(); + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await.unwrap(); assert_eq!(accounts.accounts.len(), 0); assert_eq!(accounts.config.get_selected_account(), 0); @@ -564,7 +657,8 @@ mod tests { let dir = tempfile::tempdir()?; let p: PathBuf = dir.path().join("accounts"); - let mut accounts = Accounts::new(p.clone()).await?; + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await?; assert!(accounts.get_selected_account().is_none()); assert_eq!(accounts.config.get_selected_account(), 0); @@ -585,7 +679,8 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let p: PathBuf = dir.path().join("accounts"); - let mut accounts = Accounts::new(p.clone()).await.unwrap(); + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await.unwrap(); assert_eq!(accounts.accounts.len(), 0); assert_eq!(accounts.config.get_selected_account(), 0); @@ -622,7 +717,8 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let p: PathBuf = dir.path().join("accounts"); - let mut accounts = Accounts::new(p.clone()).await.unwrap(); + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await.unwrap(); for expected_id in 1..10 { let id = accounts.add_account().await.unwrap(); @@ -642,7 +738,8 @@ mod tests { let dummy_accounts = 10; let (id0, id1, id2) = { - let mut accounts = Accounts::new(p.clone()).await?; + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await?; accounts.add_account().await?; let ids = accounts.get_all(); assert_eq!(ids.len(), 1); @@ -677,7 +774,8 @@ mod tests { assert!(id2 > id1 + dummy_accounts); let (id0_reopened, id1_reopened, id2_reopened) = { - let accounts = Accounts::new(p.clone()).await?; + let writable = false; + let accounts = Accounts::new(p.clone(), writable).await?; let ctx = accounts.get_selected_account().unwrap(); assert_eq!( ctx.get_config(crate::config::Config::Addr).await?, @@ -722,7 +820,8 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let p: PathBuf = dir.path().join("accounts"); - let accounts = Accounts::new(p.clone()).await?; + let writable = true; + let accounts = Accounts::new(p.clone(), writable).await?; // Make sure there are no accounts. assert_eq!(accounts.accounts.len(), 0); @@ -748,7 +847,8 @@ mod tests { let dir = tempfile::tempdir().context("failed to create tempdir")?; let p: PathBuf = dir.path().join("accounts"); - let mut accounts = Accounts::new(p.clone()) + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable) .await .context("failed to create accounts manager")?; @@ -768,7 +868,8 @@ mod tests { assert!(passphrase_set_success); drop(accounts); - let accounts = Accounts::new(p.clone()) + let writable = false; + let accounts = Accounts::new(p.clone(), writable) .await .context("failed to create second accounts manager")?; let account = accounts @@ -792,7 +893,8 @@ mod tests { let dir = tempfile::tempdir().unwrap(); let p: PathBuf = dir.path().join("accounts"); - let mut accounts = Accounts::new(p.clone()).await?; + let writable = true; + let mut accounts = Accounts::new(p.clone(), writable).await?; accounts.add_account().await?; accounts.add_account().await?; diff --git a/src/chat.rs b/src/chat.rs index 378a19ba4..b6d593822 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -27,6 +27,7 @@ use crate::download::DownloadState; use crate::ephemeral::Timer as EphemeralTimer; use crate::events::EventType; use crate::html::new_html_mimepart; +use crate::location; use crate::message::{self, Message, MessageState, MsgId, Viewtype}; use crate::mimefactory::MimeFactory; use crate::mimeparser::SystemMessage; @@ -35,6 +36,7 @@ use crate::peerstate::{Peerstate, PeerstateVerifiedStatus}; use crate::receive_imf::ReceivedMsg; use crate::scheduler::InterruptInfo; use crate::smtp::send_msg_to_smtp; +use crate::sql; use crate::stock_str; use crate::tools::{ buf_compress, create_id, create_outgoing_rfc724_mid, create_smeared_timestamp, @@ -42,7 +44,6 @@ use crate::tools::{ strip_rtlo_characters, time, IsNoneOrEmpty, }; use crate::webxdc::WEBXDC_SUFFIX; -use crate::{location, sql}; /// An chat item, such as a message or a marker. #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -88,6 +89,14 @@ pub enum ProtectionStatus { /// /// All members of the chat must be verified. Protected = 1, + + /// The chat was protected, but now a new message came in + /// which was not encrypted / signed correctly. + /// The user has to confirm that this is OK. + /// + /// We only do this in 1:1 chats; in group chats, the chat just + /// stays protected. + ProtectionBroken = 3, // `2` was never used as a value. } /// The reason why messages cannot be sent to the chat. @@ -104,6 +113,10 @@ pub(crate) enum CantSendReason { /// The chat is a contact request, it needs to be accepted before sending a message. ContactRequest, + /// The chat was protected, but now a new message came in + /// which was not encrypted / signed correctly. + ProtectionBroken, + /// Mailing list without known List-Post header. ReadOnlyMailingList, @@ -120,6 +133,10 @@ impl fmt::Display for CantSendReason { f, "contact request chat should be accepted before sending messages" ), + Self::ProtectionBroken => write!( + f, + "accept that the encryption isn't verified anymore before sending messages" + ), Self::ReadOnlyMailingList => { write!(f, "mailing list does not have a know post address") } @@ -272,6 +289,7 @@ impl ChatId { param: Option, ) -> Result { let grpname = strip_rtlo_characters(grpname); + let smeared_time = create_smeared_timestamp(context); let row_id = context.sql.insert( "INSERT INTO chats (type, name, grpid, blocked, created_timestamp, protected, param) VALUES(?, ?, ?, ?, ?, ?, ?);", @@ -280,13 +298,20 @@ impl ChatId { &grpname, grpid, create_blocked, - create_smeared_timestamp(context), + smeared_time, create_protected, param.unwrap_or_default(), ), ).await?; let chat_id = ChatId::new(u32::try_from(row_id)?); + + if create_protected == ProtectionStatus::Protected { + chat_id + .add_protection_msg(context, ProtectionStatus::Protected, None, smeared_time) + .await?; + } + info!( context, "Created group/mailinglist '{}' grpid={} as {}, blocked={}.", @@ -334,7 +359,7 @@ impl ChatId { let chat = Chat::load_from_db(context, self).await?; match chat.typ { - Chattype::Undefined | Chattype::Broadcast => { + Chattype::Broadcast => { bail!("Can't block chat of type {:?}", chat.typ) } Chattype::Single => { @@ -375,7 +400,16 @@ impl ChatId { let chat = Chat::load_from_db(context, self).await?; match chat.typ { - Chattype::Undefined => bail!("Can't accept chat of undefined chattype"), + Chattype::Single + if chat.blocked == Blocked::Not + && chat.protected == ProtectionStatus::ProtectionBroken => + { + // The protection was broken, then the user clicked 'Accept'/'OK', + // so, now we want to set the status to Unprotected again: + chat.id + .inner_set_protection(context, ProtectionStatus::Unprotected) + .await?; + } Chattype::Single | Chattype::Group | Chattype::Broadcast => { // User has "created a chat" with all these contacts. // @@ -402,20 +436,19 @@ impl ChatId { /// Sets protection without sending a message. /// - /// Used when a message arrives indicating that someone else has - /// changed the protection value for a chat. + /// Returns whether the protection status was actually modified. pub(crate) async fn inner_set_protection( self, context: &Context, protect: ProtectionStatus, - ) -> Result<()> { - ensure!(!self.is_special(), "Invalid chat-id."); + ) -> Result { + ensure!(!self.is_special(), "Invalid chat-id {self}."); let chat = Chat::load_from_db(context, self).await?; if protect == chat.protected { info!(context, "Protection status unchanged for {}.", self); - return Ok(()); + return Ok(false); } match protect { @@ -430,9 +463,8 @@ impl ChatId { } } Chattype::Mailinglist => bail!("Cannot protect mailing lists"), - Chattype::Undefined => bail!("Undefined group type"), }, - ProtectionStatus::Unprotected => {} + ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => {} }; context @@ -445,68 +477,58 @@ impl ChatId { // make sure, the receivers will get all keys self.reset_gossiped_timestamp(context).await?; - Ok(()) + Ok(true) } - /// Send protected status message to the chat. + /// Adds an info message to the chat, telling the user that the protection status changed. /// - /// This sends the message with the protected status change to the chat, - /// notifying the user on this device as well as the other users in the chat. + /// Params: /// - /// If `promote` is false this means, the message must not be sent out - /// and only a local info message should be added to the chat. - /// This is used when protection is enabled implicitly or when a chat is not yet promoted. + /// * `contact_id`: In a 1:1 chat, pass the chat partner's contact id. + /// * `timestamp_sort` is used as the timestamp of the added message + /// and should be the timestamp of the change happening. pub(crate) async fn add_protection_msg( self, context: &Context, protect: ProtectionStatus, - promote: bool, - from_id: ContactId, + contact_id: Option, + timestamp_sort: i64, ) -> Result<()> { - let text = context.stock_protection_msg(protect, from_id).await; + let text = context.stock_protection_msg(protect, contact_id).await; let cmd = match protect { ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled, ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled, + ProtectionStatus::ProtectionBroken => SystemMessage::ChatProtectionDisabled, }; - - if promote { - let mut msg = Message { - viewtype: Viewtype::Text, - text, - ..Default::default() - }; - msg.param.set_cmd(cmd); - send_msg(context, self, &mut msg).await?; - } else { - add_info_msg_with_cmd( - context, - self, - &text, - cmd, - create_smeared_timestamp(context), - None, - None, - None, - ) - .await?; - } + add_info_msg_with_cmd(context, self, &text, cmd, timestamp_sort, None, None, None).await?; Ok(()) } /// Sets protection and sends or adds a message. - pub async fn set_protection(self, context: &Context, protect: ProtectionStatus) -> Result<()> { - ensure!(!self.is_special(), "set protection: invalid chat-id."); - - let chat = Chat::load_from_db(context, self).await?; - - if let Err(e) = self.inner_set_protection(context, protect).await { - error!(context, "Cannot set protection: {e:#}."); // make error user-visible - return Err(e); + /// + /// `timestamp_sort` is used as the timestamp of the added message + /// and should be the timestamp of the change happening. + pub(crate) async fn set_protection( + self, + context: &Context, + protect: ProtectionStatus, + timestamp_sort: i64, + contact_id: Option, + ) -> Result<()> { + match self.inner_set_protection(context, protect).await { + Ok(protection_status_modified) => { + if protection_status_modified { + self.add_protection_msg(context, protect, contact_id, timestamp_sort) + .await?; + } + Ok(()) + } + Err(e) => { + error!(context, "Cannot set protection: {e:#}."); // make error user-visible + Err(e) + } } - - self.add_protection_msg(context, protect, chat.is_promoted(), ContactId::SELF) - .await } /// Archives or unarchives a chat. @@ -743,14 +765,6 @@ impl ChatId { } } - let chat = Chat::load_from_db(context, self).await?; - if let Some(cant_send_reason) = chat.why_cant_send(context).await? { - bail!( - "Can't set a draft because chat is not writeable: {}", - cant_send_reason - ); - } - // set back draft information to allow identifying the draft later on - // no matter if message object is reused or reloaded from db msg.state = MessageState::OutDraft; @@ -1260,7 +1274,7 @@ pub struct Chat { pub grpid: String, /// Whether the chat is blocked, unblocked or a contact request. - pub(crate) blocked: Blocked, + pub blocked: Blocked, /// Additional chat parameters stored in the database. pub param: Params, @@ -1272,7 +1286,7 @@ pub struct Chat { pub mute_duration: MuteDuration, /// If the chat is protected (verified). - protected: ProtectionStatus, + pub(crate) protected: ProtectionStatus, } impl Chat { @@ -1367,6 +1381,8 @@ impl Chat { Some(DeviceChat) } else if self.is_contact_request() { Some(ContactRequest) + } else if self.is_protection_broken() { + Some(ProtectionBroken) } else if self.is_mailing_list() && self.get_mailinglist_addr().is_none_or_empty() { Some(ReadOnlyMailingList) } else if !self.is_self_in_chat(context).await? { @@ -1391,7 +1407,6 @@ impl Chat { match self.typ { Chattype::Single | Chattype::Broadcast | Chattype::Mailinglist => Ok(true), Chattype::Group => is_contact_in_chat(context, self.id, ContactId::SELF).await, - Chattype::Undefined => Ok(false), } } @@ -1530,6 +1545,27 @@ impl Chat { self.protected == ProtectionStatus::Protected } + /// Returns true if the chat was protected, and then an incoming message broke this protection. + /// + /// This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag, + /// otherwise it will return false for all chats. + /// + /// 1:1 chats are automatically set as protected when a contact is verified. + /// When a message comes in that is not encrypted / signed correctly, + /// the chat is automatically set as unprotected again. + /// `is_protection_broken()` will return true until `chat_id.accept()` is called. + /// + /// The UI should let the user confirm that this is OK with a message like + /// `Bob sent a message from another device. Tap to learn more` + /// and then call `chat_id.accept()`. + pub fn is_protection_broken(&self) -> bool { + match self.protected { + ProtectionStatus::Protected => false, + ProtectionStatus::Unprotected => false, + ProtectionStatus::ProtectionBroken => true, + } + } + /// Returns true if location streaming is enabled in the chat. pub fn is_sending_locations(&self) -> bool { self.is_sending_locations @@ -1560,15 +1596,6 @@ impl Chat { let mut to_id = 0; let mut location_id = 0; - if let Some(reason) = self.why_cant_send(context).await? { - if self.typ == Chattype::Group && reason == CantSendReason::NotAMember { - context.emit_event(EventType::ErrorSelfNotInGroup( - "Cannot send message; self not in group.".into(), - )); - } - bail!("Cannot send message to {}: {}", self.id, reason); - } - let from = context.get_primary_self_addr().await?; let new_rfc724_mid = { let grpid = match self.typ { @@ -2089,19 +2116,30 @@ impl ChatIdBlocked { _ => (), } + let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?; + let protected = peerstate.map_or(false, |p| { + p.is_using_verified_key() && p.prefer_encrypt == EncryptPreference::Mutual + }); + let smeared_time = create_smeared_timestamp(context); + let chat_id = context .sql .transaction(move |transaction| { transaction.execute( "INSERT INTO chats - (type, name, param, blocked, created_timestamp) - VALUES(?, ?, ?, ?, ?)", + (type, name, param, blocked, created_timestamp, protected) + VALUES(?, ?, ?, ?, ?, ?)", ( Chattype::Single, chat_name, params.to_string(), create_blocked as u8, - create_smeared_timestamp(context), + smeared_time, + if protected { + ProtectionStatus::Protected + } else { + ProtectionStatus::Unprotected + }, ), )?; let chat_id = ChatId::new( @@ -2122,6 +2160,17 @@ impl ChatIdBlocked { }) .await?; + if protected { + chat_id + .add_protection_msg( + context, + ProtectionStatus::Protected, + Some(contact_id), + smeared_time, + ) + .await?; + } + match contact_id { ContactId::SELF => update_saved_messages_icon(context).await?, ContactId::DEVICE => update_device_icon(context).await?, @@ -2159,9 +2208,12 @@ async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> { .with_context(|| format!("attachment missing for message of type #{}", msg.viewtype))?; let mut maybe_sticker = msg.viewtype == Viewtype::Sticker; - if msg.viewtype == Viewtype::Image || maybe_sticker { + if msg.viewtype == Viewtype::Image + || maybe_sticker && !msg.param.exists(Param::ForceSticker) + { blob.recode_to_image_size(context, &mut maybe_sticker) .await?; + if !maybe_sticker { msg.viewtype = Viewtype::Image; } @@ -2235,7 +2287,13 @@ async fn prepare_msg_common( // Check if the chat can be sent to. if let Some(reason) = chat.why_cant_send(context).await? { - bail!("cannot send to {}: {}", chat_id, reason); + if reason == CantSendReason::ProtectionBroken + && msg.param.get_cmd() == SystemMessage::SecurejoinMessage + { + // Send out the message, the securejoin message is supposed to repair the verification + } else { + bail!("cannot send to {chat_id}: {reason}"); + } } // check current MessageState for drafts (to keep msg_id) ... @@ -2459,14 +2517,13 @@ pub(crate) async fn create_send_msg_job( msg.chat_id.set_gossiped_timestamp(context, time()).await?; } - if 0 != rendered_msg.last_added_location_id { + if let Some(last_added_location_id) = rendered_msg.last_added_location_id { if let Err(err) = location::set_kml_sent_timestamp(context, msg.chat_id, time()).await { error!(context, "Failed to set kml sent_timestamp: {err:#}."); } if !msg.hidden { if let Err(err) = - location::set_msg_location_id(context, msg.id, rendered_msg.last_added_location_id) - .await + location::set_msg_location_id(context, msg.id, last_added_location_id).await { error!(context, "Failed to set msg_location_id: {err:#}."); } @@ -2979,18 +3036,14 @@ pub async fn create_group_chat( let grpid = create_id(); + let timestamp = create_smeared_timestamp(context); let row_id = context .sql .insert( "INSERT INTO chats (type, name, grpid, param, created_timestamp) VALUES(?, ?, ?, \'U=1\', ?);", - ( - Chattype::Group, - chat_name, - grpid, - create_smeared_timestamp(context), - ), + (Chattype::Group, chat_name, grpid, timestamp), ) .await?; @@ -3002,9 +3055,9 @@ pub async fn create_group_chat( context.emit_msgs_changed_without_ids(); if protect == ProtectionStatus::Protected { - // this part is to stay compatible to verified groups, - // in some future, we will drop the "protect"-flag from create_group_chat() - chat_id.inner_set_protection(context, protect).await?; + chat_id + .set_protection(context, protect, timestamp, None) + .await?; } Ok(chat_id) @@ -5260,72 +5313,6 @@ mod tests { Ok(()) } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_set_protection() -> Result<()> { - let t = TestContext::new_alice().await; - t.set_config_bool(Config::BccSelf, false).await?; - let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?; - let chat = Chat::load_from_db(&t, chat_id).await?; - assert!(!chat.is_protected()); - assert!(chat.is_unpromoted()); - - // enable protection on unpromoted chat, the info-message is added via add_info_msg() - chat_id - .set_protection(&t, ProtectionStatus::Protected) - .await?; - - let chat = Chat::load_from_db(&t, chat_id).await?; - assert!(chat.is_protected()); - assert!(chat.is_unpromoted()); - - let msgs = get_chat_msgs(&t, chat_id).await?; - assert_eq!(msgs.len(), 1); - - let msg = t.get_last_msg_in(chat_id).await; - assert!(msg.is_info()); - assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled); - assert_eq!(msg.get_state(), MessageState::InNoticed); - - // disable protection again, still unpromoted - chat_id - .set_protection(&t, ProtectionStatus::Unprotected) - .await?; - - let chat = Chat::load_from_db(&t, chat_id).await?; - assert!(!chat.is_protected()); - assert!(chat.is_unpromoted()); - - let msg = t.get_last_msg_in(chat_id).await; - assert!(msg.is_info()); - assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionDisabled); - assert_eq!(msg.get_state(), MessageState::InNoticed); - - // send a message, this switches to promoted state - send_text_msg(&t, chat_id, "hi!".to_string()).await?; - - let chat = Chat::load_from_db(&t, chat_id).await?; - assert!(!chat.is_protected()); - assert!(!chat.is_unpromoted()); - - let msgs = get_chat_msgs(&t, chat_id).await?; - assert_eq!(msgs.len(), 3); - - // enable protection on promoted chat, the info-message is sent via send_msg() this time - chat_id - .set_protection(&t, ProtectionStatus::Protected) - .await?; - let chat = Chat::load_from_db(&t, chat_id).await?; - assert!(chat.is_protected()); - assert!(!chat.is_unpromoted()); - - let msg = t.get_last_msg_in(chat_id).await; - assert!(msg.is_info()); - assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled); - assert_eq!(msg.get_state(), MessageState::OutDelivered); // as bcc-self is disabled and there is nobody else in the chat - - Ok(()) - } - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_lookup_by_contact_id() { let ctx = TestContext::new_alice().await; @@ -5695,6 +5682,51 @@ mod tests { .await } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_sticker_jpeg_force() { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + let alice_chat = alice.create_chat(&bob).await; + + let file = alice.get_blobdir().join("sticker.jpg"); + tokio::fs::write( + &file, + include_bytes!("../test-data/image/avatar1000x1000.jpg"), + ) + .await + .unwrap(); + + // Images without force_sticker should be turned into [Viewtype::Image] + let mut msg = Message::new(Viewtype::Sticker); + msg.set_file(file.to_str().unwrap(), None); + let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; + let msg = bob.recv_msg(&sent_msg).await; + assert_eq!(msg.get_viewtype(), Viewtype::Image); + + // Images with `force_sticker = true` should keep [Viewtype::Sticker] + let mut msg = Message::new(Viewtype::Sticker); + msg.set_file(file.to_str().unwrap(), None); + msg.force_sticker(); + let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; + let msg = bob.recv_msg(&sent_msg).await; + assert_eq!(msg.get_viewtype(), Viewtype::Sticker); + + // Images with `force_sticker = true` should keep [Viewtype::Sticker] + // even on drafted messages + let mut msg = Message::new(Viewtype::Sticker); + msg.set_file(file.to_str().unwrap(), None); + msg.force_sticker(); + alice_chat + .id + .set_draft(&alice, Some(&mut msg)) + .await + .unwrap(); + let mut msg = alice_chat.id.get_draft(&alice).await.unwrap().unwrap(); + let sent_msg = alice.send_msg(alice_chat.id, &mut msg).await; + let msg = bob.recv_msg(&sent_msg).await; + assert_eq!(msg.get_viewtype(), Viewtype::Sticker); + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_sticker_gif() -> Result<()> { test_sticker( diff --git a/src/chatlist.rs b/src/chatlist.rs index 70b07ba73..883ca780c 100644 --- a/src/chatlist.rs +++ b/src/chatlist.rs @@ -406,7 +406,7 @@ impl Chatlist { .context("loading contact failed")?; (Some(lastmsg), Some(lastcontact)) } - Chattype::Single | Chattype::Undefined => (Some(lastmsg), None), + Chattype::Single => (Some(lastmsg), None), } } } else { diff --git a/src/config.rs b/src/config.rs index 8611778fa..1ffba283e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -324,6 +324,16 @@ pub enum Config { /// This is not supposed to be changed by UIs and only used for testing. #[strum(props(default = "172800"))] GossipPeriod, + + /// Feature flag for verified 1:1 chats; the UI should set it + /// to 1 if it supports verified 1:1 chats. + /// Regardless of this setting, `chat.is_protected()` returns true while the key is verified, + /// and when the key changes, an info message is posted into the chat. + /// 0=Nothing else happens when the key changes. + /// 1=After the key changed, `can_send()` returns false and `is_protection_broken()` returns true + /// until `chat_id.accept()` is called. + #[strum(props(default = "0"))] + VerifiedOneOnOneChats, } impl Context { diff --git a/src/constants.rs b/src/constants.rs index 957a09ef3..165de5391 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -125,7 +125,6 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9); /// Chat type. #[derive( Debug, - Default, Display, Clone, Copy, @@ -141,10 +140,6 @@ pub const DC_CHAT_ID_LAST_SPECIAL: ChatId = ChatId::new(9); )] #[repr(u32)] pub enum Chattype { - /// Undefined chat type. - #[default] - Undefined = 0, - /// 1:1 chat. Single = 100, @@ -223,8 +218,6 @@ mod tests { #[test] fn test_chattype_values() { // values may be written to disk and must not change - assert_eq!(Chattype::Undefined, Chattype::default()); - assert_eq!(Chattype::Undefined, Chattype::from_i32(0).unwrap()); assert_eq!(Chattype::Single, Chattype::from_i32(100).unwrap()); assert_eq!(Chattype::Group, Chattype::from_i32(120).unwrap()); assert_eq!(Chattype::Mailinglist, Chattype::from_i32(140).unwrap()); diff --git a/src/contact.rs b/src/contact.rs index 4a4ff3540..fed85e053 100644 --- a/src/contact.rs +++ b/src/contact.rs @@ -25,7 +25,7 @@ use crate::config::Config; use crate::constants::{Blocked, Chattype, DC_GCL_ADD_SELF, DC_GCL_VERIFIED_ONLY}; use crate::context::Context; use crate::events::EventType; -use crate::key::{DcKey, SignedPublicKey}; +use crate::key::{load_self_public_key, DcKey}; use crate::login_param::LoginParam; use crate::message::MessageState; use crate::mimeparser::AvatarAction; @@ -1032,7 +1032,7 @@ impl Contact { let finger_prints = stock_str::finger_prints(context).await; ret += &format!("{stock_message}.\n{finger_prints}:"); - let fingerprint_self = SignedPublicKey::load_self(context) + let fingerprint_self = load_self_public_key(context) .await? .fingerprint() .to_string(); @@ -1234,31 +1234,15 @@ impl Contact { /// and if the key has not changed since this verification. /// /// The UI may draw a checkbox or something like that beside verified contacts. - /// pub async fn is_verified(&self, context: &Context) -> Result { - self.is_verified_ex(context, None).await - } - - /// Same as `Contact::is_verified` but allows speeding up things - /// by adding the peerstate belonging to the contact. - /// If you do not have the peerstate available, it is loaded automatically. - pub async fn is_verified_ex( - &self, - context: &Context, - peerstate: Option<&Peerstate>, - ) -> Result { // We're always sort of secured-verified as we could verify the key on this device any time with the key // on this device if self.id == ContactId::SELF { return Ok(VerifiedStatus::BidirectVerified); } - if let Some(peerstate) = peerstate { - if peerstate.verified_key.is_some() { - return Ok(VerifiedStatus::BidirectVerified); - } - } else if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? { - if peerstate.verified_key.is_some() { + if let Some(peerstate) = Peerstate::from_addr(context, &self.addr).await? { + if peerstate.is_using_verified_key() { return Ok(VerifiedStatus::BidirectVerified); } } diff --git a/src/context.rs b/src/context.rs index 3fd97a2ad..a0f78f267 100644 --- a/src/context.rs +++ b/src/context.rs @@ -19,7 +19,7 @@ use crate::constants::DC_VERSION_STR; use crate::contact::Contact; use crate::debug_logging::DebugLogging; use crate::events::{Event, EventEmitter, EventType, Events}; -use crate::key::{DcKey, SignedPublicKey}; +use crate::key::{load_self_public_key, DcKey as _}; use crate::login_param::LoginParam; use crate::message::{self, MessageState, MsgId}; use crate::quota::QuotaInfo; @@ -593,7 +593,7 @@ impl Context { .sql .count("SELECT COUNT(*) FROM acpeerstates;", ()) .await?; - let fingerprint_str = match SignedPublicKey::load_self(self).await { + let fingerprint_str = match load_self_public_key(self).await { Ok(key) => key.fingerprint().hex(), Err(err) => format!(""), }; @@ -772,6 +772,12 @@ impl Context { "gossip_period", self.get_config_int(Config::GossipPeriod).await?.to_string(), ); + res.insert( + "verified_one_on_one_chats", + self.get_config_bool(Config::VerifiedOneOnOneChats) + .await? + .to_string(), + ); let elapsed = self.creation_time.elapsed(); res.insert("uptime", duration_to_str(elapsed.unwrap_or_default())); diff --git a/src/debug_logging.rs b/src/debug_logging.rs index 7b343a35f..d9a67473e 100644 --- a/src/debug_logging.rs +++ b/src/debug_logging.rs @@ -1,14 +1,12 @@ //! Forward log messages to logging webxdc -use crate::{ - chat::ChatId, - config::Config, - context::Context, - message::{Message, MsgId, Viewtype}, - param::Param, - tools::time, - webxdc::StatusUpdateItem, - EventType, -}; +use crate::chat::ChatId; +use crate::config::Config; +use crate::context::Context; +use crate::events::EventType; +use crate::message::{Message, MsgId, Viewtype}; +use crate::param::Param; +use crate::tools::time; +use crate::webxdc::StatusUpdateItem; use async_channel::{self as channel, Receiver, Sender}; use serde_json::json; use std::path::PathBuf; diff --git a/src/decrypt.rs b/src/decrypt.rs index ce5624ea5..24ab873f0 100644 --- a/src/decrypt.rs +++ b/src/decrypt.rs @@ -13,7 +13,6 @@ use crate::contact::addr_cmp; use crate::context::Context; use crate::headerdef::{HeaderDef, HeaderDefMap}; use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey}; -use crate::keyring::Keyring; use crate::peerstate::Peerstate; use crate::pgp; @@ -26,8 +25,8 @@ use crate::pgp; pub fn try_decrypt( context: &Context, mail: &ParsedMail<'_>, - private_keyring: &Keyring, - public_keyring_for_validate: &Keyring, + private_keyring: &[SignedSecretKey], + public_keyring_for_validate: &[SignedPublicKey], ) -> Result, HashSet)>> { let encrypted_data_part = match { let mime = get_autocrypt_mime(mail); @@ -227,8 +226,8 @@ fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail /// Returns Ok(None) if nothing encrypted was found. fn decrypt_part( mail: &ParsedMail<'_>, - private_keyring: &Keyring, - public_keyring_for_validate: &Keyring, + private_keyring: &[SignedSecretKey], + public_keyring_for_validate: &[SignedPublicKey], ) -> Result, HashSet)>> { let data = mail.get_body_raw()?; @@ -263,7 +262,7 @@ fn has_decrypted_pgp_armor(input: &[u8]) -> bool { /// Returns None if the message is not Multipart/Signed or doesn't contain necessary parts. pub(crate) fn validate_detached_signature<'a, 'b>( mail: &'a ParsedMail<'b>, - public_keyring_for_validate: &Keyring, + public_keyring_for_validate: &[SignedPublicKey], ) -> Option<(&'a ParsedMail<'b>, HashSet)> { if mail.ctype.mimetype != "multipart/signed" { return None; @@ -283,13 +282,13 @@ pub(crate) fn validate_detached_signature<'a, 'b>( } } -pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Keyring { - let mut public_keyring_for_validate: Keyring = Keyring::new(); +pub(crate) fn keyring_from_peerstate(peerstate: Option<&Peerstate>) -> Vec { + let mut public_keyring_for_validate = Vec::new(); if let Some(peerstate) = peerstate { if let Some(key) = &peerstate.public_key { - public_keyring_for_validate.add(key.clone()); + public_keyring_for_validate.push(key.clone()); } else if let Some(key) = &peerstate.gossip_key { - public_keyring_for_validate.add(key.clone()); + public_keyring_for_validate.push(key.clone()); } } public_keyring_for_validate diff --git a/src/e2ee.rs b/src/e2ee.rs index b870f89fe..6a966b488 100644 --- a/src/e2ee.rs +++ b/src/e2ee.rs @@ -6,8 +6,7 @@ use num_traits::FromPrimitive; use crate::aheader::{Aheader, EncryptPreference}; use crate::config::Config; use crate::context::Context; -use crate::key::{DcKey, SignedPublicKey, SignedSecretKey}; -use crate::keyring::Keyring; +use crate::key::{load_self_public_key, load_self_secret_key, SignedPublicKey}; use crate::peerstate::{Peerstate, PeerstateVerifiedStatus}; use crate::pgp; @@ -24,7 +23,7 @@ impl EncryptHelper { EncryptPreference::from_i32(context.get_config_int(Config::E2eeEnabled).await?) .unwrap_or_default(); let addr = context.get_primary_self_addr().await?; - let public_key = SignedPublicKey::load_self(context).await?; + let public_key = load_self_public_key(context).await?; Ok(EncryptHelper { prefer_encrypt, @@ -104,7 +103,7 @@ impl EncryptHelper { mail_to_encrypt: lettre_email::PartBuilder, peerstates: Vec<(Option, &str)>, ) -> Result { - let mut keyring: Keyring = Keyring::new(); + let mut keyring: Vec = Vec::new(); for (peerstate, addr) in peerstates .into_iter() @@ -113,10 +112,10 @@ impl EncryptHelper { let key = peerstate .take_key(min_verified) .with_context(|| format!("proper enc-key for {addr} missing, cannot encrypt"))?; - keyring.add(key); + keyring.push(key); } - keyring.add(self.public_key.clone()); - let sign_key = SignedSecretKey::load_self(context).await?; + keyring.push(self.public_key.clone()); + let sign_key = load_self_secret_key(context).await?; let raw_message = mail_to_encrypt.build().as_string().into_bytes(); @@ -132,7 +131,7 @@ impl EncryptHelper { context: &Context, mail: lettre_email::PartBuilder, ) -> Result<(lettre_email::MimeMessage, String)> { - let sign_key = SignedSecretKey::load_self(context).await?; + let sign_key = load_self_secret_key(context).await?; let mime_message = mail.build(); let signature = pgp::pk_calc_signature(mime_message.as_string().as_bytes(), &sign_key)?; Ok((mime_message, signature)) @@ -145,20 +144,17 @@ impl EncryptHelper { /// sent but in a few locations there are no such guarantees, /// e.g. when exporting keys, and calling this function ensures a /// private key will be present. -/// -/// If this succeeds you are also guaranteed that the -/// [Config::ConfiguredAddr] is configured, this address is returned. // TODO, remove this once deltachat::key::Key no longer exists. -pub async fn ensure_secret_key_exists(context: &Context) -> Result { - let self_addr = context.get_primary_self_addr().await?; - SignedPublicKey::load_self(context).await?; - Ok(self_addr) +pub async fn ensure_secret_key_exists(context: &Context) -> Result<()> { + load_self_public_key(context).await?; + Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::chat; + use crate::key::DcKey; use crate::message::{Message, Viewtype}; use crate::param::Param; use crate::test_utils::{bob_keypair, TestContext}; @@ -169,10 +165,7 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_prexisting() { let t = TestContext::new_alice().await; - assert_eq!( - ensure_secret_key_exists(&t).await.unwrap(), - "alice@example.org" - ); + assert!(ensure_secret_key_exists(&t).await.is_ok()); } #[tokio::test(flavor = "multi_thread", worker_threads = 2)] diff --git a/src/ephemeral.rs b/src/ephemeral.rs index 48cf0801b..694c1d41c 100644 --- a/src/ephemeral.rs +++ b/src/ephemeral.rs @@ -545,7 +545,7 @@ async fn next_expiration_timestamp(context: &Context) -> Option { ephemeral_timestamp .into_iter() - .chain(delete_device_after_timestamp.into_iter()) + .chain(delete_device_after_timestamp) .min() } @@ -986,7 +986,7 @@ mod tests { t.send_text(self_chat.id, "Saved message, which we delete manually") .await; let msg = t.get_last_msg_in(self_chat.id).await; - msg.id.delete_from_db(&t).await?; + msg.id.trash(&t).await?; check_msg_is_deleted(&t, &self_chat, msg.id).await; self_chat @@ -1003,7 +1003,7 @@ mod tests { .await .unwrap(); - // Set DeleteDeviceAfter to 1800s. Thend send a saved message which will + // Set DeleteDeviceAfter to 1800s. Then send a saved message which will // still be deleted after 3600s because DeleteDeviceAfter doesn't apply to saved messages. t.set_config(Config::DeleteDeviceAfter, Some("1800")) .await?; @@ -1260,8 +1260,8 @@ mod tests { ); let msg = alice.get_last_msg().await; - // Message is deleted from the database when its timer expires. - msg.id.delete_from_db(&alice).await?; + // Message is deleted when its timer expires. + msg.id.trash(&alice).await?; // Message with Message-ID , referencing and // , is received. The message is not in the diff --git a/src/html.rs b/src/html.rs index 7124c0a1a..5af221b70 100644 --- a/src/html.rs +++ b/src/html.rs @@ -17,12 +17,12 @@ use lettre_email::mime::{self, Mime}; use lettre_email::PartBuilder; use mailparse::ParsedContentType; +use crate::context::Context; use crate::headerdef::{HeaderDef, HeaderDefMap}; -use crate::message::{Message, MsgId}; +use crate::message::{self, Message, MsgId}; use crate::mimeparser::parse_message_id; use crate::param::Param::SendHtml; use crate::plaintext::PlainText; -use crate::{context::Context, message}; impl Message { /// Check if the message can be retrieved as HTML. diff --git a/src/imex.rs b/src/imex.rs index d137c8b20..236f5e6fc 100644 --- a/src/imex.rs +++ b/src/imex.rs @@ -19,7 +19,9 @@ 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::key::{ + self, load_self_secret_key, DcKey, DcSecretKey, SignedPublicKey, SignedSecretKey, +}; use crate::log::LogExt; use crate::message::{Message, MsgId, Viewtype}; use crate::mimeparser::SystemMessage; @@ -185,7 +187,7 @@ pub async fn render_setup_file(context: &Context, passphrase: &str) -> Result None, true => Some(("Autocrypt-Prefer-Encrypt", "mutual")), @@ -785,6 +787,11 @@ async fn export_database(context: &Context, dest: &Path, passphrase: String) -> let res = conn .query_row("SELECT sqlcipher_export('backup')", [], |_row| Ok(())) .context("failed to export to attached backup database"); + conn.execute( + "UPDATE backup.config SET value='0' WHERE keyname='verified_one_on_one_chats';", + [], + ) + .ok(); // If verified_one_on_one_chats was not set, this errors, which we ignore conn.execute("DETACH DATABASE backup", []) .context("failed to detach backup database")?; res?; @@ -915,55 +922,73 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_export_and_import_backup() -> Result<()> { - let backup_dir = tempfile::tempdir().unwrap(); + for set_verified_oneonone_chats in [true, false] { + let backup_dir = tempfile::tempdir().unwrap(); - let context1 = TestContext::new_alice().await; - assert!(context1.is_configured().await?); + let context1 = TestContext::new_alice().await; + assert!(context1.is_configured().await?); + if set_verified_oneonone_chats { + context1 + .set_config_bool(Config::VerifiedOneOnOneChats, true) + .await?; + } - let context2 = TestContext::new().await; - assert!(!context2.is_configured().await?); - assert!(has_backup(&context2, backup_dir.path()).await.is_err()); + let context2 = TestContext::new().await; + assert!(!context2.is_configured().await?); + assert!(has_backup(&context2, backup_dir.path()).await.is_err()); - // export from context1 - assert!( - imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None) - .await - .is_ok() - ); - let _event = context1 - .evtracker - .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000))) - .await; + // export from context1 + assert!( + imex(&context1, ImexMode::ExportBackup, backup_dir.path(), None) + .await + .is_ok() + ); + let _event = context1 + .evtracker + .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000))) + .await; - // import to context2 - let backup = has_backup(&context2, backup_dir.path()).await?; + // import to context2 + let backup = has_backup(&context2, backup_dir.path()).await?; - // Import of unencrypted backup with incorrect "foobar" backup passphrase fails. - assert!(imex( - &context2, - ImexMode::ImportBackup, - backup.as_ref(), - Some("foobar".to_string()) - ) - .await - .is_err()); + // Import of unencrypted backup with incorrect "foobar" backup passphrase fails. + assert!(imex( + &context2, + ImexMode::ImportBackup, + backup.as_ref(), + Some("foobar".to_string()) + ) + .await + .is_err()); - assert!( - imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None) - .await - .is_ok() - ); - let _event = context2 - .evtracker - .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000))) - .await; - - assert!(context2.is_configured().await?); - assert_eq!( - context2.get_config(Config::Addr).await?, - Some("alice@example.org".to_string()) - ); + assert!( + imex(&context2, ImexMode::ImportBackup, backup.as_ref(), None) + .await + .is_ok() + ); + let _event = context2 + .evtracker + .get_matching(|evt| matches!(evt, EventType::ImexProgress(1000))) + .await; + assert!(context2.is_configured().await?); + assert_eq!( + context2.get_config(Config::Addr).await?, + Some("alice@example.org".to_string()) + ); + assert_eq!( + context2 + .get_config_bool(Config::VerifiedOneOnOneChats) + .await?, + false + ); + assert_eq!( + context1 + .get_config_bool(Config::VerifiedOneOnOneChats) + .await?, + set_verified_oneonone_chats + ); + } Ok(()) } diff --git a/src/key.rs b/src/key.rs index 32deec2e3..230f00f8c 100644 --- a/src/key.rs +++ b/src/key.rs @@ -3,11 +3,9 @@ use std::collections::BTreeMap; use std::fmt; use std::io::Cursor; -use std::pin::Pin; use anyhow::{ensure, Context as _, Result}; use base64::Engine as _; -use futures::Future; use num_traits::FromPrimitive; use pgp::composed::Deserializable; pub use pgp::composed::{SignedPublicKey, SignedSecretKey}; @@ -18,8 +16,7 @@ use tokio::runtime::Handle; use crate::config::Config; use crate::constants::KeyGenType; use crate::context::Context; -// Re-export key types -pub use crate::pgp::KeyPair; +use crate::pgp::KeyPair; use crate::tools::{time, EmailAddress}; /// Convenience trait for working with keys. @@ -27,7 +24,7 @@ use crate::tools::{time, EmailAddress}; /// This trait is implemented for rPGP's [SignedPublicKey] and /// [SignedSecretKey] types and makes working with them a little /// easier in the deltachat world. -pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone { +pub(crate) trait DcKey: Serialize + Deserializable + KeyTrait + Clone { /// Create a key from some bytes. fn from_slice(bytes: &[u8]) -> Result { Ok(::from_bytes(Cursor::new(bytes))?) @@ -50,11 +47,6 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone { Self::from_armor_single(Cursor::new(bytes)).context("rPGP error") } - /// Load the users' default key from the database. - fn load_self<'a>( - context: &'a Context, - ) -> Pin> + 'a + Send>>; - /// Serialise the key as bytes. fn to_bytes(&self) -> Vec { // Not using Serialize::to_bytes() to make clear *why* it is @@ -85,38 +77,55 @@ pub trait DcKey: Serialize + Deserializable + KeyTrait + Clone { } } -impl DcKey for SignedPublicKey { - fn load_self<'a>( - context: &'a Context, - ) -> Pin> + 'a + Send>> { - Box::pin(async move { - let addr = context.get_primary_self_addr().await?; - match context - .sql - .query_row_optional( - r#" - SELECT public_key - FROM keypairs - WHERE addr=? - AND is_default=1; - "#, - (addr,), - |row| { - let bytes: Vec = row.get(0)?; - Ok(bytes) - }, - ) - .await? - { - Some(bytes) => Self::from_slice(&bytes), - None => { - let keypair = generate_keypair(context).await?; - Ok(keypair.public) - } - } - }) +pub(crate) async fn load_self_public_key(context: &Context) -> Result { + match context + .sql + .query_row_optional( + r#"SELECT public_key + FROM keypairs + WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr") + AND is_default=1"#, + (), + |row| { + let bytes: Vec = row.get(0)?; + Ok(bytes) + }, + ) + .await? + { + Some(bytes) => SignedPublicKey::from_slice(&bytes), + None => { + let keypair = generate_keypair(context).await?; + Ok(keypair.public) + } } +} +pub(crate) async fn load_self_secret_key(context: &Context) -> Result { + match context + .sql + .query_row_optional( + r#"SELECT private_key + FROM keypairs + WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr") + AND is_default=1"#, + (), + |row| { + let bytes: Vec = row.get(0)?; + Ok(bytes) + }, + ) + .await? + { + Some(bytes) => SignedSecretKey::from_slice(&bytes), + None => { + let keypair = generate_keypair(context).await?; + Ok(keypair.secret) + } + } +} + +impl DcKey for SignedPublicKey { fn to_asc(&self, header: Option<(&str, &str)>) -> String { // Not using .to_armored_string() to make clear *why* it is // safe to ignore this error. @@ -135,36 +144,6 @@ impl DcKey for SignedPublicKey { } impl DcKey for SignedSecretKey { - fn load_self<'a>( - context: &'a Context, - ) -> Pin> + 'a + Send>> { - Box::pin(async move { - match context - .sql - .query_row_optional( - r#" - SELECT private_key - FROM keypairs - WHERE addr=(SELECT value FROM config WHERE keyname="configured_addr") - AND is_default=1; - "#, - (), - |row| { - let bytes: Vec = row.get(0)?; - Ok(bytes) - }, - ) - .await? - { - Some(bytes) => Self::from_slice(&bytes), - None => { - let keypair = generate_keypair(context).await?; - Ok(keypair.secret) - } - } - }) - } - fn to_asc(&self, header: Option<(&str, &str)>) -> String { // Not using .to_armored_string() to make clear *why* it is // safe to do these unwraps. @@ -185,7 +164,7 @@ impl DcKey for SignedSecretKey { /// Deltachat extension trait for secret keys. /// /// Provides some convenience wrappers only applicable to [SignedSecretKey]. -pub trait DcSecretKey { +pub(crate) trait DcSecretKey { /// Create a public key from a private one. fn split_public_key(&self) -> Result; } @@ -328,6 +307,24 @@ pub async fn store_self_keypair( Ok(()) } +/// Saves a keypair as the default keys. +/// +/// This API is used for testing purposes +/// to avoid generating the key in tests. +/// Use import/export APIs instead. +pub async fn preconfigure_keypair(context: &Context, addr: &str, secret_data: &str) -> Result<()> { + let addr = EmailAddress::new(addr)?; + let secret = SignedSecretKey::from_asc(secret_data)?.0; + let public = secret.split_public_key()?; + let keypair = KeyPair { + addr, + public, + secret, + }; + store_self_keypair(context, &keypair, KeyPairUse::Default).await?; + Ok(()) +} + /// A key fingerprint #[derive(Clone, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] pub struct Fingerprint(Vec); @@ -522,9 +519,9 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD async fn test_load_self_existing() { let alice = alice_keypair(); let t = TestContext::new_alice().await; - let pubkey = SignedPublicKey::load_self(&t).await.unwrap(); + let pubkey = load_self_public_key(&t).await.unwrap(); assert_eq!(alice.public, pubkey); - let seckey = SignedSecretKey::load_self(&t).await.unwrap(); + let seckey = load_self_secret_key(&t).await.unwrap(); assert_eq!(alice.secret, seckey); } @@ -534,7 +531,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD t.set_config(Config::ConfiguredAddr, Some("alice@example.org")) .await .unwrap(); - let key = SignedPublicKey::load_self(&t).await; + let key = load_self_public_key(&t).await; assert!(key.is_ok()); } @@ -544,7 +541,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD t.set_config(Config::ConfiguredAddr, Some("alice@example.org")) .await .unwrap(); - let key = SignedSecretKey::load_self(&t).await; + let key = load_self_secret_key(&t).await; assert!(key.is_ok()); } @@ -561,7 +558,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD thread::spawn(move || { tokio::runtime::Runtime::new() .unwrap() - .block_on(SignedPublicKey::load_self(&ctx)) + .block_on(load_self_public_key(&ctx)) }) }; let thr1 = { @@ -569,7 +566,7 @@ i8pcjGO+IZffvyZJVRWfVooBJmWWbPB1pueo3tx8w3+fcuzpxz+RLFKaPyqXO+dD thread::spawn(move || { tokio::runtime::Runtime::new() .unwrap() - .block_on(SignedPublicKey::load_self(&ctx)) + .block_on(load_self_public_key(&ctx)) }) }; let res0 = thr0.join().unwrap(); diff --git a/src/keyring.rs b/src/keyring.rs deleted file mode 100644 index e354d2348..000000000 --- a/src/keyring.rs +++ /dev/null @@ -1,87 +0,0 @@ -//! Keyring to perform rpgp operations with. - -use anyhow::Result; - -use crate::context::Context; -use crate::key::DcKey; - -/// An in-memory keyring. -/// -/// Instances are usually constructed just for the rpgp operation and -/// short-lived. -#[derive(Clone, Debug, Default)] -pub struct Keyring -where - T: DcKey, -{ - keys: Vec, -} - -impl Keyring -where - T: DcKey, -{ - /// New empty keyring. - pub fn new() -> Keyring { - Keyring { keys: Vec::new() } - } - - /// Create a new keyring with the the user's secret key loaded. - pub async fn new_self(context: &Context) -> Result> { - let mut keyring: Keyring = Keyring::new(); - keyring.load_self(context).await?; - Ok(keyring) - } - - /// Load the user's key into the keyring. - pub async fn load_self(&mut self, context: &Context) -> Result<()> { - self.add(T::load_self(context).await?); - Ok(()) - } - - /// Add a key to the keyring. - pub fn add(&mut self, key: T) { - self.keys.push(key); - } - - pub fn is_empty(&self) -> bool { - self.keys.is_empty() - } - - /// A vector with reference to all the keys in the keyring. - pub fn keys(&self) -> &[T] { - &self.keys - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::key::{SignedPublicKey, SignedSecretKey}; - use crate::test_utils::{alice_keypair, TestContext}; - - #[test] - fn test_keyring_add_keys() { - let alice = alice_keypair(); - let mut pub_ring: Keyring = Keyring::new(); - pub_ring.add(alice.public.clone()); - assert_eq!(pub_ring.keys(), [alice.public]); - - let mut sec_ring: Keyring = Keyring::new(); - sec_ring.add(alice.secret.clone()); - assert_eq!(sec_ring.keys(), [alice.secret]); - } - - #[tokio::test(flavor = "multi_thread", worker_threads = 2)] - async fn test_keyring_load_self() { - // new_self() implies load_self() - let t = TestContext::new_alice().await; - let alice = alice_keypair(); - - let pub_ring: Keyring = Keyring::new_self(&t).await.unwrap(); - assert_eq!(pub_ring.keys(), [alice.public]); - - let sec_ring: Keyring = Keyring::new_self(&t).await.unwrap(); - assert_eq!(sec_ring.keys(), [alice.secret]); - } -} diff --git a/src/lib.rs b/src/lib.rs index c0b00724e..20f15972c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,7 +8,6 @@ missing_debug_implementations, missing_docs, clippy::all, - clippy::indexing_slicing, clippy::wildcard_imports, clippy::needless_borrow, clippy::cast_lossless, @@ -17,6 +16,7 @@ clippy::explicit_into_iter_loop, clippy::cloned_instead_of_copied )] +#![cfg_attr(not(test), warn(clippy::indexing_slicing))] #![allow( clippy::match_bool, clippy::mixed_read_write_in_expression, @@ -66,7 +66,6 @@ pub mod ephemeral; mod imap; pub mod imex; pub mod key; -mod keyring; pub mod location; mod login_param; pub mod message; diff --git a/src/location.rs b/src/location.rs index 9084e752f..455997813 100644 --- a/src/location.rs +++ b/src/location.rs @@ -733,7 +733,7 @@ async fn maybe_send_locations(context: &Context) -> Result> { next_event = next_event .into_iter() - .chain(u64::try_from(locations_send_until - now).into_iter()) + .chain(u64::try_from(locations_send_until - now)) .min(); if has_locations { @@ -757,7 +757,7 @@ async fn maybe_send_locations(context: &Context) -> Result> { ); next_event = next_event .into_iter() - .chain(u64::try_from(locations_last_sent + 61 - now).into_iter()) + .chain(u64::try_from(locations_last_sent + 61 - now)) .min(); } } else { diff --git a/src/login_param.rs b/src/login_param.rs index e01e43a0f..25511c419 100644 --- a/src/login_param.rs +++ b/src/login_param.rs @@ -5,9 +5,10 @@ use std::fmt; use anyhow::{ensure, Result}; use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2}; +use crate::context::Context; +use crate::provider::Socket; use crate::provider::{get_provider_by_id, Provider}; use crate::socks::Socks5Config; -use crate::{context::Context, provider::Socket}; #[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)] #[repr(u32)] diff --git a/src/message.rs b/src/message.rs index 8d5ec6fea..915c19af3 100644 --- a/src/message.rs +++ b/src/message.rs @@ -114,24 +114,16 @@ WHERE id=?; } /// Deletes a message, corresponding MDNs and unsent SMTP messages from the database. - pub async fn delete_from_db(self, context: &Context) -> Result<()> { - // We don't use transactions yet, so remove MDNs first to make - // sure they are not left while the message is deleted. + pub(crate) async fn delete_from_db(self, context: &Context) -> Result<()> { context .sql - .execute("DELETE FROM smtp WHERE msg_id=?", (self,)) - .await?; - context - .sql - .execute("DELETE FROM msgs_mdns WHERE msg_id=?;", (self,)) - .await?; - context - .sql - .execute("DELETE FROM msgs_status_updates WHERE msg_id=?;", (self,)) - .await?; - context - .sql - .execute("DELETE FROM msgs WHERE id=?;", (self,)) + .transaction(move |transaction| { + transaction.execute("DELETE FROM smtp WHERE msg_id=?", (self,))?; + transaction.execute("DELETE FROM msgs_mdns WHERE msg_id=?", (self,))?; + transaction.execute("DELETE FROM msgs_status_updates WHERE msg_id=?", (self,))?; + transaction.execute("DELETE FROM msgs WHERE id=?", (self,))?; + Ok(()) + }) .await?; Ok(()) } @@ -672,6 +664,12 @@ impl Message { self.viewtype } + /// Forces the message to **keep** [Viewtype::Sticker] + /// e.g the message will not be converted to a [Viewtype::Image]. + pub fn force_sticker(&mut self) { + self.param.set_int(Param::ForceSticker, 1); + } + /// Returns the state of the message. pub fn get_state(&self) -> MessageState { self.state @@ -772,7 +770,7 @@ impl Message { Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => { Some(Contact::get_by_id(context, self.from_id).await?) } - Chattype::Single | Chattype::Undefined => None, + Chattype::Single => None, } } else { None diff --git a/src/mimefactory.rs b/src/mimefactory.rs index 1a016f00a..f54b2fa38 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -66,7 +66,7 @@ pub struct MimeFactory<'a> { in_reply_to: String, references: String, req_mdn: bool, - last_added_location_id: u32, + last_added_location_id: Option, /// If the created mime-structure contains sync-items, /// the IDs of these items are listed here. @@ -85,7 +85,7 @@ pub struct RenderedEmail { // pub envelope: Envelope, pub is_encrypted: bool, pub is_gossiped: bool, - pub last_added_location_id: u32, + pub last_added_location_id: Option, /// A comma-separated string of sync-IDs that are used by the rendered email /// and must be deleted once the message is actually queued for sending @@ -223,7 +223,7 @@ impl<'a> MimeFactory<'a> { in_reply_to, references, req_mdn, - last_added_location_id: 0, + last_added_location_id: None, sync_ids_to_delete: None, attach_selfavatar, }; @@ -264,7 +264,7 @@ impl<'a> MimeFactory<'a> { in_reply_to: String::default(), references: String::default(), req_mdn: false, - last_added_location_id: 0, + last_added_location_id: None, sync_ids_to_delete: None, attach_selfavatar: false, }; @@ -894,7 +894,7 @@ impl<'a> MimeFactory<'a> { .body(kml_content); if !self.msg.param.exists(Param::SetLatitude) { // otherwise, the independent location is already filed - self.last_added_location_id = last_added_location_id; + self.last_added_location_id = Some(last_added_location_id); } Ok(Some(part)) } @@ -914,7 +914,16 @@ impl<'a> MimeFactory<'a> { let mut placeholdertext = None; let mut meta_part = None; - if chat.is_protected() { + let send_verified_headers = match chat.typ { + // In single chats, the protection status isn't necessarily the same for both sides, + // so we don't send the Chat-Verified header: + Chattype::Single => false, + Chattype::Group => true, + // Mailinglists and broadcast lists can actually never be verified: + Chattype::Mailinglist => false, + Chattype::Broadcast => false, + }; + if chat.is_protected() && send_verified_headers { headers .protected .push(Header::new("Chat-Verified".to_string(), "1".to_string())); @@ -1975,7 +1984,7 @@ mod tests { let incoming_msg = get_chat_msg(&t, new_msg.chat_id, 0, 2).await; if delete_original_msg { - incoming_msg.id.delete_from_db(&t).await.unwrap(); + incoming_msg.id.trash(&t).await.unwrap(); } if message_arrives_inbetween { diff --git a/src/mimeparser.rs b/src/mimeparser.rs index cda1b00cd..3e09cac14 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -27,8 +27,7 @@ use crate::decrypt::{ use crate::dehtml::dehtml; use crate::events::EventType; use crate::headerdef::{HeaderDef, HeaderDefMap}; -use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey}; -use crate::keyring::Keyring; +use crate::key::{load_self_secret_key, DcKey, Fingerprint, SignedPublicKey}; use crate::message::{ self, set_msg_failed, update_msg_state, Message, MessageState, MsgId, Viewtype, }; @@ -265,9 +264,10 @@ impl MimeMessage { headers.remove("chat-verified"); let from = from.context("No from in message")?; - let private_keyring: Keyring = Keyring::new_self(context) + let private_keyring = vec![load_self_secret_key(context) .await - .context("failed to get own keyring")?; + .context("Failed to get own key")?]; + let mut decryption_info = prepare_decryption(context, &mail, &from.addr, message_time).await?; @@ -2188,7 +2188,7 @@ async fn ndn_maybe_add_info_msg( // If we get an NDN for the mailing list, just issue a warning. warn!(context, "ignoring NDN for mailing list."); } - Chattype::Single | Chattype::Undefined => {} + Chattype::Single => {} } Ok(()) } diff --git a/src/param.rs b/src/param.rs index d0f3bb1aa..a9f5c1a42 100644 --- a/src/param.rs +++ b/src/param.rs @@ -187,6 +187,9 @@ pub enum Param { /// For Webxdc Message Instances: timestamp of summary update. WebxdcSummaryTimestamp = b'Q', + + /// For messages: Whether [crate::message::Viewtype::Sticker] should be forced. + ForceSticker = b'X', } /// An object for handling key=value parameter lists. diff --git a/src/peerstate.rs b/src/peerstate.rs index e41752541..6a2a83380 100644 --- a/src/peerstate.rs +++ b/src/peerstate.rs @@ -392,6 +392,31 @@ impl Peerstate { } } + /// Returns a reference to the contact's public key fingerprint. + /// + /// Similar to [`Self::peek_key`], but returns the fingerprint instead of the key. + fn peek_key_fingerprint(&self, min_verified: PeerstateVerifiedStatus) -> Option<&Fingerprint> { + match min_verified { + PeerstateVerifiedStatus::BidirectVerified => self.verified_key_fingerprint.as_ref(), + PeerstateVerifiedStatus::Unverified => self + .public_key_fingerprint + .as_ref() + .or(self.gossip_key_fingerprint.as_ref()), + } + } + + /// Returns true if the key used for opportunistic encryption in the 1:1 chat + /// is the same as the verified key. + /// + /// Note that verified groups always use the verified key no matter if the + /// opportunistic key matches or not. + pub(crate) fn is_using_verified_key(&self) -> bool { + let verified = self.peek_key_fingerprint(PeerstateVerifiedStatus::BidirectVerified); + + verified.is_some() + && verified == self.peek_key_fingerprint(PeerstateVerifiedStatus::Unverified) + } + /// Set this peerstate to verified /// Make sure to call `self.save_to_db` to save these changes /// Params: diff --git a/src/pgp.rs b/src/pgp.rs index d4a5a072b..c6989bbb5 100644 --- a/src/pgp.rs +++ b/src/pgp.rs @@ -20,7 +20,6 @@ use tokio::runtime::Handle; use crate::constants::KeyGenType; use crate::key::{DcKey, Fingerprint}; -use crate::keyring::Keyring; use crate::tools::EmailAddress; #[allow(missing_docs)] @@ -237,7 +236,7 @@ fn select_pk_for_encryption(key: &SignedPublicKey) -> Option, + public_keys_for_encryption: Vec, private_key_for_signing: Option, ) -> Result { let lit_msg = Message::new_literal_bytes("", plain); @@ -245,7 +244,6 @@ pub async fn pk_encrypt( Handle::current() .spawn_blocking(move || { let pkeys: Vec = public_keys_for_encryption - .keys() .iter() .filter_map(select_pk_for_encryption) .collect(); @@ -298,15 +296,15 @@ pub fn pk_calc_signature( #[allow(clippy::implicit_hasher)] pub fn pk_decrypt( ctext: Vec, - private_keys_for_decryption: &Keyring, - public_keys_for_validation: &Keyring, + private_keys_for_decryption: &[SignedSecretKey], + public_keys_for_validation: &[SignedPublicKey], ) -> Result<(Vec, HashSet)> { let mut ret_signature_fingerprints: HashSet = Default::default(); let cursor = Cursor::new(ctext); let (msg, _) = Message::from_armor_single(cursor)?; - let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.keys().iter().collect(); + let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect(); let (decryptor, _) = msg.decrypt(|| "".into(), &skeys[..])?; let msgs = decryptor.collect::>>()?; @@ -321,20 +319,13 @@ pub fn pk_decrypt( None => bail!("The decrypted message is empty"), }; - if !public_keys_for_validation.is_empty() { - let pkeys = public_keys_for_validation.keys(); - - let mut fingerprints: Vec = Vec::new(); - if let signed_msg @ pgp::composed::Message::Signed { .. } = msg { - for pkey in pkeys { - if signed_msg.verify(&pkey.primary_key).is_ok() { - let fp = DcKey::fingerprint(pkey); - fingerprints.push(fp); - } + if let signed_msg @ pgp::composed::Message::Signed { .. } = msg { + for pkey in public_keys_for_validation { + if signed_msg.verify(&pkey.primary_key).is_ok() { + let fp = DcKey::fingerprint(pkey); + ret_signature_fingerprints.insert(fp); } } - - ret_signature_fingerprints.extend(fingerprints); } Ok((content, ret_signature_fingerprints)) } else { @@ -346,12 +337,11 @@ pub fn pk_decrypt( pub fn pk_validate( content: &[u8], signature: &[u8], - public_keys_for_validation: &Keyring, + public_keys_for_validation: &[SignedPublicKey], ) -> Result> { let mut ret: HashSet = Default::default(); let standalone_signature = StandaloneSignature::from_armor_single(Cursor::new(signature))?.0; - let pkeys = public_keys_for_validation.keys(); // Remove trailing CRLF before the delimiter. // According to RFC 3156 it is considered to be part of the MIME delimiter for the purpose of @@ -360,7 +350,7 @@ pub fn pk_validate( .get(..content.len().saturating_sub(2)) .context("index is out of range")?; - for pkey in pkeys { + for pkey in public_keys_for_validation { if standalone_signature.verify(pkey, content).is_ok() { let fp = DcKey::fingerprint(pkey); ret.insert(fp); @@ -495,9 +485,7 @@ mod tests { async fn ctext_signed() -> &'static String { CTEXT_SIGNED .get_or_init(|| async { - let mut keyring = Keyring::new(); - keyring.add(KEYS.alice_public.clone()); - keyring.add(KEYS.bob_public.clone()); + let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()]; pk_encrypt(CLEARTEXT, keyring, Some(KEYS.alice_secret.clone())) .await @@ -510,9 +498,7 @@ mod tests { async fn ctext_unsigned() -> &'static String { CTEXT_UNSIGNED .get_or_init(|| async { - let mut keyring = Keyring::new(); - keyring.add(KEYS.alice_public.clone()); - keyring.add(KEYS.bob_public.clone()); + let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()]; pk_encrypt(CLEARTEXT, keyring, None).await.unwrap() }) .await @@ -537,10 +523,8 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decrypt_singed() { // Check decrypting as Alice - let mut decrypt_keyring: Keyring = Keyring::new(); - decrypt_keyring.add(KEYS.alice_secret.clone()); - let mut sig_check_keyring: Keyring = Keyring::new(); - sig_check_keyring.add(KEYS.alice_public.clone()); + let decrypt_keyring = vec![KEYS.alice_secret.clone()]; + let sig_check_keyring = vec![KEYS.alice_public.clone()]; let (plain, valid_signatures) = pk_decrypt( ctext_signed().await.as_bytes().to_vec(), &decrypt_keyring, @@ -551,10 +535,8 @@ mod tests { assert_eq!(valid_signatures.len(), 1); // Check decrypting as Bob - let mut decrypt_keyring = Keyring::new(); - decrypt_keyring.add(KEYS.bob_secret.clone()); - let mut sig_check_keyring = Keyring::new(); - sig_check_keyring.add(KEYS.alice_public.clone()); + let decrypt_keyring = vec![KEYS.bob_secret.clone()]; + let sig_check_keyring = vec![KEYS.alice_public.clone()]; let (plain, valid_signatures) = pk_decrypt( ctext_signed().await.as_bytes().to_vec(), &decrypt_keyring, @@ -567,15 +549,9 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decrypt_no_sig_check() { - let mut keyring = Keyring::new(); - keyring.add(KEYS.alice_secret.clone()); - let empty_keyring = Keyring::new(); - let (plain, valid_signatures) = pk_decrypt( - ctext_signed().await.as_bytes().to_vec(), - &keyring, - &empty_keyring, - ) - .unwrap(); + let keyring = vec![KEYS.alice_secret.clone()]; + let (plain, valid_signatures) = + pk_decrypt(ctext_signed().await.as_bytes().to_vec(), &keyring, &[]).unwrap(); assert_eq!(plain, CLEARTEXT); assert_eq!(valid_signatures.len(), 0); } @@ -583,10 +559,8 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decrypt_signed_no_key() { // The validation does not have the public key of the signer. - let mut decrypt_keyring = Keyring::new(); - decrypt_keyring.add(KEYS.bob_secret.clone()); - let mut sig_check_keyring = Keyring::new(); - sig_check_keyring.add(KEYS.bob_public.clone()); + let decrypt_keyring = vec![KEYS.bob_secret.clone()]; + let sig_check_keyring = vec![KEYS.bob_public.clone()]; let (plain, valid_signatures) = pk_decrypt( ctext_signed().await.as_bytes().to_vec(), &decrypt_keyring, @@ -599,13 +573,11 @@ mod tests { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_decrypt_unsigned() { - let mut decrypt_keyring = Keyring::new(); - decrypt_keyring.add(KEYS.bob_secret.clone()); - let sig_check_keyring = Keyring::new(); + let decrypt_keyring = vec![KEYS.bob_secret.clone()]; let (plain, valid_signatures) = pk_decrypt( ctext_unsigned().await.as_bytes().to_vec(), &decrypt_keyring, - &sig_check_keyring, + &[], ) .unwrap(); assert_eq!(plain, CLEARTEXT); diff --git a/src/qr.rs b/src/qr.rs index e11b74f56..7d89d3e93 100644 --- a/src/qr.rs +++ b/src/qr.rs @@ -17,11 +17,12 @@ use crate::contact::{ addr_normalize, may_be_valid_addr, Contact, ContactAddress, ContactId, Origin, }; use crate::context::Context; +use crate::events::EventType; use crate::key::Fingerprint; use crate::message::Message; use crate::peerstate::Peerstate; use crate::socks::Socks5Config; -use crate::{token, EventType}; +use crate::token; const OPENPGP4FPR_SCHEME: &str = "OPENPGP4FPR:"; // yes: uppercase const DCACCOUNT_SCHEME: &str = "DCACCOUNT:"; diff --git a/src/qr_code_generator.rs b/src/qr_code_generator.rs index 4fb9a2deb..85c3ecba6 100644 --- a/src/qr_code_generator.rs +++ b/src/qr_code_generator.rs @@ -4,17 +4,15 @@ use anyhow::Result; use base64::Engine as _; use qrcodegen::{QrCode, QrCodeEcc}; -use crate::{ - blob::BlobObject, - chat::{Chat, ChatId}, - color::color_int_to_hex_string, - config::Config, - contact::{Contact, ContactId}, - context::Context, - qr::{self, Qr}, - securejoin, - stock_str::{self, backup_transfer_qr}, -}; +use crate::blob::BlobObject; +use crate::chat::{Chat, ChatId}; +use crate::color::color_int_to_hex_string; +use crate::config::Config; +use crate::contact::{Contact, ContactId}; +use crate::context::Context; +use crate::qr::{self, Qr}; +use crate::securejoin; +use crate::stock_str::{self, backup_transfer_qr}; /// Returns SVG of the QR code to join the group or verify contact. /// diff --git a/src/receive_imf.rs b/src/receive_imf.rs index f254459e7..1ca7323c9 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -4,7 +4,7 @@ use std::cmp::min; use std::collections::HashSet; use std::convert::TryFrom; -use anyhow::{bail, ensure, Context as _, Result}; +use anyhow::{Context as _, Result}; use mailparse::{parse_mail, SingleInfo}; use num_traits::FromPrimitive; use once_cell::sync::Lazy; @@ -14,7 +14,7 @@ use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus}; use crate::config::Config; use crate::constants::{Blocked, Chattype, ShowEmails, DC_CHAT_ID_TRASH}; use crate::contact::{ - may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin, VerifiedStatus, + may_be_valid_addr, normalize_name, Contact, ContactAddress, ContactId, Origin, }; use crate::context::Context; use crate::debug_logging::maybe_set_logging_xdc_inner; @@ -534,6 +534,10 @@ async fn add_parts( securejoin_seen = true; } } + // Peerstate could be updated by handling the Securejoin handshake. + let contact = Contact::get_by_id(context, from_id).await?; + mime_parser.decryption_info.peerstate = + Peerstate::from_addr(context, contact.get_addr()).await?; } else { securejoin_seen = false; } @@ -624,12 +628,17 @@ async fn add_parts( // In lookup_chat_by_reply() and create_or_lookup_group(), it can happen that the message is put into a chat // but the From-address is not a member of this chat. - if let Some(chat_id) = chat_id { - if !chat::is_contact_in_chat(context, chat_id, from_id).await? { - let chat = Chat::load_from_db(context, chat_id).await?; + if let Some(group_chat_id) = chat_id { + if !chat::is_contact_in_chat(context, group_chat_id, from_id).await? { + let chat = Chat::load_from_db(context, group_chat_id).await?; if chat.is_protected() { - let s = stock_str::unknown_sender_for_chat(context).await; - mime_parser.repl_msg_by_error(&s); + if chat.typ == Chattype::Single { + // Just assign the message to the 1:1 chat with the actual sender instead. + chat_id = None; + } else { + let s = stock_str::unknown_sender_for_chat(context).await; + mime_parser.repl_msg_by_error(&s); + } } else { // In non-protected chats, just mark the sender as overridden. Therefore, the UI will prepend `~` // to the sender's name, indicating to the user that he/she is not part of the group. @@ -645,7 +654,7 @@ async fn add_parts( context, mime_parser, sent_timestamp, - chat_id, + group_chat_id, from_id, to_ids, ) @@ -726,6 +735,51 @@ async fn add_parts( ); } } + + // Check if the message was sent with verified encryption and set the protection of + // the 1:1 chat accordingly. + let chat = match is_partial_download.is_none() + && mime_parser.get_header(HeaderDef::SecureJoin).is_none() + && !is_mdn + { + true => Some(Chat::load_from_db(context, chat_id).await?) + .filter(|chat| chat.typ == Chattype::Single), + false => None, + }; + if let Some(chat) = chat { + let mut new_protection = match has_verified_encryption( + context, + mime_parser, + from_id, + to_ids, + Chattype::Single, + ) + .await? + { + VerifiedEncryption::Verified => ProtectionStatus::Protected, + VerifiedEncryption::NotVerified(_) => ProtectionStatus::Unprotected, + }; + + if chat.protected != ProtectionStatus::Unprotected + && new_protection == ProtectionStatus::Unprotected + // `chat.protected` must be maintained regardless of the `Config::VerifiedOneOnOneChats`. + // That's why the config is checked here, and not above. + && context.get_config_bool(Config::VerifiedOneOnOneChats).await? + { + new_protection = ProtectionStatus::ProtectionBroken; + } + if chat.protected != new_protection { + // The message itself will be sorted under the device message since the device + // message is `MessageState::InNoticed`, which means that all following + // messages are sorted under it. + let sort_timestamp = + calc_sort_timestamp(context, sent_timestamp, chat_id, true, incoming) + .await?; + chat_id + .set_protection(context, new_protection, sort_timestamp, Some(from_id)) + .await?; + } + } } } @@ -914,7 +968,8 @@ async fn add_parts( }; let in_fresh = state == MessageState::InFresh; - let sort_timestamp = calc_sort_timestamp(context, sent_timestamp, chat_id, in_fresh).await?; + let sort_timestamp = + calc_sort_timestamp(context, sent_timestamp, chat_id, false, incoming).await?; // Apply ephemeral timer changes to the chat. // @@ -993,42 +1048,14 @@ async fn add_parts( // if a chat is protected and the message is fully downloaded, check additional properties if !chat_id.is_special() && is_partial_download.is_none() { let chat = Chat::load_from_db(context, chat_id).await?; - let new_status = match mime_parser.is_system_message { - SystemMessage::ChatProtectionEnabled => Some(ProtectionStatus::Protected), - SystemMessage::ChatProtectionDisabled => Some(ProtectionStatus::Unprotected), - _ => None, - }; - if chat.is_protected() || new_status.is_some() { - if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await + if chat.is_protected() { + if let VerifiedEncryption::NotVerified(err) = + has_verified_encryption(context, mime_parser, from_id, to_ids, chat.typ).await? { warn!(context, "Verification problem: {err:#}."); let s = format!("{err}. See 'Info' for more details"); mime_parser.repl_msg_by_error(&s); - } else { - // change chat protection only when verification check passes - if let Some(new_status) = new_status { - if chat_id - .update_timestamp( - context, - Param::ProtectionSettingsTimestamp, - sent_timestamp, - ) - .await? - { - if let Err(e) = chat_id.inner_set_protection(context, new_status).await { - chat::add_info_msg( - context, - chat_id, - &format!("Cannot set protection: {e}"), - sort_timestamp, - ) - .await?; - // do not return an error as this would result in retrying the message - } - } - better_msg = Some(context.stock_protection_msg(new_status, from_id).await); - } } } } @@ -1268,7 +1295,7 @@ RETURNING id if let Some(replace_msg_id) = replace_msg_id { // "Replace" placeholder with a message that has no parts. - replace_msg_id.delete_from_db(context).await?; + replace_msg_id.trash(context).await?; } chat_id.unarchive_if_not_muted(context, state).await?; @@ -1375,25 +1402,43 @@ async fn calc_sort_timestamp( context: &Context, message_timestamp: i64, chat_id: ChatId, - is_fresh_msg: bool, + always_sort_to_bottom: bool, + incoming: bool, ) -> Result { let mut sort_timestamp = message_timestamp; - // get newest non fresh message for this chat - // update sort_timestamp if less than that - if is_fresh_msg { - let last_msg_time: Option = context + let last_msg_time: Option = if always_sort_to_bottom { + // get newest message for this chat + context + .sql + .query_get_value( + "SELECT MAX(timestamp) FROM msgs WHERE chat_id=?", + (chat_id,), + ) + .await? + } else if incoming { + // get newest non fresh message for this chat. + + // If a user hasn't been online for some time, the Inbox is fetched first and then the + // Sentbox. In order for Inbox and Sent messages to be allowed to mingle, outgoing messages + // are purely sorted by their sent timestamp. NB: The Inbox must be fetched first otherwise + // Inbox messages would be always below old Sentbox messages. We could take in the query + // below only incoming messages, but then new incoming messages would mingle with just sent + // outgoing ones and apear somewhere in the middle of the chat. + context .sql .query_get_value( "SELECT MAX(timestamp) FROM msgs WHERE chat_id=? AND state>?", (chat_id, MessageState::InFresh), ) - .await?; + .await? + } else { + None + }; - if let Some(last_msg_time) = last_msg_time { - if last_msg_time > sort_timestamp { - sort_timestamp = last_msg_time; - } + if let Some(last_msg_time) = last_msg_time { + if last_msg_time > sort_timestamp { + sort_timestamp = last_msg_time; } } @@ -1546,7 +1591,9 @@ async fn create_or_lookup_group( } let create_protected = if mime_parser.get_header(HeaderDef::ChatVerified).is_some() { - if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await { + if let VerifiedEncryption::NotVerified(err) = + has_verified_encryption(context, mime_parser, from_id, to_ids, Chattype::Group).await? + { warn!(context, "Verification problem: {err:#}."); let s = format!("{err}. See 'Info' for more details"); mime_parser.repl_msg_by_error(&s); @@ -1705,6 +1752,22 @@ async fn apply_group_changes( allow_member_list_changes }; + if mime_parser.get_header(HeaderDef::ChatVerified).is_some() { + if let VerifiedEncryption::NotVerified(err) = + has_verified_encryption(context, mime_parser, from_id, to_ids, chat.typ).await? + { + warn!(context, "Verification problem: {err:#}."); + let s = format!("{err}. See 'Info' for more details"); + mime_parser.repl_msg_by_error(&s); + } + + if !chat.is_protected() { + chat_id + .inner_set_protection(context, ProtectionStatus::Protected) + .await?; + } + } + if let Some(removed_addr) = mime_parser.get_header(HeaderDef::ChatGroupMemberRemoved) { removed_id = Contact::lookup_id_by_addr(context, removed_addr, Origin::Unknown).await?; @@ -1785,20 +1848,6 @@ async fn apply_group_changes( } } - if mime_parser.get_header(HeaderDef::ChatVerified).is_some() { - if let Err(err) = check_verified_properties(context, mime_parser, from_id, to_ids).await { - warn!(context, "Verification problem: {err:#}."); - let s = format!("{err}. See 'Info' for more details"); - mime_parser.repl_msg_by_error(&s); - } - - if !chat.is_protected() { - chat_id - .inner_set_protection(context, ProtectionStatus::Protected) - .await?; - } - } - if allow_member_list_changes { let mut new_members = HashSet::from_iter(to_ids.iter().copied()); new_members.insert(ContactId::SELF); @@ -2191,49 +2240,55 @@ async fn create_adhoc_group( Ok(Some(new_chat_id)) } -async fn check_verified_properties( +enum VerifiedEncryption { + Verified, + NotVerified(String), // The string contains the reason why it's not verified +} + +/// Checks whether the message is allowed to appear in a protected chat. +/// +/// This means that it is encrypted, signed with a verified key, +/// and if it's a group, all the recipients are verified. +async fn has_verified_encryption( context: &Context, mimeparser: &MimeMessage, from_id: ContactId, to_ids: &[ContactId], -) -> Result<()> { - let contact = Contact::get_by_id(context, from_id).await?; + chat_type: Chattype, +) -> Result { + use VerifiedEncryption::*; - ensure!(mimeparser.was_encrypted(), "This message is not encrypted"); - - if mimeparser.get_header(HeaderDef::ChatVerified).is_none() { - // we do not fail here currently, this would exclude (a) non-deltas - // and (b) deltas with different protection views across multiple devices. - // for group creation or protection enabled/disabled, however, Chat-Verified is respected. - warn!( - context, - "{} did not mark message as protected.", - contact.get_addr() - ); + if from_id == ContactId::SELF && chat_type == Chattype::Single { + // For outgoing emails in the 1:1 chat, we have an exception that + // they are allowed to be unencrypted: + // 1. They can't be an attack (they are outgoing, not incoming) + // 2. Probably the unencryptedness is just a temporary state, after all + // the user obviously still uses DC + // -> Showing info messages everytime would be a lot of noise + // 3. The info messages that are shown to the user ("Your chat partner + // likely reinstalled DC" or similar) would be wrong. + return Ok(Verified); } + if !mimeparser.was_encrypted() { + return Ok(NotVerified("This message is not encrypted".to_string())); + }; + // ensure, the contact is verified // and the message is signed with a verified key of the sender. // this check is skipped for SELF as there is no proper SELF-peerstate // and results in group-splits otherwise. if from_id != ContactId::SELF { - let peerstate = Peerstate::from_addr(context, contact.get_addr()).await?; + let Some(peerstate) = &mimeparser.decryption_info.peerstate else { + return Ok(NotVerified( + "No peerstate, the contact isn't verified".to_string(), + )); + }; - if peerstate.is_none() - || contact.is_verified_ex(context, peerstate.as_ref()).await? - != VerifiedStatus::BidirectVerified - { - bail!( - "Sender of this message is not verified: {}", - contact.get_addr() - ); - } - - if let Some(peerstate) = peerstate { - ensure!( - peerstate.has_verified_key(&mimeparser.signatures), - "The message was sent with non-verified encryption" - ); + if !peerstate.has_verified_key(&mimeparser.signatures) { + return Ok(NotVerified( + "The message was sent with non-verified encryption".to_string(), + )); } } @@ -2245,7 +2300,7 @@ async fn check_verified_properties( .collect::>(); if to_ids.is_empty() { - return Ok(()); + return Ok(Verified); } let rows = context @@ -2269,10 +2324,12 @@ async fn check_verified_properties( ) .await?; + let contact = Contact::get_by_id(context, from_id).await?; + for (to_addr, mut is_verified) in rows { info!( context, - "check_verified_properties: {:?} self={:?}.", + "has_verified_encryption: {:?} self={:?}.", to_addr, context.is_self_addr(&to_addr).await ); @@ -2306,13 +2363,13 @@ async fn check_verified_properties( } } if !is_verified { - bail!( + return Ok(NotVerified(format!( "{} is not a member of this protected chat", - to_addr.to_string() - ); + to_addr + ))); } } - Ok(()) + Ok(Verified) } /// Returns the last message referenced from `References` header if it is in the database. diff --git a/src/receive_imf/tests.rs b/src/receive_imf/tests.rs index 25de7a7d9..1b1476313 100644 --- a/src/receive_imf/tests.rs +++ b/src/receive_imf/tests.rs @@ -2945,7 +2945,7 @@ async fn test_outgoing_private_reply_multidevice() -> Result<()> { let received = alice2.get_last_msg().await; // That's a regression test for https://github.com/deltachat/deltachat-core-rust/issues/2949: - assert_eq!(received.chat_id, alice2.get_chat(&bob).await.unwrap().id); + assert_eq!(received.chat_id, alice2.get_chat(&bob).await.id); let alice2_bob_contact = alice2.add_or_lookup_contact(&bob).await; assert_eq!(received.from_id, ContactId::SELF); diff --git a/src/scheduler.rs b/src/scheduler.rs index 59b805e5d..a30df79bb 100644 --- a/src/scheduler.rs +++ b/src/scheduler.rs @@ -905,7 +905,7 @@ impl Scheduler { // Actually shutdown tasks. let timeout_duration = std::time::Duration::from_secs(30); - for b in once(self.inbox).chain(self.oboxes.into_iter()) { + for b in once(self.inbox).chain(self.oboxes) { tokio::time::timeout(timeout_duration, b.handle) .await .log_err(context) diff --git a/src/securejoin.rs b/src/securejoin.rs index 5ffa387ed..df045ad4c 100644 --- a/src/securejoin.rs +++ b/src/securejoin.rs @@ -6,15 +6,15 @@ use anyhow::{bail, Context as _, Error, Result}; use percent_encoding::{utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC}; use crate::aheader::EncryptPreference; -use crate::chat::{self, Chat, ChatId, ChatIdBlocked}; +use crate::chat::{self, Chat, ChatId, ChatIdBlocked, ProtectionStatus}; use crate::config::Config; -use crate::constants::Blocked; +use crate::constants::{Blocked, Chattype}; use crate::contact::{Contact, ContactId, Origin, VerifiedStatus}; use crate::context::Context; use crate::e2ee::ensure_secret_key_exists; use crate::events::EventType; use crate::headerdef::HeaderDef; -use crate::key::{DcKey, Fingerprint, SignedPublicKey}; +use crate::key::{load_self_public_key, DcKey, Fingerprint}; use crate::message::{Message, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::Param; @@ -130,7 +130,7 @@ pub async fn get_securejoin_qr(context: &Context, group: Option) -> Resu } async fn get_self_fingerprint(context: &Context) -> Option { - match SignedPublicKey::load_self(context).await { + match load_self_public_key(context).await { Ok(key) => Some(key.fingerprint()), Err(_) => { warn!(context, "get_self_fingerprint(): failed to load key"); @@ -701,6 +701,22 @@ async fn secure_connection_established( let contact = Contact::get_by_id(context, contact_id).await?; let msg = stock_str::contact_verified(context, &contact).await; chat::add_info_msg(context, chat_id, &msg, time()).await?; + if context + .get_config_bool(Config::VerifiedOneOnOneChats) + .await? + { + let chat = Chat::load_from_db(context, chat_id).await?; + if chat.typ == Chattype::Single { + chat_id + .set_protection( + context, + ProtectionStatus::Protected, + time(), + Some(contact_id), + ) + .await?; + } + } context.emit_event(EventType::ChatModified(chat_id)); Ok(()) } @@ -778,11 +794,12 @@ mod tests { use crate::chat; use crate::chat::ProtectionStatus; use crate::chatlist::Chatlist; - use crate::constants::Chattype; use crate::contact::ContactAddress; use crate::contact::VerifiedStatus; use crate::peerstate::Peerstate; use crate::receive_imf::receive_imf; + use crate::stock_str::chat_protection_enabled; + use crate::test_utils::get_chat_msg; use crate::test_utils::{TestContext, TestContextManager}; use crate::tools::EmailAddress; @@ -791,6 +808,14 @@ mod tests { let mut tcm = TestContextManager::new(); let alice = tcm.alice().await; let bob = tcm.bob().await; + alice + .set_config(Config::VerifiedOneOnOneChats, Some("1")) + .await + .unwrap(); + bob.set_config(Config::VerifiedOneOnOneChats, Some("1")) + .await + .unwrap(); + assert_eq!( Chatlist::try_load(&alice, 0, None, None) .await @@ -874,10 +899,7 @@ mod tests { "vc-request-with-auth" ); assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some()); - let bob_fp = SignedPublicKey::load_self(&bob.ctx) - .await - .unwrap() - .fingerprint(); + let bob_fp = load_self_public_key(&bob.ctx).await.unwrap().fingerprint(); assert_eq!( *msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(), bob_fp.hex() @@ -921,7 +943,7 @@ mod tests { // Check Alice got the verified message in her 1:1 chat. { let chat = alice.create_chat(&bob).await; - let msg_id = chat::get_chat_msgs(&alice.ctx, chat.get_id()) + let msg_ids: Vec<_> = chat::get_chat_msgs(&alice.ctx, chat.get_id()) .await .unwrap() .into_iter() @@ -929,11 +951,17 @@ mod tests { chat::ChatItem::Message { msg_id } => Some(msg_id), _ => None, }) - .max() - .expect("No messages in Alice's 1:1 chat"); - let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap(); - assert!(msg.is_info()); - assert!(msg.get_text().contains("bob@example.net verified")); + .collect(); + assert_eq!(msg_ids.len(), 2); + + let msg0 = Message::load_from_db(&alice.ctx, msg_ids[0]).await.unwrap(); + assert!(msg0.is_info()); + assert!(msg0.get_text().contains("bob@example.net verified")); + + let msg1 = Message::load_from_db(&alice.ctx, msg_ids[1]).await.unwrap(); + assert!(msg1.is_info()); + let expected_text = chat_protection_enabled(&alice).await; + assert_eq!(msg1.get_text(), expected_text); } // Check Alice sent the right message to Bob. @@ -969,7 +997,7 @@ mod tests { // Check Bob got the verified message in his 1:1 chat. { let chat = bob.create_chat(&alice).await; - let msg_id = chat::get_chat_msgs(&bob.ctx, chat.get_id()) + let msg_ids: Vec<_> = chat::get_chat_msgs(&bob.ctx, chat.get_id()) .await .unwrap() .into_iter() @@ -977,11 +1005,16 @@ mod tests { chat::ChatItem::Message { msg_id } => Some(msg_id), _ => None, }) - .max() - .expect("No messages in Bob's 1:1 chat"); - let msg = Message::load_from_db(&bob.ctx, msg_id).await.unwrap(); - assert!(msg.is_info()); - assert!(msg.get_text().contains("alice@example.org verified")); + .collect(); + + let msg0 = Message::load_from_db(&bob.ctx, msg_ids[0]).await.unwrap(); + assert!(msg0.is_info()); + assert!(msg0.get_text().contains("alice@example.org verified")); + + let msg1 = Message::load_from_db(&bob.ctx, msg_ids[1]).await.unwrap(); + assert!(msg1.is_info()); + let expected_text = chat_protection_enabled(&bob).await; + assert_eq!(msg1.get_text(), expected_text); } // Check Bob sent the final message @@ -1008,7 +1041,7 @@ mod tests { let bob = tcm.bob().await; // Ensure Bob knows Alice_FP - let alice_pubkey = SignedPublicKey::load_self(&alice.ctx).await?; + let alice_pubkey = load_self_public_key(&alice.ctx).await?; let peerstate = Peerstate { addr: "alice@example.org".into(), last_seen: 10, @@ -1062,7 +1095,7 @@ mod tests { "vc-request-with-auth" ); assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some()); - let bob_fp = SignedPublicKey::load_self(&bob.ctx).await?.fingerprint(); + let bob_fp = load_self_public_key(&bob.ctx).await?.fingerprint(); assert_eq!( *msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(), bob_fp.hex() @@ -1233,7 +1266,7 @@ mod tests { "vg-request-with-auth" ); assert!(msg.get_header(HeaderDef::SecureJoinAuth).is_some()); - let bob_fp = SignedPublicKey::load_self(&bob.ctx).await?.fingerprint(); + let bob_fp = load_self_public_key(&bob.ctx).await?.fingerprint(); assert_eq!( *msg.get_header(HeaderDef::SecureJoinFingerprint).unwrap(), bob_fp.hex() @@ -1269,26 +1302,17 @@ mod tests { // Now Alice's chat with Bob should still be hidden, the verified message should // appear in the group chat. - let chat = alice - .get_chat(&bob) - .await - .expect("Alice has no 1:1 chat with bob"); + let chat = alice.get_chat(&bob).await; assert_eq!( chat.blocked, Blocked::Yes, "Alice's 1:1 chat with Bob is not hidden" ); - let msg_id = chat::get_chat_msgs(&alice.ctx, alice_chatid) - .await - .unwrap() - .into_iter() - .filter_map(|item| match item { - chat::ChatItem::Message { msg_id } => Some(msg_id), - _ => None, - }) - .min() - .expect("No messages in Alice's group chat"); - let msg = Message::load_from_db(&alice.ctx, msg_id).await.unwrap(); + // There should be 3 messages in the chat: + // - The ChatProtectionEnabled message + // - bob@example.net verified + // - You added member bob@example.net + let msg = get_chat_msg(&alice, alice_chatid, 1, 3).await; assert!(msg.is_info()); assert!(msg.get_text().contains("bob@example.net verified")); } @@ -1313,10 +1337,7 @@ mod tests { contact_alice.is_verified(&bob.ctx).await?, VerifiedStatus::BidirectVerified ); - let chat = bob - .get_chat(&alice) - .await - .expect("Bob has no 1:1 chat with Alice"); + let chat = bob.get_chat(&alice).await; assert_eq!( chat.blocked, Blocked::Yes, diff --git a/src/securejoin/bob.rs b/src/securejoin/bob.rs index 674fa94f7..73903e2ce 100644 --- a/src/securejoin/bob.rs +++ b/src/securejoin/bob.rs @@ -9,6 +9,7 @@ use super::bobstate::{BobHandshakeStage, BobState}; use super::qrinvite::QrInvite; use super::HandshakeMessage; use crate::chat::{is_contact_in_chat, ChatId, ProtectionStatus}; +use crate::config::Config; use crate::constants::{Blocked, Chattype}; use crate::contact::Contact; use crate::context::Context; @@ -222,6 +223,22 @@ impl BobState { let msg = stock_str::contact_verified(context, &contact).await; let chat_id = self.joining_chat_id(context).await?; chat::add_info_msg(context, chat_id, &msg, time()).await?; + + if context + .get_config_bool(Config::VerifiedOneOnOneChats) + .await? + && chat_id == self.alice_chat() + { + chat_id + .set_protection( + context, + ProtectionStatus::Protected, + time(), + Some(contact.id), + ) + .await?; + } + context.emit_event(EventType::ChatModified(chat_id)); Ok(()) } diff --git a/src/securejoin/bobstate.rs b/src/securejoin/bobstate.rs index 2c48c54f2..b46b45bed 100644 --- a/src/securejoin/bobstate.rs +++ b/src/securejoin/bobstate.rs @@ -17,7 +17,7 @@ use crate::contact::{Contact, Origin}; use crate::context::Context; use crate::events::EventType; use crate::headerdef::HeaderDef; -use crate::key::{DcKey, SignedPublicKey}; +use crate::key::{load_self_public_key, DcKey}; use crate::message::{Message, Viewtype}; use crate::mimeparser::{MimeMessage, SystemMessage}; use crate::param::Param; @@ -448,7 +448,7 @@ async fn send_handshake_message( }; // Sends our own fingerprint in the Secure-Join-Fingerprint header. - let bob_fp = SignedPublicKey::load_self(context).await?.fingerprint(); + let bob_fp = load_self_public_key(context).await?.fingerprint(); msg.param.set(Param::Arg3, bob_fp.hex()); // Sends the grpid in the Secure-Join-Group header. diff --git a/src/smtp.rs b/src/smtp.rs index f2998b8cc..c30f4626e 100644 --- a/src/smtp.rs +++ b/src/smtp.rs @@ -12,6 +12,7 @@ use tokio::task; use crate::config::Config; use crate::contact::{Contact, ContactId}; +use crate::context::Context; use crate::events::EventType; use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam}; use crate::message::Message; @@ -22,9 +23,9 @@ use crate::net::session::SessionBufStream; use crate::net::tls::wrap_tls; use crate::oauth2::get_oauth2_access_token; use crate::provider::Socket; +use crate::scheduler::connectivity::ConnectivityStore; use crate::socks::Socks5Config; use crate::sql; -use crate::{context::Context, scheduler::connectivity::ConnectivityStore}; /// SMTP write and read timeout. const SMTP_TIMEOUT: Duration = Duration::from_secs(30); diff --git a/src/sql/migrations.rs b/src/sql/migrations.rs index 8f3b24509..6e69311c5 100644 --- a/src/sql/migrations.rs +++ b/src/sql/migrations.rs @@ -730,6 +730,7 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid); ) .await?; } + if dbversion < 102 { sql.execute_migration( "CREATE TABLE download ( diff --git a/src/stock_str.rs b/src/stock_str.rs index 0dc305107..888c474c4 100644 --- a/src/stock_str.rs +++ b/src/stock_str.rs @@ -393,18 +393,6 @@ pub enum StockMessage { #[strum(props(fallback = "Message deletion timer is set to %1$s weeks by %2$s."))] MsgEphemeralTimerWeeksBy = 157, - #[strum(props(fallback = "You enabled chat protection."))] - YouEnabledProtection = 158, - - #[strum(props(fallback = "Chat protection enabled by %1$s."))] - ProtectionEnabledBy = 159, - - #[strum(props(fallback = "You disabled chat protection."))] - YouDisabledProtection = 160, - - #[strum(props(fallback = "Chat protection disabled by %1$s."))] - ProtectionDisabledBy = 161, - #[strum(props(fallback = "Scan to set up second device for %1$s"))] BackupTransferQr = 162, @@ -419,6 +407,12 @@ pub enum StockMessage { #[strum(props(fallback = "I left the group."))] MsgILeftGroup = 166, + + #[strum(props(fallback = "Messages are guaranteed to be end-to-end encrypted from now on."))] + ChatProtectionEnabled = 170, + + #[strum(props(fallback = "%1$s sent a message from another device."))] + ChatProtectionDisabled = 171, } impl StockMessage { @@ -515,13 +509,21 @@ trait StockStringMods: AsRef + Sized { } impl ContactId { - /// Get contact name for stock string. - async fn get_stock_name(self, context: &Context) -> String { + /// Get contact name and address for stock string, e.g. `Bob (bob@example.net)` + async fn get_stock_name_n_addr(self, context: &Context) -> String { Contact::get_by_id(context, self) .await .map(|contact| contact.get_name_n_addr()) .unwrap_or_else(|_| self.to_string()) } + + /// Get contact name, e.g. `Bob`, or `bob@exmple.net` if no name is set. + async fn get_stock_name(self, context: &Context) -> String { + Contact::get_by_id(context, self) + .await + .map(|contact| contact.get_display_name().to_string()) + .unwrap_or_else(|_| self.to_string()) + } } impl StockStringMods for String {} @@ -583,7 +585,7 @@ pub(crate) async fn msg_grp_name( .await .replace1(from_group) .replace2(to_group) - .replace3(&by_contact.get_stock_name(context).await) + .replace3(&by_contact.get_stock_name_n_addr(context).await) } } @@ -593,7 +595,7 @@ pub(crate) async fn msg_grp_img_changed(context: &Context, by_contact: ContactId } else { translated(context, StockMessage::MsgGrpImgChangedBy) .await - .replace1(&by_contact.get_stock_name(context).await) + .replace1(&by_contact.get_stock_name_n_addr(context).await) } } @@ -640,7 +642,7 @@ pub(crate) async fn msg_add_member_local( translated(context, StockMessage::MsgAddMemberBy) .await .replace1(whom) - .replace2(&by_contact.get_stock_name(context).await) + .replace2(&by_contact.get_stock_name_n_addr(context).await) } } @@ -687,7 +689,7 @@ pub(crate) async fn msg_del_member_local( translated(context, StockMessage::MsgDelMemberBy) .await .replace1(whom) - .replace2(&by_contact.get_stock_name(context).await) + .replace2(&by_contact.get_stock_name_n_addr(context).await) } } @@ -703,7 +705,7 @@ pub(crate) async fn msg_group_left_local(context: &Context, by_contact: ContactI } else { translated(context, StockMessage::MsgGroupLeftBy) .await - .replace1(&by_contact.get_stock_name(context).await) + .replace1(&by_contact.get_stock_name_n_addr(context).await) } } @@ -756,7 +758,7 @@ pub(crate) async fn msg_grp_img_deleted(context: &Context, by_contact: ContactId } else { translated(context, StockMessage::MsgGrpImgDeletedBy) .await - .replace1(&by_contact.get_stock_name(context).await) + .replace1(&by_contact.get_stock_name_n_addr(context).await) } } @@ -782,13 +784,9 @@ pub(crate) async fn secure_join_started( /// Stock string: `%1$s replied, waiting for being added to the group…`. pub(crate) async fn secure_join_replies(context: &Context, contact_id: ContactId) -> String { - if let Ok(contact) = Contact::get_by_id(context, contact_id).await { - translated(context, StockMessage::SecureJoinReplies) - .await - .replace1(contact.get_display_name()) - } else { - format!("secure_join_replies: unknown contact {contact_id}") - } + translated(context, StockMessage::SecureJoinReplies) + .await + .replace1(&contact_id.get_stock_name(context).await) } /// Stock string: `Scan to chat with %1$s`. @@ -881,7 +879,7 @@ pub(crate) async fn msg_location_enabled_by(context: &Context, contact: ContactI } else { translated(context, StockMessage::MsgLocationEnabledBy) .await - .replace1(&contact.get_stock_name(context).await) + .replace1(&contact.get_stock_name_n_addr(context).await) } } @@ -950,7 +948,7 @@ pub(crate) async fn msg_ephemeral_timer_disabled( } else { translated(context, StockMessage::MsgEphemeralTimerDisabledBy) .await - .replace1(&by_contact.get_stock_name(context).await) + .replace1(&by_contact.get_stock_name_n_addr(context).await) } } @@ -968,7 +966,7 @@ pub(crate) async fn msg_ephemeral_timer_enabled( translated(context, StockMessage::MsgEphemeralTimerEnabledBy) .await .replace1(timer) - .replace2(&by_contact.get_stock_name(context).await) + .replace2(&by_contact.get_stock_name_n_addr(context).await) } } @@ -979,7 +977,7 @@ pub(crate) async fn msg_ephemeral_timer_minute(context: &Context, by_contact: Co } else { translated(context, StockMessage::MsgEphemeralTimerMinuteBy) .await - .replace1(&by_contact.get_stock_name(context).await) + .replace1(&by_contact.get_stock_name_n_addr(context).await) } } @@ -990,7 +988,7 @@ pub(crate) async fn msg_ephemeral_timer_hour(context: &Context, by_contact: Cont } else { translated(context, StockMessage::MsgEphemeralTimerHourBy) .await - .replace1(&by_contact.get_stock_name(context).await) + .replace1(&by_contact.get_stock_name_n_addr(context).await) } } @@ -1001,7 +999,7 @@ pub(crate) async fn msg_ephemeral_timer_day(context: &Context, by_contact: Conta } else { translated(context, StockMessage::MsgEphemeralTimerDayBy) .await - .replace1(&by_contact.get_stock_name(context).await) + .replace1(&by_contact.get_stock_name_n_addr(context).await) } } @@ -1012,7 +1010,7 @@ pub(crate) async fn msg_ephemeral_timer_week(context: &Context, by_contact: Cont } else { translated(context, StockMessage::MsgEphemeralTimerWeekBy) .await - .replace1(&by_contact.get_stock_name(context).await) + .replace1(&by_contact.get_stock_name_n_addr(context).await) } } @@ -1053,26 +1051,16 @@ pub(crate) async fn error_no_network(context: &Context) -> String { translated(context, StockMessage::ErrorNoNetwork).await } -/// Stock string: `Chat protection enabled.`. -pub(crate) async fn protection_enabled(context: &Context, by_contact: ContactId) -> String { - if by_contact == ContactId::SELF { - translated(context, StockMessage::YouEnabledProtection).await - } else { - translated(context, StockMessage::ProtectionEnabledBy) - .await - .replace1(&by_contact.get_stock_name(context).await) - } +/// Stock string: `Messages are guaranteed to be end-to-end encrypted from now on.` +pub(crate) async fn chat_protection_enabled(context: &Context) -> String { + translated(context, StockMessage::ChatProtectionEnabled).await } -/// Stock string: `Chat protection disabled.`. -pub(crate) async fn protection_disabled(context: &Context, by_contact: ContactId) -> String { - if by_contact == ContactId::SELF { - translated(context, StockMessage::YouDisabledProtection).await - } else { - translated(context, StockMessage::ProtectionDisabledBy) - .await - .replace1(&by_contact.get_stock_name(context).await) - } +/// Stock string: `%1$s sent a message from another device.` +pub(crate) async fn chat_protection_disabled(context: &Context, contact_id: ContactId) -> String { + translated(context, StockMessage::ChatProtectionDisabled) + .await + .replace1(&contact_id.get_stock_name(context).await) } /// Stock string: `Reply`. @@ -1104,7 +1092,7 @@ pub(crate) async fn msg_ephemeral_timer_minutes( translated(context, StockMessage::MsgEphemeralTimerMinutesBy) .await .replace1(minutes) - .replace2(&by_contact.get_stock_name(context).await) + .replace2(&by_contact.get_stock_name_n_addr(context).await) } } @@ -1122,7 +1110,7 @@ pub(crate) async fn msg_ephemeral_timer_hours( translated(context, StockMessage::MsgEphemeralTimerHoursBy) .await .replace1(hours) - .replace2(&by_contact.get_stock_name(context).await) + .replace2(&by_contact.get_stock_name_n_addr(context).await) } } @@ -1140,7 +1128,7 @@ pub(crate) async fn msg_ephemeral_timer_days( translated(context, StockMessage::MsgEphemeralTimerDaysBy) .await .replace1(days) - .replace2(&by_contact.get_stock_name(context).await) + .replace2(&by_contact.get_stock_name_n_addr(context).await) } } @@ -1158,7 +1146,7 @@ pub(crate) async fn msg_ephemeral_timer_weeks( translated(context, StockMessage::MsgEphemeralTimerWeeksBy) .await .replace1(weeks) - .replace2(&by_contact.get_stock_name(context).await) + .replace2(&by_contact.get_stock_name_n_addr(context).await) } } @@ -1332,11 +1320,19 @@ impl Context { pub(crate) async fn stock_protection_msg( &self, protect: ProtectionStatus, - from_id: ContactId, + contact_id: Option, ) -> String { match protect { - ProtectionStatus::Unprotected => protection_enabled(self, from_id).await, - ProtectionStatus::Protected => protection_disabled(self, from_id).await, + ProtectionStatus::Unprotected | ProtectionStatus::ProtectionBroken => { + if let Some(contact_id) = contact_id { + chat_protection_disabled(self, contact_id).await + } else { + // In a group chat, it's not possible to downgrade verification. + // In a 1:1 chat, the `contact_id` always has to be provided. + "[Error] No contact_id given".to_string() + } + } + ProtectionStatus::Protected => chat_protection_enabled(self).await, } } diff --git a/src/summary.rs b/src/summary.rs index 731f4edf0..dfeab5f59 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -82,7 +82,7 @@ impl Summary { .map(SummaryPrefix::Username) } } - Chattype::Single | Chattype::Undefined => None, + Chattype::Single => None, } }; diff --git a/src/test_utils.rs b/src/test_utils.rs index 596c0cba9..fe02cfa1f 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -27,15 +27,19 @@ use crate::chat::{ }; use crate::chatlist::Chatlist; use crate::config::Config; -use crate::constants::Chattype; +use crate::constants::{Blocked, Chattype}; use crate::constants::{DC_GCL_NO_SPECIALS, DC_MSG_ID_DAYMARKER}; use crate::contact::{Contact, ContactAddress, ContactId, Modifier, Origin}; use crate::context::Context; +use crate::e2ee::EncryptHelper; use crate::events::{Event, EventType, Events}; -use crate::key::{self, DcKey, KeyPair, KeyPairUse}; +use crate::key::{self, DcKey, KeyPairUse}; use crate::message::{update_msg_state, Message, MessageState, MsgId, Viewtype}; -use crate::mimeparser::MimeMessage; +use crate::mimeparser::{MimeMessage, SystemMessage}; +use crate::peerstate::Peerstate; +use crate::pgp::KeyPair; use crate::receive_imf::receive_imf; +use crate::securejoin::{get_securejoin_qr, join_securejoin}; use crate::stock_str::StockStrings; use crate::tools::EmailAddress; @@ -115,6 +119,10 @@ impl TestContextManager { msg: &str, ) -> Message { let received_msg = self.send_recv(from, to, msg).await; + assert_eq!( + received_msg.chat_blocked, Blocked::Request, + "`send_recv_accept()` is meant to be used for chat requests. Use `send_recv()` if the chat is already accepted." + ); received_msg.chat_id.accept(to).await.unwrap(); received_msg } @@ -158,6 +166,27 @@ impl TestContextManager { new_addr ); } + + pub async fn execute_securejoin(&self, scanner: &TestContext, scanned: &TestContext) { + self.section(&format!( + "{} scans {}'s QR code", + scanner.name(), + scanned.name() + )); + + let qr = get_securejoin_qr(&scanned.ctx, None).await.unwrap(); + join_securejoin(&scanner.ctx, &qr).await.unwrap(); + + loop { + if let Some(sent) = scanner.pop_sent_msg_opt(Duration::ZERO).await { + scanned.recv_msg(&sent).await; + } else if let Some(sent) = scanned.pop_sent_msg_opt(Duration::ZERO).await { + scanner.recv_msg(&sent).await; + } else { + break; + } + } + } } #[derive(Debug, Clone, Default)] @@ -562,19 +591,21 @@ impl TestContext { Contact::get_by_id(&self.ctx, contact_id).await.unwrap() } - /// Returns 1:1 [`Chat`] with another account, if it exists. + /// Returns 1:1 [`Chat`] with another account. Panics if it doesn't exist. /// /// This first creates a contact using the configured details on the other account, then - /// creates a 1:1 chat with this contact. - pub async fn get_chat(&self, other: &TestContext) -> Option { + /// gets the 1:1 chat with this contact. + pub async fn get_chat(&self, other: &TestContext) -> Chat { let contact = self.add_or_lookup_contact(other).await; - match ChatId::lookup_by_contact(&self.ctx, contact.id) + let chat_id = ChatId::lookup_by_contact(&self.ctx, contact.id) .await .unwrap() - { - Some(id) => Some(Chat::load_from_db(&self.ctx, id).await.unwrap()), - None => None, - } + .expect( + "There is no chat with this contact. \ + Hint: Use create_chat() instead of get_chat() if this is expected.", + ); + + Chat::load_from_db(&self.ctx, chat_id).await.unwrap() } /// Creates or returns an existing 1:1 [`Chat`] with another account. @@ -633,7 +664,6 @@ impl TestContext { res } - #[allow(unused)] pub async fn golden_test_chat(&self, chat_id: ChatId, filename: &str) { let filename = Path::new("test-data/golden/").join(filename); @@ -642,7 +672,7 @@ impl TestContext { // We're using `unwrap_or_default()` here so that if the file doesn't exist, // it can be created using `write` below. let expected = fs::read(&filename).await.unwrap_or_default(); - let expected = String::from_utf8(expected).unwrap(); + let expected = String::from_utf8(expected).unwrap().replace("\r\n", "\n"); if (std::env::var("UPDATE_GOLDEN_TESTS") == Ok("1".to_string())) && actual != expected { fs::write(&filename, &actual) .await @@ -660,8 +690,6 @@ impl TestContext { /// You can use this to debug your test by printing the entire chat conversation. // This code is mainly the same as `log_msglist` in `cmdline.rs`, so one day, we could // merge them to a public function in the `deltachat` crate. - #[allow(dead_code)] - #[allow(clippy::indexing_slicing)] async fn display_chat(&self, chat_id: ChatId) -> String { let mut res = String::new(); @@ -886,7 +914,7 @@ pub fn alice_keypair() -> KeyPair { let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/alice-secret.asc")) .unwrap() .0; - key::KeyPair { + KeyPair { addr, public, secret, @@ -904,7 +932,7 @@ pub fn bob_keypair() -> KeyPair { let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/bob-secret.asc")) .unwrap() .0; - key::KeyPair { + KeyPair { addr, public, secret, @@ -914,7 +942,7 @@ pub fn bob_keypair() -> KeyPair { /// Load a pre-generated keypair for fiona@example.net from disk. /// /// Like [alice_keypair] but a different key and identity. -pub fn fiona_keypair() -> key::KeyPair { +pub fn fiona_keypair() -> KeyPair { let addr = EmailAddress::new("fiona@example.net").unwrap(); let public = key::SignedPublicKey::from_asc(include_str!("../test-data/key/fiona-public.asc")) .unwrap() @@ -922,7 +950,7 @@ pub fn fiona_keypair() -> key::KeyPair { let secret = key::SignedSecretKey::from_asc(include_str!("../test-data/key/fiona-secret.asc")) .unwrap() .0; - key::KeyPair { + KeyPair { addr, public, secret, @@ -1014,6 +1042,26 @@ fn print_logevent(logevent: &LogEvent) { } } +/// Saves the other account's public key as verified. +pub(crate) async fn mark_as_verified(this: &TestContext, other: &TestContext) { + let mut peerstate = Peerstate::from_header( + &EncryptHelper::new(other).await.unwrap().get_aheader(), + // We have to give 0 as the time, not the current time: + // The time is going to be saved in peerstate.last_seen. + // The code in `peerstate.rs` then compares `if message_time > self.last_seen`, + // and many similar checks in peerstate.rs, and doesn't allow changes otherwise. + // Giving the current time would mean that message_time == peerstate.last_seen, + // so changes would not be allowed. + // This might lead to flaky tests. + 0, + ); + + peerstate.verified_key = peerstate.public_key.clone(); + peerstate.verified_key_fingerprint = peerstate.public_key_fingerprint.clone(); + + peerstate.save_to_db(&this.sql).await.unwrap(); +} + /// Pretty-print an event to stdout /// /// Done during tests this is captured by `cargo test` and associated with the test itself. @@ -1120,7 +1168,17 @@ async fn write_msg(context: &Context, prefix: &str, msg: &Message, buf: &mut Str } else { "[FRESH]" }, - if msg.is_info() { "[INFO]" } else { "" }, + if msg.is_info() { + if msg.get_info_type() == SystemMessage::ChatProtectionEnabled { + "[INFO πŸ›‘οΈ]" + } else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled { + "[INFO πŸ›‘οΈβŒ]" + } else { + "[INFO]" + } + } else { + "" + }, if msg.get_viewtype() == Viewtype::VideochatInvitation { format!( "[VIDEOCHAT-INVITATION: {}, type={}]", diff --git a/src/tests.rs b/src/tests.rs index 9067f1962..3b417c0d4 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1 +1,2 @@ mod aeap; +mod verified_chats; diff --git a/src/tests/aeap.rs b/src/tests/aeap.rs index 0c43e8c17..db7cedd5f 100644 --- a/src/tests/aeap.rs +++ b/src/tests/aeap.rs @@ -8,10 +8,10 @@ use crate::contact; use crate::contact::Contact; use crate::contact::ContactId; use crate::message::Message; -use crate::peerstate; use crate::peerstate::Peerstate; use crate::receive_imf::receive_imf; use crate::stock_str; +use crate::test_utils::mark_as_verified; use crate::test_utils::TestContext; use crate::test_utils::TestContextManager; @@ -134,11 +134,11 @@ async fn check_aeap_transition( let fiona = tcm.fiona().await; tcm.send_recv_accept(&fiona, &bob, "Hi").await; - tcm.send_recv_accept(&bob, &fiona, "Hi back").await; + tcm.send_recv(&bob, &fiona, "Hi back").await; } tcm.send_recv_accept(&alice, &bob, "Hi").await; - tcm.send_recv_accept(&bob, &alice, "Hi back").await; + tcm.send_recv(&bob, &alice, "Hi back").await; if verified { mark_as_verified(&alice, &bob).await; @@ -327,19 +327,6 @@ async fn check_no_transition_done(groups: &[ChatId], old_alice_addr: &str, bob: } } -async fn mark_as_verified(this: &TestContext, other: &TestContext) { - let other_addr = other.get_primary_self_addr().await.unwrap(); - let mut peerstate = peerstate::Peerstate::from_addr(this, &other_addr) - .await - .unwrap() - .unwrap(); - - peerstate.verified_key = peerstate.public_key.clone(); - peerstate.verified_key_fingerprint = peerstate.public_key_fingerprint.clone(); - - peerstate.save_to_db(&this.sql).await.unwrap(); -} - async fn get_last_info_msg(t: &TestContext, chat_id: ChatId) -> Option { let msgs = chat::get_chat_msgs_ex( &t.ctx, @@ -368,7 +355,7 @@ async fn test_aeap_replay_attack() -> Result<()> { let bob = tcm.bob().await; tcm.send_recv_accept(&alice, &bob, "Hi").await; - tcm.send_recv_accept(&bob, &alice, "Hi back").await; + tcm.send_recv(&bob, &alice, "Hi back").await; let group = chat::create_group_chat(&bob, chat::ProtectionStatus::Unprotected, "Group 0").await?; diff --git a/src/tests/verified_chats.rs b/src/tests/verified_chats.rs new file mode 100644 index 000000000..413338929 --- /dev/null +++ b/src/tests/verified_chats.rs @@ -0,0 +1,770 @@ +use anyhow::Result; +use pretty_assertions::assert_eq; + +use crate::chat::{Chat, ProtectionStatus}; +use crate::chatlist::Chatlist; +use crate::config::Config; +use crate::constants::DC_GCL_FOR_FORWARDING; +use crate::contact::VerifiedStatus; +use crate::contact::{Contact, Origin}; +use crate::message::{Message, Viewtype}; +use crate::mimefactory::MimeFactory; +use crate::mimeparser::SystemMessage; +use crate::receive_imf::receive_imf; +use crate::stock_str; +use crate::test_utils::{get_chat_msg, mark_as_verified, TestContext, TestContextManager}; +use crate::{e2ee, message}; + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_verified_oneonone_chat_broken_by_classical() { + check_verified_oneonone_chat(true).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_verified_oneonone_chat_broken_by_device_change() { + check_verified_oneonone_chat(false).await; +} + +async fn check_verified_oneonone_chat(broken_by_classical_email: bool) { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + enable_verified_oneonone_chats(&[&alice, &bob]).await; + + tcm.execute_securejoin(&alice, &bob).await; + + assert_verified(&alice, &bob, ProtectionStatus::Protected).await; + assert_verified(&bob, &alice, ProtectionStatus::Protected).await; + + if broken_by_classical_email { + tcm.section("Bob uses a classical MUA to send a message to Alice"); + receive_imf( + &alice, + b"Subject: Re: Message from alice\r\n\ + From: \r\n\ + To: \r\n\ + Date: Mon, 12 Dec 2022 14:33:39 +0000\r\n\ + Message-ID: \r\n\ + \r\n\ + Heyho!\r\n", + false, + ) + .await + .unwrap() + .unwrap(); + } else { + tcm.section("Bob sets up another Delta Chat device"); + let bob2 = TestContext::new().await; + enable_verified_oneonone_chats(&[&bob2]).await; + bob2.set_name("bob2"); + bob2.configure_addr("bob@example.net").await; + + tcm.send_recv(&bob2, &alice, "Using another device now") + .await; + } + + // Bob's contact is still verified, but the chat isn't marked as protected anymore + assert_verified(&alice, &bob, ProtectionStatus::ProtectionBroken).await; + + tcm.section("Bob sends another message from DC"); + tcm.send_recv(&bob, &alice, "Using DC again").await; + + let contact = alice.add_or_lookup_contact(&bob).await; + assert_eq!( + contact.is_verified(&alice.ctx).await.unwrap(), + VerifiedStatus::BidirectVerified + ); + + // Bob's chat is marked as verified again + assert_verified(&alice, &bob, ProtectionStatus::Protected).await; +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_create_verified_oneonone_chat() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let fiona = tcm.fiona().await; + enable_verified_oneonone_chats(&[&alice, &bob, &fiona]).await; + + tcm.execute_securejoin(&alice, &bob).await; + tcm.execute_securejoin(&bob, &fiona).await; + assert_verified(&alice, &bob, ProtectionStatus::Protected).await; + assert_verified(&bob, &alice, ProtectionStatus::Protected).await; + assert_verified(&bob, &fiona, ProtectionStatus::Protected).await; + assert_verified(&fiona, &bob, ProtectionStatus::Protected).await; + + let group_id = bob + .create_group_with_members( + ProtectionStatus::Protected, + "Group with everyone", + &[&alice, &fiona], + ) + .await; + assert_eq!( + get_chat_msg(&bob, group_id, 0, 1).await.get_info_type(), + SystemMessage::ChatProtectionEnabled + ); + + { + let sent = bob.send_text(group_id, "Heyho").await; + alice.recv_msg(&sent).await; + + let msg = fiona.recv_msg(&sent).await; + assert_eq!( + get_chat_msg(&fiona, msg.chat_id, 0, 2) + .await + .get_info_type(), + SystemMessage::ChatProtectionEnabled + ); + } + + // Alice and Fiona should now be verified because of gossip + let alice_fiona_contact = alice.add_or_lookup_contact(&fiona).await; + assert_eq!( + alice_fiona_contact.is_verified(&alice).await.unwrap(), + VerifiedStatus::BidirectVerified + ); + + // As soon as Alice creates a chat with Fiona, it should directly be protected + { + let chat = alice.create_chat(&fiona).await; + assert!(chat.is_protected()); + + let msg = alice.get_last_msg().await; + let expected_text = stock_str::chat_protection_enabled(&alice).await; + assert_eq!(msg.text, expected_text); + } + + // Fiona should also see the chat as protected + { + let rcvd = tcm.send_recv(&alice, &fiona, "Hi Fiona").await; + let alice_fiona_id = rcvd.chat_id; + let chat = Chat::load_from_db(&fiona, alice_fiona_id).await?; + assert!(chat.is_protected()); + + let msg0 = get_chat_msg(&fiona, chat.id, 0, 2).await; + let expected_text = stock_str::chat_protection_enabled(&fiona).await; + assert_eq!(msg0.text, expected_text); + } + + tcm.section("Fiona reinstalls DC"); + drop(fiona); + + let fiona_new = tcm.unconfigured().await; + enable_verified_oneonone_chats(&[&fiona_new]).await; + fiona_new.configure_addr("fiona@example.net").await; + e2ee::ensure_secret_key_exists(&fiona_new).await?; + + tcm.send_recv(&fiona_new, &alice, "I have a new device") + .await; + + // The chat should be and stay unprotected + { + let chat = alice.get_chat(&fiona_new).await; + assert!(!chat.is_protected()); + assert!(chat.is_protection_broken()); + + // After recreating the chat, it should still be unprotected + chat.id.delete(&alice).await?; + + let chat = alice.create_chat(&fiona_new).await; + assert!(!chat.is_protected()); + assert!(!chat.is_protection_broken()); + } + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_create_unverified_oneonone_chat() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + enable_verified_oneonone_chats(&[&alice, &bob]).await; + + // A chat with an unknown contact should be created unprotected + let chat = alice.create_chat(&bob).await; + assert!(!chat.is_protected()); + assert!(!chat.is_protection_broken()); + + receive_imf( + &alice, + b"From: Bob \n\ + To: alice@example.org\n\ + Message-ID: <1234-2@example.org>\n\ + \n\ + hello\n", + false, + ) + .await?; + + chat.id.delete(&alice).await.unwrap(); + // Now Bob is a known contact, new chats should still be created unprotected + let chat = alice.create_chat(&bob).await; + assert!(!chat.is_protected()); + assert!(!chat.is_protection_broken()); + + tcm.send_recv(&bob, &alice, "hi").await; + chat.id.delete(&alice).await.unwrap(); + // Now we have a public key, new chats should still be created unprotected + let chat = alice.create_chat(&bob).await; + assert!(!chat.is_protected()); + assert!(!chat.is_protection_broken()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_degrade_verified_oneonone_chat() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + enable_verified_oneonone_chats(&[&alice, &bob]).await; + + mark_as_verified(&alice, &bob).await; + + let alice_chat = alice.create_chat(&bob).await; + assert!(alice_chat.is_protected()); + + receive_imf( + &alice, + b"From: Bob \n\ + To: alice@example.org\n\ + Message-ID: <1234-2@example.org>\n\ + \n\ + hello\n", + false, + ) + .await?; + + let contact_id = Contact::lookup_id_by_addr(&alice, "bob@example.net", Origin::Hidden) + .await? + .unwrap(); + + let msg0 = get_chat_msg(&alice, alice_chat.id, 0, 3).await; + let enabled = stock_str::chat_protection_enabled(&alice).await; + assert_eq!(msg0.text, enabled); + assert_eq!(msg0.param.get_cmd(), SystemMessage::ChatProtectionEnabled); + + let msg1 = get_chat_msg(&alice, alice_chat.id, 1, 3).await; + let disabled = stock_str::chat_protection_disabled(&alice, contact_id).await; + assert_eq!(msg1.text, disabled); + assert_eq!(msg1.param.get_cmd(), SystemMessage::ChatProtectionDisabled); + + let msg2 = get_chat_msg(&alice, alice_chat.id, 2, 3).await; + assert_eq!(msg2.text, "hello".to_string()); + assert!(!msg2.is_system_message()); + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_verified_oneonone_chat_enable_disable() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + enable_verified_oneonone_chats(&[&alice, &bob]).await; + + // Alice & Bob verify each other + mark_as_verified(&alice, &bob).await; + mark_as_verified(&bob, &alice).await; + + let chat = alice.create_chat(&bob).await; + assert!(chat.is_protected()); + + for alice_accepts_breakage in [true, false] { + // Bob uses Thunderbird to send a message + receive_imf( + &alice, + format!( + "From: Bob \n\ + To: alice@example.org\n\ + Message-ID: <1234-2{alice_accepts_breakage}@example.org>\n\ + \n\ + Message from Thunderbird\n" + ) + .as_bytes(), + false, + ) + .await?; + + let chat = alice.get_chat(&bob).await; + assert!(!chat.is_protected()); + assert!(chat.is_protection_broken()); + + if alice_accepts_breakage { + tcm.section("Alice clicks 'Accept' on the input-bar-dialog"); + chat.id.accept(&alice).await?; + let chat = alice.get_chat(&bob).await; + assert!(!chat.is_protected()); + assert!(!chat.is_protection_broken()); + } + + // Bob sends a message from DC again + tcm.send_recv(&bob, &alice, "Hello from DC").await; + let chat = alice.get_chat(&bob).await; + assert!(chat.is_protected()); + assert!(!chat.is_protection_broken()); + } + + alice + .golden_test_chat(chat.id, "test_verified_oneonone_chat_enable_disable") + .await; + + Ok(()) +} + +/// Messages with old timestamps are difficult for verified chats: +/// - They must not be sorted over a protection-changed info message. +/// That's what `test_old_message_2` tests +/// - If they change the protection, then they must not be sorted over existing other messages, +/// because then the protection-changed info message would also be above these existing messages. +/// That's what `test_old_message_3` tests. +/// +/// `test_old_message_1` tests the case where both the old and the new message +/// change verification +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_old_message_1() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + enable_verified_oneonone_chats(&[&alice, &bob]).await; + + mark_as_verified(&alice, &bob).await; + + let chat = alice.create_chat(&bob).await; // This creates a protection-changed info message + assert!(chat.is_protected()); + + // This creates protection-changed info message #2; + // even though the date is old, info message and email must be sorted below the original info message. + receive_imf( + &alice, + b"From: Bob \n\ + To: alice@example.org\n\ + Message-ID: <1234-2-3@example.org>\n\ + Date: Sat, 07 Dec 2019 19:00:27 +0000\n\ + \n\ + Message from Thunderbird\n", + true, + ) + .await?; + + alice.golden_test_chat(chat.id, "test_old_message_1").await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_old_message_2() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + enable_verified_oneonone_chats(&[&alice, &bob]).await; + + mark_as_verified(&alice, &bob).await; + + // This creates protection-changed info message #1: + let chat = alice.create_chat(&bob).await; + assert!(chat.is_protected()); + let protection_msg = alice.get_last_msg().await; + assert_eq!( + protection_msg.param.get_cmd(), + SystemMessage::ChatProtectionEnabled + ); + + // This creates protection-changed info message #2. + let first_email = receive_imf( + &alice, + b"From: Bob \n\ + To: alice@example.org\n\ + Message-ID: <1234-2-3@example.org>\n\ + Date: Sun, 08 Dec 2019 19:00:27 +0000\n\ + \n\ + Somewhat old message\n", + false, + ) + .await? + .unwrap(); + + // Both messages will get the same timestamp as the protection-changed + // message, so this one will be sorted under the previous one + // even though it has an older timestamp. + let second_email = receive_imf( + &alice, + b"From: Bob \n\ + To: alice@example.org\n\ + Message-ID: <2319-2-3@example.org>\n\ + Date: Sat, 07 Dec 2019 19:00:27 +0000\n\ + \n\ + Even older message, that must NOT be shown before the info message\n", + true, + ) + .await? + .unwrap(); + + assert_eq!(first_email.sort_timestamp, second_email.sort_timestamp); + assert_eq!(first_email.sort_timestamp, protection_msg.timestamp_sort); + + alice.golden_test_chat(chat.id, "test_old_message_2").await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_old_message_3() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + enable_verified_oneonone_chats(&[&alice, &bob]).await; + + mark_as_verified(&alice, &bob).await; + mark_as_verified(&bob, &alice).await; + + tcm.send_recv_accept(&bob, &alice, "Heyho from my verified device!") + .await; + + // This unverified message must not be sorted over the message sent in the previous line: + receive_imf( + &alice, + b"From: Bob \n\ + To: alice@example.org\n\ + Message-ID: <1234-2-3@example.org>\n\ + Date: Sat, 07 Dec 2019 19:00:27 +0000\n\ + \n\ + Old, unverified message\n", + true, + ) + .await?; + + alice + .golden_test_chat(alice.get_chat(&bob).await.id, "test_old_message_3") + .await; + + Ok(()) +} + +/// Alice is offline for some time. +/// When she comes online, first her inbox is synced and then her sentbox. +/// This test tests that the messages are still in the right order. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_old_message_4() -> Result<()> { + let alice = TestContext::new_alice().await; + let msg_incoming = receive_imf( + &alice, + b"From: Bob \n\ + To: alice@example.org\n\ + Message-ID: <1234-2-3@example.org>\n\ + Date: Sun, 08 Dec 2019 19:00:27 +0000\n\ + \n\ + Thanks, Alice!\n", + true, + ) + .await? + .unwrap(); + + let msg_sent = receive_imf( + &alice, + b"From: alice@example.org\n\ + To: Bob \n\ + Message-ID: <1234-2-4@example.org>\n\ + Date: Sat, 07 Dec 2019 19:00:27 +0000\n\ + \n\ + Happy birthday, Bob!\n", + true, + ) + .await? + .unwrap(); + + // The "Happy birthday" message should be shown first, and then the "Thanks" message + assert!(msg_sent.sort_timestamp < msg_incoming.sort_timestamp); + + Ok(()) +} + +/// Alice is offline for some time. +/// When they come online, first their sentbox is synced and then their inbox. +/// This test tests that the messages are still in the right order. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_old_message_5() -> Result<()> { + let alice = TestContext::new_alice().await; + let msg_sent = receive_imf( + &alice, + b"From: alice@example.org\n\ + To: Bob \n\ + Message-ID: <1234-2-4@example.org>\n\ + Date: Sat, 07 Dec 2019 19:00:27 +0000\n\ + \n\ + Happy birthday, Bob!\n", + true, + ) + .await? + .unwrap(); + + let msg_incoming = receive_imf( + &alice, + b"From: Bob \n\ + To: alice@example.org\n\ + Message-ID: <1234-2-3@example.org>\n\ + Date: Sun, 07 Dec 2019 19:00:26 +0000\n\ + \n\ + Happy birthday to me, Alice!\n", + false, + ) + .await? + .unwrap(); + + assert!(msg_sent.sort_timestamp == msg_incoming.sort_timestamp); + alice + .golden_test_chat(msg_sent.chat_id, "test_old_message_5") + .await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_mdn_doesnt_disable_verification() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + enable_verified_oneonone_chats(&[&alice, &bob]).await; + bob.set_config_bool(Config::MdnsEnabled, true).await?; + + // Alice & Bob verify each other + mark_as_verified(&alice, &bob).await; + mark_as_verified(&bob, &alice).await; + + let rcvd = tcm.send_recv_accept(&alice, &bob, "Heyho").await; + message::markseen_msgs(&bob, vec![rcvd.id]).await?; + + let mimefactory = MimeFactory::from_mdn(&bob, &rcvd, vec![]).await?; + let rendered_msg = mimefactory.render(&bob).await?; + let body = rendered_msg.message; + receive_imf(&alice, body.as_bytes(), false).await.unwrap(); + + assert_verified(&alice, &bob, ProtectionStatus::Protected).await; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_outgoing_mua_msg() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + enable_verified_oneonone_chats(&[&alice, &bob]).await; + + mark_as_verified(&alice, &bob).await; + mark_as_verified(&bob, &alice).await; + + tcm.send_recv_accept(&bob, &alice, "Heyho from DC").await; + assert_verified(&alice, &bob, ProtectionStatus::Protected).await; + + let sent = receive_imf( + &alice, + b"From: alice@example.org\n\ + To: bob@example.net\n\ + \n\ + One classical MUA message", + false, + ) + .await? + .unwrap(); + tcm.send_recv(&alice, &bob, "Sending with DC again").await; + + alice + .golden_test_chat(sent.chat_id, "test_outgoing_mua_msg") + .await; + + Ok(()) +} + +/// If Bob answers unencrypted from another address with a classical MUA, +/// the message is under some circumstances still assigned to the original +/// chat (see lookup_chat_by_reply()); this is meant to make aliases +/// work nicely. +/// However, if the original chat is verified, the unencrypted message +/// must NOT be assigned to it (it would be replaced by an error +/// message in the verified chat, so, this would just be a usability issue, +/// not a security issue). +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_reply() -> Result<()> { + for verified in [false, true] { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + enable_verified_oneonone_chats(&[&alice, &bob]).await; + + if verified { + mark_as_verified(&alice, &bob).await; + mark_as_verified(&bob, &alice).await; + } + + tcm.send_recv_accept(&bob, &alice, "Heyho from DC").await; + let encrypted_msg = tcm.send_recv(&alice, &bob, "Heyho back").await; + + let unencrypted_msg = receive_imf( + &alice, + format!( + "From: bob@someotherdomain.org\n\ + To: some-alias-forwarding-to-alice@example.org\n\ + In-Reply-To: {}\n\ + \n\ + Weird reply", + encrypted_msg.rfc724_mid + ) + .as_bytes(), + false, + ) + .await? + .unwrap(); + + let unencrypted_msg = Message::load_from_db(&alice, unencrypted_msg.msg_ids[0]).await?; + assert_eq!(unencrypted_msg.text, "Weird reply"); + + if verified { + assert_ne!(unencrypted_msg.chat_id, encrypted_msg.chat_id); + } else { + assert_eq!(unencrypted_msg.chat_id, encrypted_msg.chat_id); + } + } + + Ok(()) +} + +/// Regression test for the following bug: +/// +/// - Scan your chat partner's QR Code +/// - They change devices +/// - They send you a message +/// - Without accepting the encryption downgrade, scan your chat partner's QR Code again +/// +/// -> The re-verification fails. +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_break_protection_then_verify_again() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + enable_verified_oneonone_chats(&[&alice, &bob]).await; + + // Cave: Bob can't write a message to Alice here. + // If he did, alice would increase his peerstate's last_seen timestamp. + // Then, after Bob reinstalls DC, alice's `if message_time > last_seen*` + // checks would return false (there are many checks of this form in peerstate.rs). + // Therefore, during the securejoin, Alice wouldn't accept the new key + // and reject the securejoin. + + mark_as_verified(&alice, &bob).await; + mark_as_verified(&bob, &alice).await; + + alice.create_chat(&bob).await; + assert_verified(&alice, &bob, ProtectionStatus::Protected).await; + let chats = Chatlist::try_load(&alice, DC_GCL_FOR_FORWARDING, None, None).await?; + assert!(chats.len() == 1); + + tcm.section("Bob reinstalls DC"); + drop(bob); + let bob_new = tcm.unconfigured().await; + enable_verified_oneonone_chats(&[&bob_new]).await; + bob_new.configure_addr("bob@example.net").await; + e2ee::ensure_secret_key_exists(&bob_new).await?; + + tcm.send_recv(&bob_new, &alice, "I have a new device").await; + + let contact = alice.add_or_lookup_contact(&bob_new).await; + assert_eq!( + contact.is_verified(&alice).await.unwrap(), + // Bob sent a message with a new key, so he most likely doesn't have + // the old key anymore. This means that Alice's device should show + // him as unverified: + VerifiedStatus::Unverified + ); + let chat = alice.get_chat(&bob_new).await; + assert_eq!(chat.is_protected(), false); + assert_eq!(chat.is_protection_broken(), true); + let chats = Chatlist::try_load(&alice, DC_GCL_FOR_FORWARDING, None, None).await?; + assert!(chats.len() == 1); + + { + let alice_bob_chat = alice.get_chat(&bob_new).await; + assert!(!alice_bob_chat.can_send(&alice).await?); + + // Alice's UI should still be able to save a draft, which Alice started to type right when she got Bob's message: + let mut msg = Message::new(Viewtype::Text); + msg.set_text("Draftttt".to_string()); + alice_bob_chat.id.set_draft(&alice, Some(&mut msg)).await?; + assert_eq!( + alice_bob_chat.id.get_draft(&alice).await?.unwrap().text, + "Draftttt" + ); + } + + tcm.execute_securejoin(&alice, &bob_new).await; + assert_verified(&alice, &bob_new, ProtectionStatus::Protected).await; + + Ok(()) +} + +/// Regression test: +/// - Verify a contact +/// - The contact stops using DC and sends a message from a classical MUA instead +/// - Delete the 1:1 chat +/// - Create a 1:1 chat +/// - Check that the created chat is not marked as protected +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_create_oneonone_chat_with_former_verified_contact() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + enable_verified_oneonone_chats(&[&alice]).await; + + mark_as_verified(&alice, &bob).await; + + receive_imf( + &alice, + b"Subject: Message from bob\r\n\ + From: \r\n\ + To: \r\n\ + Date: Mon, 12 Dec 2022 14:33:39 +0000\r\n\ + Message-ID: \r\n\ + \r\n\ + Heyho!\r\n", + false, + ) + .await + .unwrap() + .unwrap(); + + alice.create_chat(&bob).await; + + assert_verified(&alice, &bob, ProtectionStatus::Unprotected).await; + + Ok(()) +} + +// ============== Helper Functions ============== + +async fn assert_verified(this: &TestContext, other: &TestContext, protected: ProtectionStatus) { + let contact = this.add_or_lookup_contact(other).await; + assert_eq!( + contact.is_verified(this).await.unwrap(), + VerifiedStatus::BidirectVerified + ); + + let chat = this.get_chat(other).await; + let (expect_protected, expect_broken) = match protected { + ProtectionStatus::Unprotected => (false, false), + ProtectionStatus::Protected => (true, false), + ProtectionStatus::ProtectionBroken => (false, true), + }; + assert_eq!(chat.is_protected(), expect_protected); + assert_eq!(chat.is_protection_broken(), expect_broken); +} + +async fn enable_verified_oneonone_chats(test_contexts: &[&TestContext]) { + for t in test_contexts { + t.set_config_bool(Config::VerifiedOneOnOneChats, true) + .await + .unwrap() + } +} diff --git a/src/webxdc.rs b/src/webxdc.rs index 3341446e4..107ae1680 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -26,11 +26,12 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; use tokio::io::AsyncReadExt; -use crate::chat::Chat; +use crate::chat::{self, Chat}; use crate::constants::Chattype; use crate::contact::ContactId; use crate::context::Context; use crate::download::DownloadState; +use crate::events::EventType; use crate::message::{Message, MessageState, MsgId, Viewtype}; use crate::mimefactory::wrapped_base64_encode; use crate::mimeparser::SystemMessage; @@ -39,7 +40,6 @@ use crate::param::Params; use crate::scheduler::InterruptInfo; use crate::tools::strip_rtlo_characters; use crate::tools::{create_smeared_timestamp, get_abs_path}; -use crate::{chat, EventType}; /// The current API version. /// If `min_api` in manifest.toml is set to a larger value, diff --git a/test-data/golden/test_old_message_1 b/test-data/golden/test_old_message_1 new file mode 100644 index 000000000..61e54d298 --- /dev/null +++ b/test-data/golden/test_old_message_1 @@ -0,0 +1,6 @@ +Single#Chat#10: Bob [bob@example.net] +-------------------------------------------------------------------------------- +Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO πŸ›‘οΈ] +Msg#11: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO πŸ›‘οΈβŒ] +Msg#12: (Contact#Contact#10): Message from Thunderbird [SEEN] +-------------------------------------------------------------------------------- diff --git a/test-data/golden/test_old_message_2 b/test-data/golden/test_old_message_2 new file mode 100644 index 000000000..d7e7712ab --- /dev/null +++ b/test-data/golden/test_old_message_2 @@ -0,0 +1,7 @@ +Single#Chat#10: Bob [bob@example.net] +-------------------------------------------------------------------------------- +Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO πŸ›‘οΈ] +Msg#11: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO πŸ›‘οΈβŒ] +Msg#12: (Contact#Contact#10): Somewhat old message [FRESH] +Msg#13: (Contact#Contact#10): Even older message, that must NOT be shown before the info message [SEEN] +-------------------------------------------------------------------------------- diff --git a/test-data/golden/test_old_message_3 b/test-data/golden/test_old_message_3 new file mode 100644 index 000000000..30d5ed041 --- /dev/null +++ b/test-data/golden/test_old_message_3 @@ -0,0 +1,7 @@ +Single#Chat#10: Bob [bob@example.net] +-------------------------------------------------------------------------------- +Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO πŸ›‘οΈ] +Msg#11πŸ”’: (Contact#Contact#10): Heyho from my verified device! [FRESH] +Msg#12: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO πŸ›‘οΈβŒ] +Msg#13: (Contact#Contact#10): Old, unverified message [SEEN] +-------------------------------------------------------------------------------- diff --git a/test-data/golden/test_old_message_5 b/test-data/golden/test_old_message_5 new file mode 100644 index 000000000..75d3c0af0 --- /dev/null +++ b/test-data/golden/test_old_message_5 @@ -0,0 +1,5 @@ +Single#Chat#10: Bob [bob@example.net] +-------------------------------------------------------------------------------- +Msg#10: Me (Contact#Contact#Self): Happy birthday, Bob! √ +Msg#11: (Contact#Contact#10): Happy birthday to me, Alice! [FRESH] +-------------------------------------------------------------------------------- diff --git a/test-data/golden/test_outgoing_mua_msg b/test-data/golden/test_outgoing_mua_msg new file mode 100644 index 000000000..549f84175 --- /dev/null +++ b/test-data/golden/test_outgoing_mua_msg @@ -0,0 +1,7 @@ +Single#Chat#10: bob@example.net [bob@example.net] πŸ›‘οΈ +-------------------------------------------------------------------------------- +Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO πŸ›‘οΈ] +Msg#11πŸ”’: (Contact#Contact#10): Heyho from DC [FRESH] +Msg#12: Me (Contact#Contact#Self): One classical MUA message √ +Msg#13πŸ”’: Me (Contact#Contact#Self): Sending with DC again √ +-------------------------------------------------------------------------------- diff --git a/test-data/golden/test_verified_oneonone_chat_enable_disable b/test-data/golden/test_verified_oneonone_chat_enable_disable new file mode 100644 index 000000000..4da4a67d0 --- /dev/null +++ b/test-data/golden/test_verified_oneonone_chat_enable_disable @@ -0,0 +1,12 @@ +Single#Chat#10: Bob [bob@example.net] πŸ›‘οΈ +-------------------------------------------------------------------------------- +Msg#10: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO πŸ›‘οΈ] +Msg#11: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO πŸ›‘οΈβŒ] +Msg#12: (Contact#Contact#10): Message from Thunderbird [FRESH] +Msg#13: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO πŸ›‘οΈ] +Msg#14πŸ”’: (Contact#Contact#10): Hello from DC [FRESH] +Msg#15: info (Contact#Contact#Info): Bob sent a message from another device. [NOTICED][INFO πŸ›‘οΈβŒ] +Msg#16: (Contact#Contact#10): Message from Thunderbird [FRESH] +Msg#17: info (Contact#Contact#Info): Messages are guaranteed to be end-to-end encrypted from now on. [NOTICED][INFO πŸ›‘οΈ] +Msg#18πŸ”’: (Contact#Contact#10): Hello from DC [FRESH] +--------------------------------------------------------------------------------