diff --git a/Cargo.lock b/Cargo.lock index 73d43bda..8ff192c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -495,6 +495,12 @@ dependencies = [ "which", ] +[[package]] +name = "bitfield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" + [[package]] name = "bitflags" version = "1.3.2" @@ -519,6 +525,28 @@ dependencies = [ "crunchy", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "bitvec-nom2" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4863ce31b7ff8812568eaffe956024c824d845a1f9f08c329706166c357cae53" +dependencies = [ + "bitvec", + "nom", +] + [[package]] name = "blake2" version = "0.10.6" @@ -560,6 +588,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "blowfish" version = "0.7.0" @@ -571,6 +608,36 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "blowfish" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +dependencies = [ + "byteorder", + "cipher 0.4.4", +] + +[[package]] +name = "bstr" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "buffer-redux" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2886ea01509598caac116942abd33ab5a88fa32acdf7e4abfa0fc489ca520c9" +dependencies = [ + "memchr", + "safemem", +] + [[package]] name = "bumpalo" version = "3.13.0" @@ -616,6 +683,34 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "camellia" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3264e2574e9ef2b53ce6f536dea83a69ac0bc600b762d1523ff83fe07230ce30" +dependencies = [ + "byteorder", + "cipher 0.4.4", +] + +[[package]] +name = "cast5" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b07d673db1ccf000e90f54b819db9e75a8348d6eb056e9b8ab53231b7a9911" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "cc" version = "1.0.79" @@ -643,6 +738,15 @@ dependencies = [ "nom", ] +[[package]] +name = "cfb-mode" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "738b8d467867f80a71351933f70461f5b56f24d5c93e0cf216e59229c968d330" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "cfg-if" version = "1.0.0" @@ -813,6 +917,12 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9cace84e55f07e7301bae1c519df89cdad8cc3cd868413d3fdbdeca9ff3db484" +[[package]] +name = "crc24" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd121741cf3eb82c08dd3023eb55bf2665e5f60ec20f89760cf836ae4562e6a0" + [[package]] name = "crc32fast" version = "1.3.2" @@ -944,6 +1054,69 @@ dependencies = [ "cipher 0.4.4", ] +[[package]] +name = "curve25519-dalek" +version = "4.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436ace70fc06e06f7f689d2624dc4e2f0ea666efb5aa704215f7249ae6e047a7" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "platforms", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + [[package]] name = "dashmap" version = "5.5.0" @@ -988,6 +1161,46 @@ dependencies = [ "rusticata-macros", ] +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + +[[package]] +name = "des" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdd80ce8ce993de27e9f063a444a4d53ce8e8db4c1f00cc03af5ad5a9867a1e" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "dialoguer" version = "0.10.4" @@ -1111,6 +1324,12 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "dotenvy" version = "0.15.7" @@ -1149,6 +1368,29 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ed25519" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faa8e9049d5d72bfc12acbc05914731b5322f79b5e2f195e9f2d705fca22ab4c" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2 0.10.7", + "zeroize", +] + [[package]] name = "either" version = "1.9.0" @@ -1296,6 +1538,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "fiat-crypto" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" + [[package]] name = "filetime" version = "0.2.21" @@ -1434,6 +1682,12 @@ dependencies = [ "bindgen 0.65.1", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.28" @@ -1899,6 +2153,21 @@ dependencies = [ "cc", ] +[[package]] +name = "idea" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075557004419d7f2031b8bb7f44bb43e55a83ca7b63076a8fb8fe75753836477" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.2.3" @@ -1993,6 +2262,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ + "block-padding", "generic-array", ] @@ -2088,11 +2358,14 @@ dependencies = [ name = "jmap" version = "0.3.2" dependencies = [ + "aes", "aes-gcm", "aes-gcm-siv", "async-stream", + "async-trait", "base64 0.21.2", "bincode", + "cbc", "chrono", "dashmap", "directory", @@ -2110,7 +2383,13 @@ dependencies = [ "mail-send", "mime", "p256", + "pgp", + "rand", + "rasn", + "rasn-cms", + "rasn-pkix", "reqwest", + "rsa", "serde", "serde_json", "sha2 0.10.7", @@ -2120,9 +2399,9 @@ dependencies = [ "sqlx", "store", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.20.0", "tracing", - "tungstenite", + "tungstenite 0.20.0", "utils", ] @@ -2143,7 +2422,7 @@ dependencies = [ "serde", "serde_json", "tokio", - "tokio-tungstenite", + "tokio-tungstenite 0.19.0", ] [[package]] @@ -2179,6 +2458,30 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6d5ed8676d904364de097082f4e7d240b571b67989ced0240f08b7f966f940" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "konst" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330f0e13e6483b8c34885f7e6c9f19b1a7bd449c673fbb948a51c99d66ef74f4" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "lazy_static" version = "1.4.0" @@ -2631,10 +2934,22 @@ dependencies = [ "num-iter", "num-traits", "rand", + "serde", "smallvec", "zeroize", ] +[[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.27", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -2898,6 +3213,18 @@ dependencies = [ "sha2 0.10.7", ] +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.7", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -3004,6 +3331,58 @@ dependencies = [ "indexmap 1.9.3", ] +[[package]] +name = "pgp" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27e1f8e085bfa9b85763fe3ddaacbe90a09cd847b3833129153a6cb063bbe132" +dependencies = [ + "aes", + "base64 0.21.2", + "bitfield", + "block-padding", + "blowfish 0.9.1", + "bstr", + "buffer-redux", + "byteorder", + "camellia", + "cast5", + "cfb-mode", + "chrono", + "cipher 0.4.4", + "crc24", + "curve25519-dalek", + "derive_builder", + "des", + "digest 0.10.7", + "ed25519-dalek", + "elliptic-curve", + "flate2", + "generic-array", + "hex", + "idea", + "log", + "md-5 0.10.5", + "nom", + "num-bigint-dig", + "num-derive", + "num-traits", + "p256", + "p384", + "rand", + "ripemd", + "rsa", + "sha1", + "sha2 0.10.7", + "sha3", + "signature", + "smallvec", + "thiserror", + "twofish", + "x25519-dalek", + "zeroize", +] + [[package]] name = "phf" version = "0.11.2" @@ -3115,6 +3494,12 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "platforms" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630" + [[package]] name = "polyval" version = "0.6.1" @@ -3261,7 +3646,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "419a3ad8fa9f9d445e69d9b185a24878ae6e6f55c96e4512f4a0e28cd3bc5c56" dependencies = [ - "blowfish", + "blowfish 0.7.0", "byteorder", "hmac 0.10.1", "md-5 0.9.1", @@ -3315,6 +3700,12 @@ dependencies = [ "scheduled-thread-pool", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -3345,6 +3736,62 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rasn" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b8d6d2a0e8ed2c702de8b7bcc6c867faa8fe662d5fa87d6dbcae945fe276004" +dependencies = [ + "arrayvec", + "bitvec", + "bitvec-nom2", + "bytes", + "chrono", + "either", + "konst", + "nom", + "num-bigint", + "num-integer", + "num-traits", + "once_cell", + "rasn-derive", + "snafu", +] + +[[package]] +name = "rasn-cms" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a7d66ec2276b9a8cee3bf3dc2d051895f009f1bffabd114871d44d3b215702b" +dependencies = [ + "rasn", + "rasn-pkix", +] + +[[package]] +name = "rasn-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52573d02ccf5e01c7d42b6009365728046a58202ec4001724b034815c9c35d3f" +dependencies = [ + "either", + "itertools", + "proc-macro2", + "quote", + "rayon", + "syn 1.0.109", + "uuid", +] + +[[package]] +name = "rasn-pkix" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ff08da0c6502e4d4e961450e3d291f47c2e2896b788436f294e5cacef57dc1" +dependencies = [ + "rasn", +] + [[package]] name = "rayon" version = "1.7.0" @@ -3522,6 +3969,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "roaring" version = "0.10.2" @@ -3664,6 +4120,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rusticata-macros" version = "4.1.0" @@ -3763,6 +4228,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "safemem" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" + [[package]] name = "salsa20" version = "0.10.2" @@ -3855,6 +4326,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + [[package]] name = "serde" version = "1.0.176" @@ -3980,6 +4457,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + [[package]] name = "sharded-slab" version = "0.1.4" @@ -4101,6 +4588,29 @@ name = "smtp-proto" version = "0.1.1" source = "git+https://github.com/stalwartlabs/smtp-proto#434ede72159b5f9ea588b9ee8c361a3247aa2f35" +[[package]] +name = "snafu" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +dependencies = [ + "backtrace", + "doc-comment", + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "socket2" version = "0.4.9" @@ -4499,6 +5009,12 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.39" @@ -4759,10 +5275,22 @@ dependencies = [ "rustls 0.21.5", "tokio", "tokio-rustls 0.24.1", - "tungstenite", + "tungstenite 0.19.0", "webpki-roots 0.23.1", ] +[[package]] +name = "tokio-tungstenite" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2dbec703c26b00d74844519606ef15d09a7d6857860f84ad223dec002ddea2" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.20.0", +] + [[package]] name = "tokio-util" version = "0.7.8" @@ -5038,6 +5566,34 @@ dependencies = [ "webpki", ] +[[package]] +name = "tungstenite" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e862a1c4128df0112ab625f55cd5c934bcb4312ba80b39ae4b4835a3fd58e649" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "twofish" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78e83a30223c757c3947cd144a31014ff04298d8719ae10d03c31c0448c8013" +dependencies = [ + "cipher 0.4.4", +] + [[package]] name = "typenum" version = "1.16.0" @@ -5157,6 +5713,9 @@ name = "uuid" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" +dependencies = [ + "getrandom", +] [[package]] name = "valuable" @@ -5538,6 +6097,27 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7fae07da688e17059d5886712c933bb0520f15eff2e09cfa18e30968f4e63a" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + [[package]] name = "x509-parser" version = "0.15.0" @@ -5581,6 +6161,20 @@ name = "zeroize" version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.27", +] [[package]] name = "zip" diff --git a/crates/imap/src/op/append.rs b/crates/imap/src/op/append.rs index 7685ef1b..556b78ff 100644 --- a/crates/imap/src/op/append.rs +++ b/crates/imap/src/op/append.rs @@ -142,6 +142,7 @@ impl SessionData { keywords: message.flags.into_iter().map(Keyword::from).collect(), received_at: message.received_at.map(|d| d as u64), skip_duplicates: false, + encrypt: true, }) .await { diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index f0f17049..c8576025 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -36,10 +36,19 @@ p256 = { version = "0.13", features = ["ecdh"] } hkdf = "0.12.3" sha2 = "0.10.1" reqwest = { version = "0.11", default-features = false, features = ["rustls-tls-webpki-roots"]} -tokio-tungstenite = "0.19.0" -tungstenite = "0.19.0" +tokio-tungstenite = "0.20.0" +tungstenite = "0.20.0" chrono = "0.4" dashmap = "5.4" +aes = "0.8.3" +cbc = { version = "0.1.2", features = ["alloc"] } +pgp = "0.10.2" +rand = "0.8.5" +rasn = "0.8.2" +rasn-cms = "0.8.2" +rasn-pkix = "0.8.2" +rsa = "0.9.2" +async-trait = "0.1.68" [dev-dependencies] ece = "2.2" diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index 947983ff..69bd2cec 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -237,6 +237,18 @@ pub async fn parse_jmap_request( _ => (), } } + "crypto" => match *req.method() { + Method::GET => { + return jmap.handle_crypto_update(&mut req).await; + } + Method::POST => { + return match jmap.is_auth_allowed(jmap.build_remote_addr(&req, remote_ip)) { + Ok(_) => jmap.handle_crypto_update(&mut req).await, + Err(err) => err.into_http_response(), + } + } + _ => (), + }, "admin" => { // Make sure the user is a superuser diff --git a/crates/jmap/src/auth/oauth/device_auth.rs b/crates/jmap/src/auth/oauth/device_auth.rs index a641fec9..c96b8aeb 100644 --- a/crates/jmap/src/auth/oauth/device_auth.rs +++ b/crates/jmap/src/auth/oauth/device_auth.rs @@ -36,15 +36,15 @@ use utils::{listener::ServerInstance, map::ttl_dashmap::TtlMap}; use crate::{ api::{http::ToHttpResponse, HtmlResponse, HttpRequest, HttpResponse, JsonResponse}, auth::oauth::{ - OAUTH_HTML_ERROR, OAUTH_HTML_LOGIN_HEADER_FAILED, OAUTH_HTML_LOGIN_SUCCESS, + MAX_POST_LEN, OAUTH_HTML_ERROR, OAUTH_HTML_LOGIN_HEADER_FAILED, OAUTH_HTML_LOGIN_SUCCESS, STATUS_AUTHORIZED, }, JMAP, }; use super::{ - parse_form_data, DeviceAuthResponse, OAuthCode, CLIENT_ID_MAX_LEN, DEVICE_CODE_LEN, - OAUTH_HTML_FOOTER, OAUTH_HTML_HEADER, OAUTH_HTML_LOGIN_CODE, OAUTH_HTML_LOGIN_FORM, + DeviceAuthResponse, FormData, OAuthCode, CLIENT_ID_MAX_LEN, DEVICE_CODE_LEN, OAUTH_HTML_FOOTER, + OAUTH_HTML_HEADER, OAUTH_HTML_LOGIN_CODE, OAUTH_HTML_LOGIN_FORM, OAUTH_HTML_LOGIN_HEADER_DEVICE, STATUS_PENDING, USER_CODE_ALPHABET, USER_CODE_LEN, }; @@ -56,7 +56,7 @@ impl JMAP { instance: Arc, ) -> HttpResponse { // Parse form - let client_id = match parse_form_data(req) + let client_id = match FormData::from_request(req, MAX_POST_LEN) .await .map(|mut p| p.remove("client_id")) { @@ -150,7 +150,7 @@ impl JMAP { // Handles POST request from the device authorization form pub async fn handle_user_device_auth_post(&self, req: &mut HttpRequest) -> HttpResponse { // Parse form - let fields = match parse_form_data(req).await { + let fields = match FormData::from_request(req, MAX_POST_LEN).await { Ok(fields) => fields, Err(err) => return err, }; @@ -209,10 +209,9 @@ impl JMAP { } Response::Failed => { response.push_str(OAUTH_HTML_LOGIN_HEADER_FAILED); - response.push_str(&OAUTH_HTML_LOGIN_CODE.replace( - "@@@", - fields.get("code").map(|s| s.as_str()).unwrap_or_default(), - )); + response.push_str( + &OAUTH_HTML_LOGIN_CODE.replace("@@@", fields.get("code").unwrap_or_default()), + ); response.push_str(&OAUTH_HTML_LOGIN_FORM.replace("@@@", "about:blank")); } Response::InvalidCode => { diff --git a/crates/jmap/src/auth/oauth/mod.rs b/crates/jmap/src/auth/oauth/mod.rs index 658969fc..be6773d8 100644 --- a/crates/jmap/src/auth/oauth/mod.rs +++ b/crates/jmap/src/auth/oauth/mod.rs @@ -33,21 +33,21 @@ pub mod device_auth; pub mod token; pub mod user_code; -const OAUTH_HTML_HEADER: &str = include_str!("../../../../../resources/oauth/header.htx"); -const OAUTH_HTML_FOOTER: &str = include_str!("../../../../../resources/oauth/footer.htx"); +const OAUTH_HTML_HEADER: &str = include_str!("../../../../../resources/htx/header.htx"); +const OAUTH_HTML_FOOTER: &str = include_str!("../../../../../resources/htx/footer.htx"); const OAUTH_HTML_LOGIN_HEADER_CLIENT: &str = - include_str!("../../../../../resources/oauth/login_hdr_client.htx"); + include_str!("../../../../../resources/htx/login_hdr_client.htx"); const OAUTH_HTML_LOGIN_HEADER_DEVICE: &str = - include_str!("../../../../../resources/oauth/login_hdr_device.htx"); + include_str!("../../../../../resources/htx/login_hdr_device.htx"); const OAUTH_HTML_LOGIN_HEADER_FAILED: &str = - include_str!("../../../../../resources/oauth/login_hdr_failed.htx"); -const OAUTH_HTML_LOGIN_FORM: &str = include_str!("../../../../../resources/oauth/login.htx"); -const OAUTH_HTML_LOGIN_CODE: &str = include_str!("../../../../../resources/oauth/login_code.htx"); + include_str!("../../../../../resources/htx/login_hdr_failed.htx"); +const OAUTH_HTML_LOGIN_FORM: &str = include_str!("../../../../../resources/htx/login.htx"); +const OAUTH_HTML_LOGIN_CODE: &str = include_str!("../../../../../resources/htx/login_code.htx"); const OAUTH_HTML_LOGIN_CODE_HIDDEN: &str = - include_str!("../../../../../resources/oauth/login_code_hidden.htx"); + include_str!("../../../../../resources/htx/login_code_hidden.htx"); const OAUTH_HTML_LOGIN_SUCCESS: &str = - include_str!("../../../../../resources/oauth/login_success.htx"); -const OAUTH_HTML_ERROR: &str = include_str!("../../../../../resources/oauth/error.htx"); + include_str!("../../../../../resources/htx/login_success.htx"); +const OAUTH_HTML_ERROR: &str = include_str!("../../../../../resources/htx/error.htx"); const STATUS_AUTHORIZED: u32 = 0; const STATUS_TOKEN_ISSUED: u32 = 1; @@ -58,6 +58,8 @@ const USER_CODE_LEN: usize = 8; const RANDOM_CODE_LEN: usize = 32; const CLIENT_ID_MAX_LEN: usize = 20; +const MAX_POST_LEN: usize = 2048; + const USER_CODE_ALPHABET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // No 0, O, I, 1 pub struct OAuth { @@ -211,44 +213,69 @@ impl TokenResponse { } } -pub async fn parse_form_data( - req: &mut HttpRequest, -) -> Result, HttpResponse> { - match ( - req.headers() - .get(CONTENT_TYPE) - .and_then(|h| h.to_str().ok()) - .and_then(|val| val.parse::().ok()), - fetch_body(req).await, - ) { - (Some(content_type), Some(body)) => { - let mut fields = HashMap::new(); - if let Some(boundary) = content_type.get_param(mime::BOUNDARY) { - for mut field in form_data::FormData::new(&body[..], boundary.as_str()).flatten() { - let value = String::from_utf8(field.bytes().unwrap_or_default().to_vec()) - .unwrap_or_default(); - fields.insert(field.name, value); - } - } else { - for (key, value) in form_urlencoded::parse(&body) { - fields.insert(key.into_owned(), value.into_owned()); +pub struct FormData { + fields: HashMap>, +} + +impl FormData { + pub async fn from_request(req: &mut HttpRequest, max_len: usize) -> Result { + match ( + req.headers() + .get(CONTENT_TYPE) + .and_then(|h| h.to_str().ok()) + .and_then(|val| val.parse::().ok()), + fetch_body(req, max_len).await, + ) { + (Some(content_type), Some(body)) => { + let mut fields = HashMap::new(); + if let Some(boundary) = content_type.get_param(mime::BOUNDARY) { + for mut field in + form_data::FormData::new(&body[..], boundary.as_str()).flatten() + { + let value = field.bytes().unwrap_or_default().to_vec(); + fields.insert(field.name, value); + } + } else { + for (key, value) in form_urlencoded::parse(&body) { + fields.insert(key.into_owned(), value.into_owned().into_bytes()); + } } + Ok(FormData { fields }) } - Ok(fields) + _ => Err(HtmlResponse::with_status( + StatusCode::BAD_REQUEST, + "Invalid post request".to_string(), + ) + .into_http_response()), } - _ => Err(HtmlResponse::with_status( - StatusCode::BAD_REQUEST, - "Invalid post request".to_string(), - ) - .into_http_response()), + } + + pub fn get(&self, key: &str) -> Option<&str> { + self.fields + .get(key) + .and_then(|v| std::str::from_utf8(v).ok()) + } + + pub fn remove(&mut self, key: &str) -> Option { + self.fields + .remove(key) + .and_then(|v| String::from_utf8(v).ok()) + } + + pub fn get_bytes(&self, key: &str) -> Option<&[u8]> { + self.fields.get(key).map(|v| v.as_slice()) + } + + pub fn remove_bytes(&mut self, key: &str) -> Option> { + self.fields.remove(key) } } -pub async fn fetch_body(req: &mut HttpRequest) -> Option> { +pub async fn fetch_body(req: &mut HttpRequest, max_len: usize) -> Option> { let mut bytes = Vec::with_capacity(1024); while let Some(Ok(frame)) = req.frame().await { if let Some(data) = frame.data_ref() { - if bytes.len() + data.len() <= 2048 { + if bytes.len() + data.len() <= max_len { bytes.extend_from_slice(data); } else { return None; diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs index 73023c31..43cf9231 100644 --- a/crates/jmap/src/auth/oauth/token.rs +++ b/crates/jmap/src/auth/oauth/token.rs @@ -42,7 +42,7 @@ use crate::{ }; use super::{ - parse_form_data, ErrorType, TokenResponse, CLIENT_ID_MAX_LEN, RANDOM_CODE_LEN, + ErrorType, FormData, TokenResponse, CLIENT_ID_MAX_LEN, MAX_POST_LEN, RANDOM_CODE_LEN, STATUS_AUTHORIZED, STATUS_PENDING, STATUS_TOKEN_ISSUED, }; @@ -50,14 +50,11 @@ impl JMAP { // Token endpoint pub async fn handle_token_request(&self, req: &mut HttpRequest) -> HttpResponse { // Parse form - let params = match parse_form_data(req).await { + let params = match FormData::from_request(req, MAX_POST_LEN).await { Ok(params) => params, Err(err) => return err, }; - let grant_type = params - .get("grant_type") - .map(|s| s.as_str()) - .unwrap_or_default(); + let grant_type = params.get("grant_type").unwrap_or_default(); let mut response = TokenResponse::error(ErrorType::InvalidGrant); @@ -68,7 +65,7 @@ impl JMAP { params.get("redirect_uri"), ) { if let Some(oauth) = self.oauth_codes.get_with_ttl(code) { - if client_id != &oauth.client_id + if client_id != oauth.client_id || redirect_uri != oauth.redirect_uri.as_deref().unwrap_or("") { TokenResponse::error(ErrorType::InvalidClient) @@ -107,7 +104,7 @@ impl JMAP { .and_then(|dc| self.oauth_codes.get_with_ttl(dc)), params.get("client_id"), ) { - response = if &oauth.client_id != client_id { + response = if oauth.client_id != client_id { TokenResponse::error(ErrorType::InvalidClient) } else { match oauth.status.load(atomic::Ordering::Relaxed) { diff --git a/crates/jmap/src/auth/oauth/user_code.rs b/crates/jmap/src/auth/oauth/user_code.rs index 76fab55e..2404b206 100644 --- a/crates/jmap/src/auth/oauth/user_code.rs +++ b/crates/jmap/src/auth/oauth/user_code.rs @@ -41,7 +41,7 @@ use crate::{ }; use super::{ - parse_form_data, OAuthCode, CLIENT_ID_MAX_LEN, DEVICE_CODE_LEN, OAUTH_HTML_FOOTER, + FormData, OAuthCode, CLIENT_ID_MAX_LEN, DEVICE_CODE_LEN, MAX_POST_LEN, OAUTH_HTML_FOOTER, OAUTH_HTML_HEADER, OAUTH_HTML_LOGIN_CODE_HIDDEN, OAUTH_HTML_LOGIN_FORM, OAUTH_HTML_LOGIN_HEADER_CLIENT, OAUTH_HTML_LOGIN_HEADER_FAILED, STATUS_AUTHORIZED, }; @@ -109,15 +109,15 @@ impl JMAP { // Handles POST request from the code authorization form pub async fn handle_user_code_auth_post(&self, req: &mut HttpRequest) -> HttpResponse { // Parse form - let params = match parse_form_data(req).await { + let params = match FormData::from_request(req, MAX_POST_LEN).await { Ok(params) => params, Err(err) => return err, }; let mut auth_code = None; let (auth_attempts, code_req) = match params - .get("code") - .and_then(|code| base64_decode(code.as_bytes())) + .get_bytes("code") + .and_then(base64_decode) .and_then(|bytes| bincode::deserialize::<(u32, HashMap)>(&bytes).ok()) { Some(code) => code, diff --git a/crates/jmap/src/email/crypto.rs b/crates/jmap/src/email/crypto.rs new file mode 100644 index 00000000..168b6aa3 --- /dev/null +++ b/crates/jmap/src/email/crypto.rs @@ -0,0 +1,670 @@ +/* + * Copyright (c) 2023 Stalwart Labs Ltd. + * + * This file is part of Stalwart Mail Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use std::{borrow::Cow, collections::BTreeSet}; + +use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit}; +use jmap_proto::types::{collection::Collection, property::Property}; +use mail_builder::{encoders::base64::base64_encode_mime, mime::make_boundary}; +use mail_parser::{decoders::base64::base64_decode, Message, MimeHeaders}; +use pgp::{composed, crypto::sym::SymmetricKeyAlgorithm, Deserializable, SignedPublicKey}; +use rand::{rngs::StdRng, RngCore, SeedableRng}; +use rasn::types::{ObjectIdentifier, OctetString}; +use rasn_cms::{ + algorithms::{AES128_CBC, AES256_CBC, RSA}, + pkcs7_compat::EncapsulatedContentInfo, + AlgorithmIdentifier, EncryptedContent, EncryptedContentInfo, EncryptedKey, EnvelopedData, + IssuerAndSerialNumber, KeyTransRecipientInfo, RecipientIdentifier, RecipientInfo, CONTENT_DATA, + CONTENT_ENVELOPED_DATA, +}; +use rsa::{pkcs1::DecodeRsaPublicKey, Pkcs1v15Encrypt, RsaPublicKey}; +use store::{ + write::{BatchBuilder, ToBitmaps, F_CLEAR, F_VALUE}, + Deserialize, Serialize, +}; + +use crate::{ + api::{http::ToHttpResponse, HtmlResponse, HttpRequest, HttpResponse}, + auth::oauth::FormData, + JMAP, +}; + +const CRYPT_HTML_HEADER: &str = include_str!("../../../../resources/htx/crypto_header.htx"); +const CRYPT_HTML_FOOTER: &str = include_str!("../../../../resources/htx/crypto_footer.htx"); +const CRYPT_HTML_FORM: &str = include_str!("../../../../resources/htx/crypto_form.htx"); +const CRYPT_HTML_SUCCESS: &str = include_str!("../../../../resources/htx/crypto_success.htx"); +const CRYPT_HTML_ERROR: &str = include_str!("../../../../resources/htx/crypto_error.htx"); + +#[derive(Debug)] +pub enum EncryptMessageError { + AlreadyEncrypted, + Error(String), +} + +#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)] +pub enum Algorithm { + Aes128, + Aes256, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum EncryptionMethod { + PGP, + SMIME, +} + +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct EncryptionParams { + method: EncryptionMethod, + algo: Algorithm, + certs: Vec>, +} + +#[async_trait::async_trait] +pub trait EncryptMessage { + async fn encrypt(&self, params: &EncryptionParams) -> Result, EncryptMessageError>; + fn is_encrypted(&self) -> bool; +} + +#[async_trait::async_trait] +impl EncryptMessage for Message<'_> { + async fn encrypt(&self, params: &EncryptionParams) -> Result, EncryptMessageError> { + let root = self.root_part(); + let raw_message = self.raw_message(); + let mut outer_message = Vec::with_capacity((raw_message.len() as f64 * 1.5) as usize); + let mut inner_message = Vec::with_capacity(raw_message.len()); + + // Move MIME headers and body to inner message + for header in root.headers() { + (if header.name.is_mime_header() { + &mut inner_message + } else { + &mut outer_message + }) + .extend_from_slice(&raw_message[header.offset_field()..header.offset_end()]); + } + inner_message.extend_from_slice(b"\r\n"); + inner_message.extend_from_slice(&raw_message[root.raw_body_offset()..]); + + // Encrypt inner message + match params.method { + EncryptionMethod::PGP => { + // Prepare encrypted message + let boundary = make_boundary("_"); + outer_message.extend_from_slice( + concat!( + "Content-Type: multipart/encrypted;\r\n\t", + "protocol=\"application/pgp-encrypted\";\r\n\t", + "boundary=\"" + ) + .as_bytes(), + ); + outer_message.extend_from_slice(boundary.as_bytes()); + outer_message.extend_from_slice( + concat!( + "\"\r\n\r\n", + "OpenPGP/MIME message (Automatically encrypted by Stalwart)\r\n\r\n", + "--" + ) + .as_bytes(), + ); + outer_message.extend_from_slice(boundary.as_bytes()); + outer_message.extend_from_slice( + concat!( + "\r\nContent-Type: application/pgp-encrypted\r\n", + "Version: 1\r\n\r\n--" + ) + .as_bytes(), + ); + outer_message.extend_from_slice(boundary.as_bytes()); + outer_message.extend_from_slice( + concat!( + "\r\nContent-Type: application/octet-stream; name=\"encrypted.asc\"\r\n", + "Content-Disposition: inline; filename=\"encrypted.asc\"\r\n\r\n" + ) + .as_bytes(), + ); + + // Parse public key + let mut keys = Vec::with_capacity(params.certs.len()); + for cert in ¶ms.certs { + keys.push(SignedPublicKey::from_bytes(&cert[..]).map_err(|err| { + EncryptMessageError::Error(format!( + "Failed to parse PGP public key: {}", + err + )) + })?); + } + + // Encrypt contents (TODO: use rayon) + let algo = params.algo; + let encrypted_contents = tokio::task::spawn_blocking(move || { + composed::message::Message::new_literal_bytes("none", &inner_message) + .encrypt_to_keys( + &mut StdRng::from_entropy(), + match algo { + Algorithm::Aes128 => SymmetricKeyAlgorithm::AES128, + Algorithm::Aes256 => SymmetricKeyAlgorithm::AES256, + }, + &keys.iter().collect::>(), + ) + .map_err(|err| { + EncryptMessageError::Error(format!( + "Failed to encrypt message: {}", + err + )) + })? + .to_armored_string(None) + .map_err(|err| { + EncryptMessageError::Error(format!( + "Failed to convert to armored string: {}", + err + )) + }) + }) + .await + .map_err(|err| { + EncryptMessageError::Error(format!("Failed to encrypt message: {}", err)) + })??; + outer_message.extend_from_slice(encrypted_contents.as_bytes()); + outer_message.extend_from_slice(b"\r\n--"); + outer_message.extend_from_slice(boundary.as_bytes()); + outer_message.extend_from_slice(b"--\r\n"); + } + EncryptionMethod::SMIME => { + // Generate random IV + let mut rng = StdRng::from_entropy(); + let mut iv = vec![0u8; 16]; + rng.fill_bytes(&mut iv); + + // Generate random key + let mut key = vec![0u8; params.algo.key_size()]; + rng.fill_bytes(&mut key); + + // Encrypt contents (TODO: use rayon) + let algo = params.algo; + let (encrypted_contents, key, iv) = tokio::task::spawn_blocking(move || { + (algo.encrypt(&key, &iv, &inner_message), key, iv) + }) + .await + .map_err(|err| { + EncryptMessageError::Error(format!("Failed to encrypt message: {}", err)) + })?; + + // Encrypt key using public keys + #[allow(clippy::mutable_key_type)] + let mut recipient_infos = BTreeSet::new(); + for cert in ¶ms.certs { + let cert = + rasn::der::decode::(cert).map_err(|err| { + EncryptMessageError::Error(format!( + "Failed to parse certificate: {}", + err + )) + })?; + + let public_key = RsaPublicKey::from_pkcs1_der( + cert.tbs_certificate + .subject_public_key_info + .subject_public_key + .as_raw_slice(), + ) + .map_err(|err| { + EncryptMessageError::Error(format!("Failed to parse public key: {}", err)) + })?; + let encrypted_key = public_key + .encrypt(&mut rng, Pkcs1v15Encrypt, &key[..]) + .map_err(|err| { + EncryptMessageError::Error(format!("Failed to encrypt key: {}", err)) + }) + .unwrap(); + + recipient_infos.insert(RecipientInfo::KeyTransRecipientInfo( + KeyTransRecipientInfo { + version: 0.into(), + rid: RecipientIdentifier::IssuerAndSerialNumber( + IssuerAndSerialNumber { + issuer: cert.tbs_certificate.issuer, + serial_number: cert.tbs_certificate.serial_number, + }, + ), + key_encryption_algorithm: AlgorithmIdentifier { + algorithm: RSA.into(), + parameters: Some( + rasn::der::encode(&()) + .map_err(|err| { + EncryptMessageError::Error(format!( + "Failed to encode RSA algorithm identifier: {}", + err + )) + })? + .into(), + ), + }, + encrypted_key: EncryptedKey::from(encrypted_key), + }, + )); + } + + let pkcs7 = rasn::der::encode(&EncapsulatedContentInfo { + content_type: CONTENT_ENVELOPED_DATA.into(), + content: Some( + rasn::der::encode(&EnvelopedData { + version: 0.into(), + originator_info: None, + recipient_infos, + encrypted_content_info: EncryptedContentInfo { + content_type: CONTENT_DATA.into(), + content_encryption_algorithm: AlgorithmIdentifier { + algorithm: params.algo.to_algorithm_identifier(), + parameters: Some( + rasn::der::encode(&OctetString::from(iv)) + .map_err(|err| { + EncryptMessageError::Error(format!( + "Failed to encode IV: {}", + err + )) + })? + .into(), + ), + }, + encrypted_content: Some(EncryptedContent::from(encrypted_contents)), + }, + unprotected_attrs: None, + }) + .map_err(|err| { + EncryptMessageError::Error(format!( + "Failed to encode EnvelopedData: {}", + err + )) + })? + .into(), + ), + }) + .map_err(|err| { + EncryptMessageError::Error(format!("Failed to encode ContentInfo: {}", err)) + })?; + + // Generate message + outer_message.extend_from_slice( + concat!( + "Content-Type: application/pkcs7-mime;\r\n", + "\tname=\"smime.p7m\";\r\n", + "\tsmime-type=enveloped-data\r\n", + "Content-Disposition: attachment;\r\n", + "\tfilename=\"smime.p7m\"\r\n", + "Content-Transfer-Encoding: base64\r\n\r\n" + ) + .as_bytes(), + ); + base64_encode_mime(&pkcs7, &mut outer_message, false).map_err(|err| { + EncryptMessageError::Error(format!("Failed to base64 encode PKCS7: {}", err)) + })?; + } + } + + Ok(outer_message) + } + + fn is_encrypted(&self) -> bool { + self.content_type().map_or(false, |ct| { + let main_type = ct.c_type.as_ref(); + let sub_type = ct + .c_subtype + .as_ref() + .map(|s| s.as_ref()) + .unwrap_or_default(); + + (main_type.eq_ignore_ascii_case("application") + && (sub_type.eq_ignore_ascii_case("pkcs7-mime") + || sub_type.eq_ignore_ascii_case("pkcs7-signature") + || (sub_type.eq_ignore_ascii_case("octet-stream") + && self.attachment_name().map_or(false, |name| { + name.rsplit_once('.').map_or(false, |(_, ext)| { + ["p7m", "p7s", "p7c", "p7z"].contains(&ext) + }) + })))) + || (main_type.eq_ignore_ascii_case("multipart") + && sub_type.eq_ignore_ascii_case("encrypted")) + }) + } +} + +impl Algorithm { + fn key_size(&self) -> usize { + match self { + Algorithm::Aes128 => 16, + Algorithm::Aes256 => 32, + } + } + + fn to_algorithm_identifier(self) -> ObjectIdentifier { + match self { + Algorithm::Aes128 => AES128_CBC.into(), + Algorithm::Aes256 => AES256_CBC.into(), + } + } + + fn encrypt(&self, key: &[u8], iv: &[u8], contents: &[u8]) -> Vec { + match self { + Algorithm::Aes128 => cbc::Encryptor::::new(key.into(), iv.into()) + .encrypt_padded_vec_mut::(contents), + Algorithm::Aes256 => cbc::Encryptor::::new(key.into(), iv.into()) + .encrypt_padded_vec_mut::(contents), + } + } +} + +pub fn try_parse_certs(bytes: Vec) -> Result<(EncryptionMethod, Vec>), String> { + // Check if it's a PEM file + if let Some(result) = try_parse_pem(&bytes)? { + Ok(result) + } else if rasn::der::decode::(&bytes[..]).is_ok() { + Ok((EncryptionMethod::SMIME, vec![bytes])) + } else if SignedPublicKey::from_bytes(&bytes[..]).is_ok() { + Ok((EncryptionMethod::PGP, vec![bytes])) + } else { + Err("Could not find any valid certificates".to_string()) + } +} + +#[allow(clippy::type_complexity)] +fn try_parse_pem(bytes: &[u8]) -> Result>)>, String> { + let mut bytes = bytes.iter(); + let mut buf = vec![]; + let mut method = None; + let mut certs = vec![]; + + loop { + // Find start of PEM block + for &ch in bytes.by_ref() { + if ch.is_ascii_whitespace() { + continue; + } else if ch == b'-' { + break; + } else { + return Ok(None); + } + } + + // Find block type + for &ch in bytes.by_ref() { + match ch { + b'-' => (), + b'\n' => break, + _ => { + if ch.is_ascii() { + buf.push(ch.to_ascii_uppercase()); + } else { + return Ok(None); + } + } + } + } + if buf.is_empty() { + break; + } + + // Find type + let tag = std::str::from_utf8(&buf).unwrap(); + if tag.contains("CERTIFICATE") { + if method.map_or(false, |m| m == EncryptionMethod::PGP) { + return Err("Cannot mix PGP and S/MIME certificates".to_string()); + } else { + method = Some(EncryptionMethod::SMIME); + } + } else if tag.contains("PGP") { + if method.map_or(false, |m| m == EncryptionMethod::SMIME) { + return Err("Cannot mix PGP and S/MIME certificates".to_string()); + } else { + method = Some(EncryptionMethod::PGP); + } + } else { + // Ignore block + let mut found_end = false; + for &ch in bytes.by_ref() { + if ch == b'-' { + found_end = true; + } else if ch == b'\n' && found_end { + break; + } + } + buf.clear(); + continue; + } + + // Collect base64 + buf.clear(); + let mut found_end = false; + for &ch in bytes.by_ref() { + match ch { + b'-' => { + found_end = true; + } + b'\n' => { + if found_end { + break; + } + } + _ => { + if !ch.is_ascii_whitespace() { + buf.push(ch); + } + } + } + } + + // Decode base64 + let cert = base64_decode(&buf) + .ok_or_else(|| "Failed to decode base64 certificate.".to_string())?; + match method.unwrap() { + EncryptionMethod::PGP => { + if let Err(err) = SignedPublicKey::from_bytes(&cert[..]) { + return Err(format!("Failed to decode PGP public key: {}", err)); + } + } + EncryptionMethod::SMIME => { + if let Err(err) = rasn::der::decode::(&cert) { + return Err(format!("Failed to decode X509 certificate: {}", err)); + } + } + } + certs.push(cert); + buf.clear(); + } + + Ok(method.map(|method| (method, certs))) +} + +impl Serialize for EncryptionParams { + fn serialize(self) -> Vec { + let len = bincode::serialized_size(&self).unwrap_or_default(); + let mut buf = Vec::with_capacity(len as usize + 1); + buf.push(1); + let _ = bincode::serialize_into(&mut buf, &self); + buf + } +} + +impl Deserialize for EncryptionParams { + fn deserialize(bytes: &[u8]) -> store::Result { + let version = *bytes.first().ok_or_else(|| { + store::Error::InternalError( + "Failed to read version while deserializing encryption params".to_string(), + ) + })?; + match version { + 1 if bytes.len() > 1 => bincode::deserialize(&bytes[1..]).map_err(|err| { + store::Error::InternalError(format!( + "Failed to deserialize encryption params: {}", + err + )) + }), + + _ => Err(store::Error::InternalError(format!( + "Unknown encryption params version: {}", + version + ))), + } + } +} + +impl ToBitmaps for EncryptionParams { + fn to_bitmaps(&self, _: &mut Vec, _: u8, _: bool) { + unreachable!() + } +} + +impl JMAP { + // Code authorization flow, handles an authorization request + pub async fn handle_crypto_update(&self, req: &mut HttpRequest) -> HttpResponse { + let response = match *req.method() { + hyper::Method::POST => { + // Parse form + let form = match FormData::from_request(req, 1024 * 1024).await { + Ok(form) => form, + Err(err) => return err, + }; + + if let Err(error) = self.validate_form(form).await { + let mut response = String::with_capacity( + CRYPT_HTML_HEADER.len() + + CRYPT_HTML_FOOTER.len() + + CRYPT_HTML_ERROR.len() + + error.len(), + ); + + response.push_str(&CRYPT_HTML_HEADER.replace("@@@", "/crypto")); + response.push_str(&CRYPT_HTML_ERROR.replace("@@@", &error)); + response.push_str(CRYPT_HTML_FOOTER); + + response + } else { + let mut response = String::with_capacity( + CRYPT_HTML_HEADER.len() + + CRYPT_HTML_FOOTER.len() + + CRYPT_HTML_SUCCESS.len(), + ); + + response.push_str(&CRYPT_HTML_HEADER.replace("@@@", "/crypto")); + response.push_str(CRYPT_HTML_SUCCESS); + response.push_str(CRYPT_HTML_FOOTER); + + response + } + } + + hyper::Method::GET => { + let mut response = String::with_capacity( + CRYPT_HTML_HEADER.len() + CRYPT_HTML_FOOTER.len() + CRYPT_HTML_FORM.len(), + ); + + response.push_str(&CRYPT_HTML_HEADER.replace("@@@", "/crypto")); + response.push_str(CRYPT_HTML_FORM); + response.push_str(CRYPT_HTML_FOOTER); + + response + } + _ => unreachable!(), + }; + + HtmlResponse::new(response).into_http_response() + } + + async fn validate_form(&self, mut form: FormData) -> Result<(), Cow> { + if let (Some(certificate), Some(email), Some(password), Some(encryption)) = ( + form.remove_bytes("certificate"), + form.get("email"), + form.get("password"), + form.get("encryption"), + ) { + // Validate fields + if email.is_empty() || password.is_empty() { + return Err(Cow::from("Please enter your login and password")); + } else if encryption != "disable" && certificate.is_empty() { + return Err(Cow::from("Please select one or more certificates")); + } + + // Authenticate + let token = self + .authenticate_plain(email, password) + .await + .ok_or_else(|| Cow::from("Invalid login or password"))?; + if encryption != "disable" { + let (method, certs) = try_parse_certs(certificate).map_err(Cow::from)?; + let algo = match (encryption, method) { + ("pgp-256", EncryptionMethod::PGP) => Algorithm::Aes256, + ("pgp-128", EncryptionMethod::PGP) => Algorithm::Aes128, + ("smime-256", EncryptionMethod::SMIME) => Algorithm::Aes256, + ("smime-128", EncryptionMethod::SMIME) => Algorithm::Aes128, + _ => { + return Err(Cow::from( + "No valid certificates found for the selected encryption", + )); + } + }; + let params = EncryptionParams { + method, + algo, + certs, + }; + + // Try a test encryption + if let Err(EncryptMessageError::Error(message)) = + Message::parse("Subject: test\r\ntest\r\n".as_bytes()) + .unwrap() + .encrypt(¶ms) + .await + { + return Err(Cow::from(message)); + } + + // Save encryption params + let mut batch = BatchBuilder::new(); + batch + .with_account_id(token.primary_id()) + .with_collection(Collection::Principal) + .update_document(0) + .value(Property::Parameters, params, F_VALUE); + self.write_batch(batch).await.map_err(|_| { + Cow::from("Failed to save encryption parameters, please try again later") + })?; + } else { + // Remove encryption params + let mut batch = BatchBuilder::new(); + batch + .with_account_id(token.primary_id()) + .with_collection(Collection::Principal) + .update_document(0) + .value(Property::Parameters, (), F_VALUE | F_CLEAR); + self.write_batch(batch).await.map_err(|_| { + Cow::from("Failed to save encryption parameters, please try again later") + })?; + } + + Ok(()) + } else { + Err(Cow::from("Missing form parameters")) + } + } +} diff --git a/crates/jmap/src/email/import.rs b/crates/jmap/src/email/import.rs index a3d2b236..277f3088 100644 --- a/crates/jmap/src/email/import.rs +++ b/crates/jmap/src/email/import.rs @@ -141,6 +141,7 @@ impl JMAP { keywords: email.keywords, received_at: email.received_at.map(|r| r.into()), skip_duplicates: false, + encrypt: true, }) .await { diff --git a/crates/jmap/src/email/ingest.rs b/crates/jmap/src/email/ingest.rs index e6a985b3..a54c66b9 100644 --- a/crates/jmap/src/email/ingest.rs +++ b/crates/jmap/src/email/ingest.rs @@ -21,6 +21,8 @@ * for more details. */ +use std::borrow::Cow; + use jmap_proto::{ object::Object, types::{ @@ -29,7 +31,7 @@ use jmap_proto::{ }, }; use mail_parser::{ - parsers::fields::thread::thread_name, HeaderName, HeaderValue, Message, RfcHeader, + parsers::fields::thread::thread_name, HeaderName, HeaderValue, Message, PartType, RfcHeader, }; use store::{ ahash::AHashSet, @@ -44,7 +46,10 @@ use crate::{ IngestError, JMAP, }; -use super::index::{TrimTextValue, MAX_SORT_FIELD_LENGTH}; +use super::{ + crypto::{EncryptMessage, EncryptMessageError, EncryptionParams}, + index::{TrimTextValue, MAX_SORT_FIELD_LENGTH}, +}; #[derive(Default)] pub struct IngestedEmail { @@ -63,6 +68,7 @@ pub struct IngestEmail<'x> { pub keywords: Vec, pub received_at: Option, pub skip_duplicates: bool, + pub encrypt: bool, } impl JMAP { @@ -72,7 +78,7 @@ impl JMAP { params: IngestEmail<'_>, ) -> Result { // Check quota - let raw_message_len = params.raw_message.len() as i64; + let mut raw_message_len = params.raw_message.len() as i64; if params.account_quota > 0 && raw_message_len + self @@ -85,89 +91,137 @@ impl JMAP { } // Parse message - let raw_message = params.raw_message; - let message = params.message.ok_or_else(|| IngestError::Permanent { + let mut raw_message = Cow::from(params.raw_message); + let mut message = params.message.ok_or_else(|| IngestError::Permanent { code: [5, 5, 0], reason: "Failed to parse e-mail message.".to_string(), })?; // Obtain message references and thread name - let mut references = Vec::with_capacity(5); - let mut subject = ""; - for header in message.root_part().headers().iter().rev() { - match header.name { - HeaderName::Rfc( - RfcHeader::MessageId - | RfcHeader::InReplyTo - | RfcHeader::References - | RfcHeader::ResentMessageId, - ) => match &header.value { - HeaderValue::Text(id) if id.len() < MAX_ID_LENGTH => { - references.push(id.as_ref()); - } - HeaderValue::TextList(ids) => { - for id in ids { - if id.len() < MAX_ID_LENGTH { - references.push(id.as_ref()); + let thread_id = { + let mut references = Vec::with_capacity(5); + let mut subject = ""; + for header in message.root_part().headers().iter().rev() { + match header.name { + HeaderName::Rfc( + RfcHeader::MessageId + | RfcHeader::InReplyTo + | RfcHeader::References + | RfcHeader::ResentMessageId, + ) => match &header.value { + HeaderValue::Text(id) if id.len() < MAX_ID_LENGTH => { + references.push(id.as_ref()); + } + HeaderValue::TextList(ids) => { + for id in ids { + if id.len() < MAX_ID_LENGTH { + references.push(id.as_ref()); + } } } + _ => (), + }, + HeaderName::Rfc(RfcHeader::Subject) if subject.is_empty() => { + subject = thread_name(match &header.value { + HeaderValue::Text(text) => text.as_ref(), + HeaderValue::TextList(list) if !list.is_empty() => { + list.first().unwrap().as_ref() + } + _ => "", + }) + .trim_text(MAX_SORT_FIELD_LENGTH); } _ => (), - }, - HeaderName::Rfc(RfcHeader::Subject) if subject.is_empty() => { - subject = thread_name(match &header.value { - HeaderValue::Text(text) => text.as_ref(), - HeaderValue::TextList(list) if !list.is_empty() => { - list.first().unwrap().as_ref() - } - _ => "", - }) - .trim_text(MAX_SORT_FIELD_LENGTH); } - _ => (), } - } - // Check for duplicates - if params.skip_duplicates - && !references.is_empty() - && !self - .store - .filter( - params.account_id, - Collection::Email, - references - .iter() - .map(|id| Filter::eq(Property::MessageId, *id)) - .collect(), - ) - .await - .map_err(|err| { - tracing::error!( + // Check for duplicates + if params.skip_duplicates + && !references.is_empty() + && !self + .store + .filter( + params.account_id, + Collection::Email, + references + .iter() + .map(|id| Filter::eq(Property::MessageId, *id)) + .collect(), + ) + .await + .map_err(|err| { + tracing::error!( event = "error", context = "find_duplicates", error = ?err, "Duplicate message search failed."); - IngestError::Temporary - })? - .results - .is_empty() - { - return Ok(IngestedEmail { - id: Id::default(), - change_id: u64::MAX, - blob_id: BlobId::default(), - size: 0, - }); - } + IngestError::Temporary + })? + .results + .is_empty() + { + return Ok(IngestedEmail { + id: Id::default(), + change_id: u64::MAX, + blob_id: BlobId::default(), + size: 0, + }); + } - let thread_id = if !references.is_empty() { - self.find_or_merge_thread(params.account_id, subject, &references) - .await? - } else { - None + if !references.is_empty() { + self.find_or_merge_thread(params.account_id, subject, &references) + .await? + } else { + None + } }; + // Encrypt message + if params.encrypt && !message.is_encrypted() { + if let Some(encrypt_params) = self + .get_property::( + params.account_id, + Collection::Principal, + 0, + Property::Parameters, + ) + .await + .map_err(|_| IngestError::Temporary)? + { + match message.encrypt(&encrypt_params).await { + Ok(new_raw_message) => { + raw_message = Cow::from(new_raw_message); + raw_message_len = raw_message.len() as i64; + + // Remove contents from parsed message + for part in &mut message.parts { + match &mut part.body { + PartType::Text(txt) | PartType::Html(txt) => { + *txt = Cow::from(""); + } + PartType::Binary(bin) | PartType::InlineBinary(bin) => { + *bin = Cow::from(&[][..]); + } + PartType::Message(_) => { + part.body = PartType::Binary(Cow::from(&[][..])); + } + PartType::Multipart(_) => (), + } + } + } + Err(EncryptMessageError::Error(err)) => { + tracing::error!( + event = "error", + context = "email_ingest", + error = ?err, + "Failed to encrypt message."); + return Err(IngestError::Temporary); + } + _ => unreachable!(), + } + } + } + // Obtain a documentId and changeId let document_id = self .store @@ -197,7 +251,7 @@ impl JMAP { // Store blob let blob_id = BlobId::maildir(params.account_id, document_id); self.store - .put_blob(&blob_id.kind, raw_message) + .put_blob(&blob_id.kind, raw_message.as_ref()) .await .map_err(|err| { tracing::error!( diff --git a/crates/jmap/src/email/mod.rs b/crates/jmap/src/email/mod.rs index df394285..e0f6a0b8 100644 --- a/crates/jmap/src/email/mod.rs +++ b/crates/jmap/src/email/mod.rs @@ -23,6 +23,7 @@ pub mod body; pub mod copy; +pub mod crypto; pub mod get; pub mod headers; pub mod import; diff --git a/crates/jmap/src/email/set.rs b/crates/jmap/src/email/set.rs index 46f6e23f..ad4eee11 100644 --- a/crates/jmap/src/email/set.rs +++ b/crates/jmap/src/email/set.rs @@ -737,6 +737,7 @@ impl JMAP { keywords, received_at, skip_duplicates: false, + encrypt: false, }) .await { diff --git a/crates/jmap/src/services/ingest.rs b/crates/jmap/src/services/ingest.rs index 78b6cc2b..bad64d89 100644 --- a/crates/jmap/src/services/ingest.rs +++ b/crates/jmap/src/services/ingest.rs @@ -104,6 +104,7 @@ impl JMAP { keywords: vec![], received_at: None, skip_duplicates: true, + encrypt: true, }) .await } diff --git a/crates/jmap/src/sieve/ingest.rs b/crates/jmap/src/sieve/ingest.rs index bc389ecd..eafd87a5 100644 --- a/crates/jmap/src/sieve/ingest.rs +++ b/crates/jmap/src/sieve/ingest.rs @@ -450,6 +450,7 @@ impl JMAP { keywords: sieve_message.flags, received_at: None, skip_duplicates: true, + encrypt: true, }) .await { diff --git a/resources/htx/crypto_error.htx b/resources/htx/crypto_error.htx new file mode 100644 index 00000000..594865b2 --- /dev/null +++ b/resources/htx/crypto_error.htx @@ -0,0 +1 @@ +

Failed to update encryption settings

@@@

\ No newline at end of file diff --git a/resources/htx/crypto_footer.htx b/resources/htx/crypto_footer.htx new file mode 100644 index 00000000..eb03f53a --- /dev/null +++ b/resources/htx/crypto_footer.htx @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/htx/crypto_form.htx b/resources/htx/crypto_form.htx new file mode 100644 index 00000000..e60617a8 --- /dev/null +++ b/resources/htx/crypto_form.htx @@ -0,0 +1 @@ +

Enable encryption at rest for your Stalwart Mail Server account

Select Certificate...
Cancel \ No newline at end of file diff --git a/resources/htx/crypto_header.htx b/resources/htx/crypto_header.htx new file mode 100644 index 00000000..710db304 --- /dev/null +++ b/resources/htx/crypto_header.htx @@ -0,0 +1 @@ +Encryption - Stalwart Mail Server