diff --git a/protobufs/unseal.proto b/protobufs/unseal.proto index 6ca7cca..8c006a0 100644 --- a/protobufs/unseal.proto +++ b/protobufs/unseal.proto @@ -4,13 +4,17 @@ package arbiter.unseal; import "google/protobuf/empty.proto"; -message UnsealStart {} +message UnsealStart { + bytes client_pubkey = 1; +} message UnsealStartResponse { - bytes pubkey = 1; + bytes server_pubkey = 1; } message UnsealEncryptedKey { - bytes key = 1; + bytes nonce = 1; + bytes ciphertext = 2; + bytes associated_data = 3; } enum UnsealResult { diff --git a/server/Cargo.lock b/server/Cargo.lock index 314de1c..fac5caf 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -17,6 +17,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -64,7 +74,9 @@ name = "arbiter-server" version = "0.1.0" dependencies = [ "arbiter-proto", + "argon2", "async-trait", + "chacha20poly1305", "chrono", "dashmap", "diesel", @@ -78,9 +90,12 @@ dependencies = [ "miette", "rand", "rcgen", + "restructed", "rustls", "secrecy", "smlang", + "statig", + "strum", "test-log", "thiserror", "tokio", @@ -88,6 +103,7 @@ dependencies = [ "tonic", "tracing", "tracing-subscriber", + "x25519-dalek", "zeroize", ] @@ -95,6 +111,19 @@ dependencies = [ name = "arbiter-useragent" version = "0.1.0" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures 0.2.17", + "password-hash", + "zeroize", +] + [[package]] name = "asn1-rs" version = "0.7.1" @@ -119,7 +148,7 @@ checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", "synstructure", ] @@ -131,7 +160,7 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -142,7 +171,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -170,9 +199,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.37.0" +version = "0.37.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c34dda4df7017c8db52132f0f8a2e0f8161649d15723ed63fc00c82d0f2081a" +checksum = "b092fe214090261288111db7a2b2c2118e5a7f30dc2569f1732c4069a6840549" dependencies = [ "cc", "cmake", @@ -253,6 +282,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bb8" version = "0.9.1" @@ -267,9 +302,27 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] [[package]] name = "block-buffer" @@ -294,9 +347,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" dependencies = [ "find-msvc-tools", "jobserver", @@ -310,6 +363,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures 0.2.17", +] + [[package]] name = "chacha20" version = "0.10.0" @@ -318,7 +382,20 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", "cpufeatures 0.3.0", - "rand_core", + "rand_core 0.10.0", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20 0.9.1", + "cipher", + "poly1305", + "zeroize", ] [[package]] @@ -335,6 +412,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", + "zeroize", +] + [[package]] name = "cmake" version = "0.1.57" @@ -395,6 +483,17 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "typenum", +] + [[package]] name = "crypto-common" version = "0.2.0" @@ -404,6 +503,21 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "fiat-crypto 0.2.9", + "rustc_version", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek" version = "5.0.0-pre.6" @@ -413,8 +527,8 @@ dependencies = [ "cfg-if", "cpufeatures 0.2.17", "curve25519-dalek-derive", - "digest", - "fiat-crypto", + "digest 0.11.0", + "fiat-crypto 0.3.0", "rustc_version", "subtle", "zeroize", @@ -428,7 +542,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -452,7 +566,7 @@ dependencies = [ "proc-macro2", "quote", "strsim", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -463,7 +577,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -502,9 +616,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.5.5" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +checksum = "cc3dc5ad92c2e2d1c193bbbbdf2ea477cb81331de4f3103f267ca18368b988c4" dependencies = [ "powerfmt", ] @@ -550,7 +664,7 @@ dependencies = [ "dsl_auto_type", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -570,17 +684,28 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fe2444076b48641147115697648dc743c2c00b61adade0f01ce67133c7babe8c" dependencies = [ - "syn 2.0.114", + "syn 2.0.115", ] [[package]] name = "digest" -version = "0.11.0-rc.11" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b42f1d9edf5207c137646b568a0168ca0ec25b7f9eaf7f9961da51a3d91cea" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", + "subtle", +] + +[[package]] +name = "digest" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bf3682cdec91817be507e4aa104314898b95b84d74f3d43882210101a545b6" +dependencies = [ + "block-buffer 0.11.0", + "crypto-common 0.2.0", ] [[package]] @@ -591,7 +716,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -611,7 +736,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -641,9 +766,9 @@ version = "3.0.0-pre.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053618a4c3d3bc24f188aa660ae75a46eeab74ef07fb415c61431e5e7cd4749b" dependencies = [ - "curve25519-dalek", + "curve25519-dalek 5.0.0-pre.6", "ed25519", - "rand_core", + "rand_core 0.10.0", "sha2", "subtle", "zeroize", @@ -683,6 +808,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "fiat-crypto" version = "0.3.0" @@ -737,9 +868,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" dependencies = [ "futures-channel", "futures-core", @@ -752,9 +883,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ "futures-core", "futures-sink", @@ -762,15 +893,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-executor" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" dependencies = [ "futures-core", "futures-task", @@ -779,38 +910,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" [[package]] name = "futures-macro" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] name = "futures-sink" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-channel", "futures-core", @@ -820,10 +951,19 @@ dependencies = [ "futures-task", "memchr", "pin-project-lite", - "pin-utils", "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -856,7 +996,7 @@ dependencies = [ "cfg-if", "libc", "r-efi", - "rand_core", + "rand_core 0.10.0", "wasip2", "wasip3", ] @@ -1080,6 +1220,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "insta" version = "1.46.3" @@ -1157,7 +1306,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1174,9 +1323,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.181" +version = "0.2.182" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" [[package]] name = "libsqlite3-sys" @@ -1268,7 +1417,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1347,7 +1496,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -1408,6 +1557,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "owo-colors" version = "4.2.3" @@ -1437,6 +1592,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "pem" version = "3.0.6" @@ -1481,7 +1647,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1502,6 +1668,17 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -1521,7 +1698,53 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.114", + "syn 2.0.115", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.115", ] [[package]] @@ -1560,7 +1783,7 @@ dependencies = [ "pulldown-cmark", "pulldown-cmark-to-cmark", "regex", - "syn 2.0.114", + "syn 2.0.115", "tempfile", ] @@ -1574,7 +1797,7 @@ dependencies = [ "itertools", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1627,9 +1850,18 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8" dependencies = [ - "chacha20", + "chacha20 0.10.0", "getrandom 0.4.1", - "rand_core", + "rand_core 0.10.0", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -1691,6 +1923,18 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +[[package]] +name = "restructed" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6f6e863d7d9d318699737c043d560dce1ea3cb6f5c78e0a3f0d1f257c73dfc" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.115", +] + [[package]] name = "ring" version = "0.17.14" @@ -1851,7 +2095,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -1884,7 +2128,7 @@ checksum = "7c5f3b1e2dc8aad28310d8410bd4d7e180eca65fca176c52ab00d364475d0024" dependencies = [ "cfg-if", "cpufeatures 0.2.17", - "digest", + "digest 0.11.0", ] [[package]] @@ -1985,6 +2229,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "statig" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c04b4a9f2d66294d63bdd8df834caad9f8e181997c3cf766b6b4f6d12d4fbc" +dependencies = [ + "statig_macro", +] + +[[package]] +name = "statig_macro" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8090ca395ee30c4b38fee68cf4ddf0bcc5f01aa83364cd4c3ec737a1596dab4d" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.115", +] + [[package]] name = "string_morph" version = "0.1.0" @@ -1997,6 +2262,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.115", +] + [[package]] name = "subtle" version = "2.6.1" @@ -2037,9 +2323,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "6e614ed320ac28113fa64972c4262d5dbc89deacdfd00c34a3e4cea073243c12" dependencies = [ "proc-macro2", "quote", @@ -2060,7 +2346,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2104,7 +2390,7 @@ checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2134,7 +2420,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2203,7 +2489,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2243,9 +2529,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.11+spec-1.1.0" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "serde_core", "serde_spanned", @@ -2265,18 +2551,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.0.8+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "0742ff5ff03ea7e67c8ae6c93cac239e0d9784833362da3f9a9c1da8dfefcbdc" dependencies = [ "winnow", ] [[package]] name = "tonic" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a286e33f82f8a1ee2df63f4fa35c0becf4a85a0cb03091a15fd7bf0b402dc94a" +checksum = "7f32a6f80051a4111560201420c7885d0082ba9efe2ab61875c587bb6b18b9a0" dependencies = [ "async-trait", "axum", @@ -2306,14 +2592,14 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27aac809edf60b741e2d7db6367214d078856b8a5bff0087e94ff330fb97b6fc" +checksum = "ce6d8958ed3be404120ca43ffa0fb1e1fc7be214e96c8d33bd43a131b6eebc9e" dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2329,16 +2615,16 @@ dependencies = [ [[package]] name = "tonic-prost-build" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4556786613791cfef4ed134aa670b61a85cfcacf71543ef33e8d801abae988f" +checksum = "65873ace111e90344b8973e94a1fc817c924473affff24629281f90daed1cd2e" dependencies = [ "prettyplease", "proc-macro2", "prost-build", "prost-types", "quote", - "syn 2.0.114", + "syn 2.0.115", "tempfile", "tonic-build", ] @@ -2393,7 +2679,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2483,6 +2769,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" @@ -2497,9 +2793,9 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "uuid" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ "js-sys", "wasm-bindgen", @@ -2517,6 +2813,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "want" version = "0.3.1" @@ -2582,7 +2884,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", "wasm-bindgen-shared", ] @@ -2672,7 +2974,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2683,7 +2985,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", ] [[package]] @@ -2911,7 +3213,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.114", + "syn 2.0.115", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2927,7 +3229,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn 2.0.114", + "syn 2.0.115", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -2969,6 +3271,18 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "x509-parser" version = "0.18.1" @@ -3001,12 +3315,26 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] [[package]] name = "zmij" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zstd" diff --git a/server/crates/.DS_Store b/server/crates/.DS_Store new file mode 100644 index 0000000..2db1fe5 Binary files /dev/null and b/server/crates/.DS_Store differ diff --git a/server/crates/arbiter-proto/build.rs b/server/crates/arbiter-proto/build.rs index 77136cb..1294e9e 100644 --- a/server/crates/arbiter-proto/build.rs +++ b/server/crates/arbiter-proto/build.rs @@ -3,6 +3,9 @@ use tonic_prost_build::configure; static PROTOBUF_DIR: &str = "../../../protobufs"; fn main() -> Result<(), Box> { + + println!("cargo::rerun-if-changed={PROTOBUF_DIR}"); + configure() .message_attribute(".", "#[derive(::kameo::Reply)]") .compile_protos( diff --git a/server/crates/arbiter-server/.DS_Store b/server/crates/arbiter-server/.DS_Store new file mode 100644 index 0000000..d8eda6f Binary files /dev/null and b/server/crates/arbiter-server/.DS_Store differ diff --git a/server/crates/arbiter-server/Cargo.toml b/server/crates/arbiter-server/Cargo.toml index f13a07c..758914b 100644 --- a/server/crates/arbiter-server/Cargo.toml +++ b/server/crates/arbiter-server/Cargo.toml @@ -5,13 +5,7 @@ edition = "2024" repository = "https://git.markettakers.org/MarketTakers/arbiter" [dependencies] -diesel = { version = "2.3.6", features = [ - "sqlite", - "uuid", - "time", - "chrono", - "serde_json", -] } +diesel = { version = "2.3.6", features = ["chrono", "returning_clauses_for_sqlite_3_35", "serde_json", "time", "uuid"] } diesel-async = { version = "0.7.4", features = [ "bb8", "migrations", @@ -45,6 +39,12 @@ chrono.workspace = true memsafe = "0.4.0" zeroize = { version = "1.8.2", features = ["std", "simd"] } kameo.workspace = true +x25519-dalek = { version = "2.0.1", features = ["getrandom"] } +chacha20poly1305 = { version = "0.10.1", features = ["std"] } +statig = { version = "0.4.1", features = ["async"] } +argon2 = { version = "0.5.3", features = ["zeroize"] } +restructed = "0.2.2" +strum = { version = "0.27.2", features = ["derive"] } [dev-dependencies] insta = "1.46.3" diff --git a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql index 1ca612d..b5daf58 100644 --- a/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql +++ b/server/crates/arbiter-server/migrations/2026-02-14-171124-0000_init/up.sql @@ -1,22 +1,34 @@ create table if not exists aead_encrypted ( id INTEGER not null PRIMARY KEY, - current_nonce integer not null default(1), -- if re-encrypted, this should be incremented + current_nonce blob not null default(1), -- if re-encrypted, this should be incremented ciphertext blob not null, tag blob not null, - schema_version integer not null default(1) -- server would need to reencrypt, because this means that we have changed algorithm + schema_version integer not null default(1), -- server would need to reencrypt, because this means that we have changed algorithm + created_at integer not null default(unixepoch ('now')) +) STRICT; + +create table if not exists root_key_history ( + id INTEGER not null PRIMARY KEY, + -- root key stored as aead encrypted artifact, with only difference that it's decrypted by unseal key (derived from user password) + root_key_encryption_nonce blob not null default(1), -- if re-encrypted, this should be incremented. Used for encrypting root key + data_encryption_nonce blob not null default(1), -- nonce used for encrypting with key itself + ciphertext blob not null, + tag blob not null, + schema_version integer not null default(1), -- server would need to reencrypt, because this means that we have changed algorithm + salt blob not null -- for key deriviation ) STRICT; -- This is a singleton create table if not exists arbiter_settings ( id INTEGER not null PRIMARY KEY CHECK (id = 1), -- singleton row, id must be 1 - root_key_id integer references aead_encrypted (id) on delete RESTRICT, -- if null, means wasn't bootstrapped yet + root_key_id integer references root_key_history (id) on delete RESTRICT, -- if null, means wasn't bootstrapped yet cert_key blob not null, cert blob not null ) STRICT; create table if not exists useragent_client ( id integer not null primary key, - nonce integer not null default (1), -- used for auth challenge + nonce integer not null default(1), -- used for auth challenge public_key blob not null, created_at integer not null default(unixepoch ('now')), updated_at integer not null default(unixepoch ('now')) @@ -24,7 +36,7 @@ create table if not exists useragent_client ( create table if not exists program_client ( id integer not null primary key, - nonce integer not null default (1), -- used for auth challenge + nonce integer not null default(1), -- used for auth challenge public_key blob not null, created_at integer not null default(unixepoch ('now')), updated_at integer not null default(unixepoch ('now')) diff --git a/server/crates/arbiter-server/src/.DS_Store b/server/crates/arbiter-server/src/.DS_Store new file mode 100644 index 0000000..4f32ec0 Binary files /dev/null and b/server/crates/arbiter-server/src/.DS_Store differ diff --git a/server/crates/arbiter-server/src/actors.rs b/server/crates/arbiter-server/src/actors.rs index 2cb579e..a987633 100644 --- a/server/crates/arbiter-server/src/actors.rs +++ b/server/crates/arbiter-server/src/actors.rs @@ -1,3 +1,4 @@ pub mod user_agent; pub mod client; -pub(crate) mod bootstrap; \ No newline at end of file +pub(crate) mod bootstrap; +pub(crate) mod keyholder; \ No newline at end of file diff --git a/server/crates/arbiter-server/src/actors/bootstrap.rs b/server/crates/arbiter-server/src/actors/bootstrap.rs index 211344c..b25788d 100644 --- a/server/crates/arbiter-server/src/actors/bootstrap.rs +++ b/server/crates/arbiter-server/src/actors/bootstrap.rs @@ -1,19 +1,13 @@ use arbiter_proto::{BOOTSTRAP_TOKEN_PATH, home_path}; -use diesel::{ExpressionMethods, QueryDsl}; +use diesel::QueryDsl; use diesel_async::RunQueryDsl; use kameo::{Actor, messages}; -use memsafe::MemSafe; use miette::Diagnostic; use rand::{RngExt, distr::StandardUniform, make_rng, rngs::StdRng}; -use secrecy::SecretString; use thiserror::Error; use tracing::info; -use zeroize::{Zeroize, Zeroizing}; -use crate::{ - context::{self, ServerContext}, - db::{self, DatabasePool, schema}, -}; +use crate::db::{self, DatabasePool, schema}; const TOKEN_LENGTH: usize = 64; diff --git a/server/crates/arbiter-server/src/actors/keyholder.rs b/server/crates/arbiter-server/src/actors/keyholder.rs new file mode 100644 index 0000000..00b1113 --- /dev/null +++ b/server/crates/arbiter-server/src/actors/keyholder.rs @@ -0,0 +1,583 @@ +use diesel::{ + ExpressionMethods as _, OptionalExtension, QueryDsl, SelectableHelper, + dsl::{insert_into, update}, +}; +use diesel_async::{AsyncConnection, RunQueryDsl}; +use kameo::{Actor, messages}; +use memsafe::MemSafe; +use tracing::{error, info}; + +use crate::{ + actors::keyholder::v1::{KeyCell, Nonce}, + db::{ + self, + models::{self, RootKeyHistory}, + schema::{self}, + }, +}; + +pub mod v1; + +#[derive(Default)] +enum State { + #[default] + Unbootstrapped, + Sealed { + encrypted_root_key: RootKeyHistory, + data_encryption_nonce: v1::Nonce, + root_key_encryption_nonce: v1::Nonce, + }, + Unsealed { + root_key_history_id: i32, + root_key: KeyCell, + nonce: v1::Nonce, + }, +} + +#[derive(Debug, thiserror::Error, miette::Diagnostic)] +pub enum Error { + #[error("Keyholder is already bootstrapped")] + #[diagnostic(code(arbiter::keyholder::already_bootstrapped))] + AlreadyBootstrapped, + #[error("Keyholder is not bootstrapped")] + #[diagnostic(code(arbiter::keyholder::not_bootstrapped))] + NotBootstrapped, + #[error("Invalid key provided")] + #[diagnostic(code(arbiter::keyholder::invalid_key))] + InvalidKey, + + #[error("Requested aead entry not found")] + #[diagnostic(code(arbiter::keyholder::aead_not_found))] + NotFound, + + #[error("Encryption error: {0}")] + #[diagnostic(code(arbiter::keyholder::encryption_error))] + Encryption(#[from] chacha20poly1305::aead::Error), + + #[error("Database error: {0}")] + #[diagnostic(code(arbiter::keyholder::database_error))] + DatabaseConnection(#[from] db::PoolError), + + #[error("Database transaction error: {0}")] + #[diagnostic(code(arbiter::keyholder::database_transaction_error))] + DatabaseTransaction(#[from] diesel::result::Error), + + #[error("Broken database")] + #[diagnostic(code(arbiter::keyholder::broken_database))] + BrokenDatabase, +} + +/// Manages vault root key and tracks current state of the vault (bootstrapped/unbootstrapped, sealed/unsealed). +/// Provides API for encrypting and decrypting data using the vault root key. +/// Abstraction over database to make sure nonces are never reused and encryption keys are never exposed in plaintext outside of this actor. +#[derive(Actor)] +pub struct KeyHolderActor { + db: db::DatabasePool, + state: State, +} + +#[messages] +impl KeyHolderActor { + pub async fn new(db: db::DatabasePool) -> Result { + let state = { + let mut conn = db.get().await?; + + let (root_key_history,) = schema::arbiter_settings::table + .left_join(schema::root_key_history::table) + .select((Option::::as_select(),)) + .get_result::<(Option,)>(&mut conn) + .await?; + + match root_key_history { + Some(root_key_history) => State::Sealed { + data_encryption_nonce: Nonce::try_from( + root_key_history.data_encryption_nonce.as_slice(), + ) + .map_err(|_| { + error!("Broken database: invalid data encryption nonce"); + Error::BrokenDatabase + })?, + root_key_encryption_nonce: Nonce::try_from( + root_key_history.root_key_encryption_nonce.as_slice(), + ) + .map_err(|_| { + error!("Broken database: invalid root key encryption nonce"); + Error::BrokenDatabase + })?, + encrypted_root_key: root_key_history, + }, + None => State::Unbootstrapped, + } + }; + + Ok(Self { db, state }) + } + + #[message] + pub async fn bootstrap(&mut self, seal_key_raw: MemSafe>) -> Result<(), Error> { + if !matches!(self.state, State::Unbootstrapped) { + return Err(Error::AlreadyBootstrapped); + } + let salt = v1::generate_salt(); + let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt); + let mut root_key = KeyCell::new_secure_random(); + + let root_key_nonce = v1::Nonce::default(); + let data_encryption_nonce = v1::Nonce::default(); + + let root_key_ciphertext: Vec = { + let root_key_reader = root_key.0.read().unwrap(); + let root_key_reader = root_key_reader.as_slice(); + seal_key + .encrypt(&root_key_nonce, v1::ROOT_KEY_TAG, root_key_reader) + .map_err(|err| { + error!(?err, "Fatal bootstrap error"); + Error::Encryption(err) + })? + }; + + let mut conn = self.db.get().await?; + + let data_encryption_nonce_bytes = data_encryption_nonce.to_vec(); + let root_key_history_id = conn + .transaction(|conn| { + Box::pin(async move { + let root_key_history_id: i32 = insert_into(schema::root_key_history::table) + .values(&models::NewRootKeyHistory { + ciphertext: root_key_ciphertext, + tag: v1::ROOT_KEY_TAG.to_vec(), + root_key_encryption_nonce: root_key_nonce.to_vec(), + data_encryption_nonce: data_encryption_nonce_bytes, + schema_version: 1, + salt: salt.to_vec(), + }) + .returning(schema::root_key_history::id) + .get_result(conn) + .await?; + + update(schema::arbiter_settings::table) + .set(schema::arbiter_settings::root_key_id.eq(root_key_history_id)) + .execute(conn) + .await?; + + Result::<_, diesel::result::Error>::Ok(root_key_history_id) + }) + }) + .await?; + + self.state = State::Unsealed { + root_key, + root_key_history_id, + nonce: data_encryption_nonce, + }; + + info!("Keyholder bootstrapped successfully"); + + Ok(()) + } + + #[message] + pub async fn try_unseal(&mut self, seal_key_raw: MemSafe>) -> Result<(), Error> { + let State::Sealed { + encrypted_root_key, + data_encryption_nonce, + root_key_encryption_nonce, + } = &mut self.state + else { + return Err(Error::NotBootstrapped); + }; + + let salt = &encrypted_root_key.salt; + let salt = v1::Salt::try_from(salt.as_slice()).map_err(|_| { + error!("Broken database: invalid salt for root key"); + Error::BrokenDatabase + })?; + let mut seal_key = v1::derive_seal_key(seal_key_raw, &salt); + + let mut root_key = MemSafe::new(encrypted_root_key.ciphertext.clone()).unwrap(); + seal_key + .decrypt_in_place(root_key_encryption_nonce, v1::ROOT_KEY_TAG, &mut root_key) + .map_err(|err| { + error!(?err, "Failed to unseal root key: invalid seal key"); + Error::InvalidKey + })?; + + self.state = State::Unsealed { + root_key_history_id: encrypted_root_key.id, + root_key: v1::KeyCell::try_from(root_key).map_err(|err| { + error!(?err, "Broken database: invalid encryption key size"); + Error::BrokenDatabase + })?, + nonce: std::mem::take(data_encryption_nonce), // we are replacing state, so it's safe to take the nonce out of it + }; + + info!("Keyholder unsealed successfully"); + + Ok(()) + } + + // Decrypts the `aead_encrypted` entry with the given ID and returns the plaintext + #[message] + pub async fn decrypt(&mut self, aead_id: i32) -> Result>, Error> { + let State::Unsealed { root_key, .. } = &mut self.state else { + return Err(Error::NotBootstrapped); + }; + let mut conn = self.db.get().await?; + let row: models::AeadEncrypted = schema::aead_encrypted::table + .select(models::AeadEncrypted::as_select()) + .filter(schema::aead_encrypted::id.eq(aead_id)) + .first(&mut conn) + .await + .optional()? + .ok_or(Error::NotFound)?; + + let nonce = v1::Nonce::try_from(row.current_nonce.as_slice()).map_err(|_| { + error!( + "Broken database: invalid nonce for aead_encrypted id={}", + aead_id + ); + Error::BrokenDatabase + })?; + let mut output = MemSafe::new(row.ciphertext).unwrap(); + root_key.decrypt_in_place(&nonce, v1::TAG, &mut output)?; + Ok(output) + } + + // Creates new `aead_encrypted` entry in the database and returns it's ID + #[message] + pub async fn create_new(&mut self, mut plaintext: MemSafe>) -> Result { + let State::Unsealed { + root_key, + root_key_history_id, + nonce, + } = &mut self.state + else { + return Err(Error::NotBootstrapped); + }; + + let mut conn = self.db.get().await?; + nonce.increment(); + + let mut ciphertext_buffer = plaintext.write().unwrap(); + let ciphertext_buffer: &mut Vec = ciphertext_buffer.as_mut(); + root_key.encrypt_in_place(&nonce, v1::TAG, &mut *ciphertext_buffer)?; + + let ciphertext = std::mem::take(ciphertext_buffer); + + let aead_id: i32 = conn + .transaction(|conn| { + Box::pin(async move { + let aead_id: i32 = insert_into(schema::aead_encrypted::table) + .values(&models::NewAeadEncrypted { + ciphertext, + tag: v1::TAG.to_vec(), + current_nonce: nonce.to_vec(), + schema_version: 1, + created_at: chrono::Utc::now().timestamp() as i32, + }) + .returning(schema::aead_encrypted::id) + .get_result(conn) + .await?; + + update(schema::root_key_history::table) + .filter(schema::root_key_history::id.eq(*root_key_history_id)) + .set(schema::root_key_history::data_encryption_nonce.eq(nonce.to_vec())) + .execute(conn) + .await?; + + Result::<_, diesel::result::Error>::Ok(aead_id) + }) + }) + .await?; + + Ok(aead_id) + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashSet; + + use diesel::dsl::insert_into; + use diesel_async::RunQueryDsl; + use memsafe::MemSafe; + + use crate::db::{self, models::ArbiterSetting}; + + use super::*; + + async fn seed_settings(pool: &db::DatabasePool) { + let mut conn = pool.get().await.unwrap(); + insert_into(schema::arbiter_settings::table) + .values(&ArbiterSetting { + id: 1, + root_key_id: None, + cert_key: vec![], + cert: vec![], + }) + .execute(&mut conn) + .await + .unwrap(); + } + + async fn bootstrapped_actor(db: &db::DatabasePool) -> KeyHolderActor { + seed_settings(db).await; + let mut actor = KeyHolderActor::new(db.clone()).await.unwrap(); + let seal_key = MemSafe::new(b"test-seal-key".to_vec()).unwrap(); + actor.bootstrap(seal_key).await.unwrap(); + actor + } + + #[tokio::test] + #[test_log::test] + async fn test_bootstrap() { + let db = db::create_test_pool().await; + seed_settings(&db).await; + let mut actor = KeyHolderActor::new(db.clone()).await.unwrap(); + + assert!(matches!(actor.state, State::Unbootstrapped)); + + let seal_key = MemSafe::new(b"test-seal-key".to_vec()).unwrap(); + actor.bootstrap(seal_key).await.unwrap(); + + assert!(matches!(actor.state, State::Unsealed { .. })); + + let mut conn = db.get().await.unwrap(); + let row: models::RootKeyHistory = schema::root_key_history::table + .select(models::RootKeyHistory::as_select()) + .first(&mut conn) + .await + .unwrap(); + + assert_eq!(row.schema_version, 1); + assert_eq!(row.tag, v1::ROOT_KEY_TAG); + assert!(!row.ciphertext.is_empty()); + assert!(!row.salt.is_empty()); + assert_eq!(row.data_encryption_nonce, v1::Nonce::default().to_vec()); + } + + #[tokio::test] + #[test_log::test] + async fn test_bootstrap_rejects_double() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + + let seal_key2 = MemSafe::new(b"test-seal-key".to_vec()).unwrap(); + let err = actor.bootstrap(seal_key2).await.unwrap_err(); + assert!(matches!(err, Error::AlreadyBootstrapped)); + } + + #[tokio::test] + #[test_log::test] + async fn test_create_decrypt_roundtrip() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + + let plaintext = b"hello arbiter"; + let aead_id = actor + .create_new(MemSafe::new(plaintext.to_vec()).unwrap()) + .await + .unwrap(); + + let mut decrypted = actor.decrypt(aead_id).await.unwrap(); + let decrypted = decrypted.read().unwrap(); + assert_eq!(*decrypted, plaintext); + } + + #[tokio::test] + #[test_log::test] + async fn test_create_new_before_bootstrap_fails() { + let db = db::create_test_pool().await; + seed_settings(&db).await; + let mut actor = KeyHolderActor::new(db).await.unwrap(); + + let err = actor + .create_new(MemSafe::new(b"data".to_vec()).unwrap()) + .await + .unwrap_err(); + assert!(matches!(err, Error::NotBootstrapped)); + } + + #[tokio::test] + #[test_log::test] + async fn test_decrypt_before_bootstrap_fails() { + let db = db::create_test_pool().await; + seed_settings(&db).await; + let mut actor = KeyHolderActor::new(db).await.unwrap(); + + let err = actor.decrypt(1).await.unwrap_err(); + assert!(matches!(err, Error::NotBootstrapped)); + } + + #[tokio::test] + #[test_log::test] + async fn test_decrypt_nonexistent_returns_not_found() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + + let err = actor.decrypt(9999).await.unwrap_err(); + assert!(matches!(err, Error::NotFound)); + } + + #[tokio::test] + #[test_log::test] + async fn test_new_restores_sealed_state() { + let db = db::create_test_pool().await; + let actor = bootstrapped_actor(&db).await; + drop(actor); + + let actor2 = KeyHolderActor::new(db).await.unwrap(); + assert!(matches!(actor2.state, State::Sealed { .. })); + } + + #[tokio::test] + #[test_log::test] + async fn test_nonce_never_reused() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + + let n = 5; + let mut ids = Vec::with_capacity(n); + for i in 0..n { + let id = actor + .create_new(MemSafe::new(format!("secret {i}").into_bytes()).unwrap()) + .await + .unwrap(); + ids.push(id); + } + + // read all stored nonces from DB + let mut conn = db.get().await.unwrap(); + let rows: Vec = schema::aead_encrypted::table + .select(models::AeadEncrypted::as_select()) + .load(&mut conn) + .await + .unwrap(); + + assert_eq!(rows.len(), n); + + let nonces: Vec<&Vec> = rows.iter().map(|r| &r.current_nonce).collect(); + let unique: HashSet<&Vec> = nonces.iter().copied().collect(); + assert_eq!(nonces.len(), unique.len(), "all nonces must be unique"); + + // verify nonces are sequential increments from 1 + for (i, row) in rows.iter().enumerate() { + let mut expected = v1::Nonce::default(); + for _ in 0..=i { + expected.increment(); + } + assert_eq!(row.current_nonce, expected.to_vec(), "nonce {i} mismatch"); + } + + // verify data_encryption_nonce on root_key_history tracks the latest nonce + let root_row: models::RootKeyHistory = schema::root_key_history::table + .select(models::RootKeyHistory::as_select()) + .first(&mut conn) + .await + .unwrap(); + let last_nonce = &rows.last().unwrap().current_nonce; + assert_eq!( + &root_row.data_encryption_nonce, last_nonce, + "root_key_history must track the latest nonce" + ); + } + + #[tokio::test] + #[test_log::test] + async fn test_unseal_correct_password() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + + let plaintext = b"survive a restart"; + let aead_id = actor + .create_new(MemSafe::new(plaintext.to_vec()).unwrap()) + .await + .unwrap(); + drop(actor); + + let mut actor = KeyHolderActor::new(db.clone()).await.unwrap(); + assert!(matches!(actor.state, State::Sealed { .. })); + + let seal_key = MemSafe::new(b"test-seal-key".to_vec()).unwrap(); + actor.try_unseal(seal_key).await.unwrap(); + assert!(matches!(actor.state, State::Unsealed { .. })); + + // previously encrypted data is still decryptable + let mut decrypted = actor.decrypt(aead_id).await.unwrap(); + assert_eq!(*decrypted.read().unwrap(), plaintext); + } + + #[tokio::test] + #[test_log::test] + async fn test_unseal_wrong_then_correct_password() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + + let plaintext = b"important data"; + let aead_id = actor + .create_new(MemSafe::new(plaintext.to_vec()).unwrap()) + .await + .unwrap(); + drop(actor); + + let mut actor = KeyHolderActor::new(db.clone()).await.unwrap(); + assert!(matches!(actor.state, State::Sealed { .. })); + + // wrong password + let bad_key = MemSafe::new(b"wrong-password".to_vec()).unwrap(); + let err = actor.try_unseal(bad_key).await.unwrap_err(); + assert!(matches!(err, Error::InvalidKey)); + assert!( + matches!(actor.state, State::Sealed { .. }), + "state must remain Sealed after failed attempt" + ); + + // correct password + let good_key = MemSafe::new(b"test-seal-key".to_vec()).unwrap(); + actor.try_unseal(good_key).await.unwrap(); + assert!(matches!(actor.state, State::Unsealed { .. })); + + let mut decrypted = actor.decrypt(aead_id).await.unwrap(); + assert_eq!(*decrypted.read().unwrap(), plaintext); + } + + #[tokio::test] + #[test_log::test] + async fn test_ciphertext_differs_across_entries() { + let db = db::create_test_pool().await; + let mut actor = bootstrapped_actor(&db).await; + + let plaintext = b"same content"; + let id1 = actor + .create_new(MemSafe::new(plaintext.to_vec()).unwrap()) + .await + .unwrap(); + let id2 = actor + .create_new(MemSafe::new(plaintext.to_vec()).unwrap()) + .await + .unwrap(); + + // different nonces => different ciphertext, even for identical plaintext + let mut conn = db.get().await.unwrap(); + let row1: models::AeadEncrypted = schema::aead_encrypted::table + .filter(schema::aead_encrypted::id.eq(id1)) + .select(models::AeadEncrypted::as_select()) + .first(&mut conn) + .await + .unwrap(); + let row2: models::AeadEncrypted = schema::aead_encrypted::table + .filter(schema::aead_encrypted::id.eq(id2)) + .select(models::AeadEncrypted::as_select()) + .first(&mut conn) + .await + .unwrap(); + + assert_ne!(row1.ciphertext, row2.ciphertext); + + // but both decrypt to the same plaintext + let mut d1 = actor.decrypt(id1).await.unwrap(); + let mut d2 = actor.decrypt(id2).await.unwrap(); + assert_eq!(*d1.read().unwrap(), plaintext); + assert_eq!(*d2.read().unwrap(), plaintext); + } +} diff --git a/server/crates/arbiter-server/src/actors/keyholder/v1.rs b/server/crates/arbiter-server/src/actors/keyholder/v1.rs new file mode 100644 index 0000000..64fa2dc --- /dev/null +++ b/server/crates/arbiter-server/src/actors/keyholder/v1.rs @@ -0,0 +1,241 @@ +use std::ops::Deref as _; + +use argon2::{Algorithm, Argon2, password_hash::Salt as ArgonSalt}; +use chacha20poly1305::{ + AeadInPlace, Key, KeyInit as _, XChaCha20Poly1305, XNonce, + aead::{AeadMut, Error, Payload}, +}; +use memsafe::MemSafe; +use rand::{ + Rng as _, SeedableRng, + rngs::{StdRng, SysRng}, +}; + +pub const ROOT_KEY_TAG: &[u8] = "arbiter/seal/v1".as_bytes(); +pub const TAG: &[u8] = "arbiter/private-key/v1".as_bytes(); +pub const NONCE_LENGTH: usize = 24; + +#[derive(Default)] +pub struct Nonce([u8; NONCE_LENGTH]); +impl Nonce { + pub fn increment(&mut self) { + for i in (0..self.0.len()).rev() { + if self.0[i] == 0xFF { + self.0[i] = 0; + } else { + self.0[i] += 1; + break; + } + } + } + + pub fn to_vec(&self) -> Vec { + self.0.to_vec() + } +} +impl<'a> TryFrom<&'a [u8]> for Nonce { + type Error = (); + + fn try_from(value: &'a [u8]) -> Result { + if value.len() != NONCE_LENGTH { + return Err(()); + } + let mut nonce = [0u8; NONCE_LENGTH]; + nonce.copy_from_slice(&value); + Ok(Self(nonce)) + } +} + +pub struct KeyCell(pub(super) MemSafe); +impl From> for KeyCell { + fn from(value: MemSafe) -> Self { + Self(value) + } +} +impl TryFrom>> for KeyCell { + type Error = (); + + fn try_from(mut value: MemSafe>) -> Result { + let value = value.read().unwrap(); + if value.len() != size_of::() { + return Err(()); + } + let mut cell = MemSafe::new(Key::default()).unwrap(); + { + let mut cell_write = cell.write().unwrap(); + let cell_slice: &mut [u8] = cell_write.as_mut(); + cell_slice.copy_from_slice(&value); + } + Ok(Self(cell)) + } +} + +impl KeyCell { + pub fn new_secure_random() -> Self { + let mut key = MemSafe::new(Key::default()).unwrap(); + { + let mut key_buffer = key.write().unwrap(); + let key_buffer: &mut [u8] = key_buffer.as_mut(); + + let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap(); + rng.fill_bytes(key_buffer); + } + + key.into() + } + + pub fn into_inner(self) -> MemSafe { + self.0 + } + + pub fn encrypt_in_place( + &mut self, + nonce: &Nonce, + associated_data: &[u8], + mut buffer: impl AsMut>, + ) -> Result<(), Error> { + let key_reader = self.0.read().unwrap(); + let key_ref = key_reader.deref(); + let cipher = XChaCha20Poly1305::new(key_ref); + let nonce = XNonce::from_slice(nonce.0.as_ref()); + let buffer = buffer.as_mut(); + cipher.encrypt_in_place(nonce, associated_data, buffer) + } + pub fn decrypt_in_place( + &mut self, + nonce: &Nonce, + associated_data: &[u8], + buffer: &mut MemSafe>, + ) -> Result<(), Error> { + let key_reader = self.0.read().unwrap(); + let key_ref = key_reader.deref(); + let cipher = XChaCha20Poly1305::new(key_ref); + let nonce = XNonce::from_slice(nonce.0.as_ref()); + let mut buffer = buffer.write().unwrap(); + let buffer: &mut Vec = buffer.as_mut(); + cipher.decrypt_in_place(nonce, associated_data, buffer) + } + + pub fn encrypt( + &mut self, + nonce: &Nonce, + associated_data: &[u8], + plaintext: impl AsRef<[u8]>, + ) -> Result, Error> { + let key_reader = self.0.read().unwrap(); + let key_ref = key_reader.deref(); + let mut cipher = XChaCha20Poly1305::new(key_ref); + let nonce = XNonce::from_slice(nonce.0.as_ref()); + + + let ciphertext = cipher.encrypt( + &nonce, + Payload { + msg: plaintext.as_ref(), + aad: associated_data, + }, + )?; + Ok(ciphertext) + } +} + +pub type Salt = [u8; ArgonSalt::RECOMMENDED_LENGTH]; + +pub(super) fn generate_salt() -> Salt { + let mut salt = Salt::default(); + let mut rng = StdRng::try_from_rng(&mut SysRng).unwrap(); + rng.fill_bytes(&mut salt); + salt +} + +/// User password might be of different length, have not enough entropy, etc... +/// Derive a fixed-length key from the password using Argon2id, which is designed for password hashing and key derivation. +pub(super) fn derive_seal_key(mut password: MemSafe>, salt: &Salt) -> KeyCell { + let params = argon2::Params::new(262_144, 3, 4, None).unwrap(); + let hasher = Argon2::new(Algorithm::Argon2id, argon2::Version::V0x13, params); + let mut key = MemSafe::new(Key::default()).unwrap(); + { + let password_source = password.read().unwrap(); + let mut key_buffer = key.write().unwrap(); + let key_buffer: &mut [u8] = key_buffer.as_mut(); + + hasher + .hash_password_into(password_source.deref(), salt, key_buffer) + .unwrap(); + } + + key.into() +} + +#[cfg(test)] +mod tests { + use super::*; + use memsafe::MemSafe; + + #[test] + pub fn derive_seal_key_deterministic() { + static PASSWORD: &[u8] = b"password"; + let password = MemSafe::new(PASSWORD.to_vec()).unwrap(); + let password2 = MemSafe::new(PASSWORD.to_vec()).unwrap(); + let salt = generate_salt(); + + let mut key1 = derive_seal_key(password, &salt); + let mut key2 = derive_seal_key(password2, &salt); + + let key1_reader = key1.0.read().unwrap(); + let key2_reader = key2.0.read().unwrap(); + + assert_eq!(key1_reader.deref(), key2_reader.deref()); + } + + #[test] + pub fn successful_derive() { + static PASSWORD: &[u8] = b"password"; + let password = MemSafe::new(PASSWORD.to_vec()).unwrap(); + let salt = generate_salt(); + + let mut key = derive_seal_key(password, &salt); + let key_reader = key.0.read().unwrap(); + let key_ref = key_reader.deref(); + + assert_ne!(key_ref.as_slice(), &[0u8; 32][..]); + } + + #[test] + pub fn encrypt_decrypt() { + static PASSWORD: &[u8] = b"password"; + let password = MemSafe::new(PASSWORD.to_vec()).unwrap(); + let salt = generate_salt(); + + let mut key = derive_seal_key(password, &salt); + let nonce = Nonce(*b"unique nonce 123 1231233"); // 24 bytes for XChaCha20Poly1305 + let associated_data = b"associated data"; + let mut buffer = b"secret data".to_vec(); + + key.encrypt_in_place(&nonce, associated_data, &mut buffer) + .unwrap(); + assert_ne!(buffer, b"secret data"); + + let mut buffer = MemSafe::new(buffer).unwrap(); + + key.decrypt_in_place(&nonce, associated_data, &mut buffer) + .unwrap(); + + let buffer = buffer.read().unwrap(); + assert_eq!(*buffer, b"secret data"); + } + + #[test] + // We should fuzz this + pub fn test_nonce_increment() { + let mut nonce = Nonce([0u8; NONCE_LENGTH]); + nonce.increment(); + + assert_eq!( + nonce.0, + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 + ] + ); + } +} diff --git a/server/crates/arbiter-server/src/actors/user_agent.rs b/server/crates/arbiter-server/src/actors/user_agent.rs index 59f7616..0542082 100644 --- a/server/crates/arbiter-server/src/actors/user_agent.rs +++ b/server/crates/arbiter-server/src/actors/user_agent.rs @@ -1,104 +1,50 @@ +use std::{ + ops::DerefMut, + sync::Mutex, +}; + use arbiter_proto::proto::{ UserAgentResponse, auth::{ - self, AuthChallenge, AuthChallengeRequest, AuthOk, ClientMessage, - ServerMessage as AuthServerMessage, client_message::Payload as ClientAuthPayload, + self, AuthChallengeRequest, AuthOk, ServerMessage as AuthServerMessage, server_message::Payload as ServerAuthPayload, }, - user_agent_request::Payload as UserAgentRequestPayload, + unseal::{UnsealEncryptedKey, UnsealResult, UnsealStart, UnsealStartResponse}, user_agent_response::Payload as UserAgentResponsePayload, }; +use chacha20poly1305::{ + AeadInPlace, XChaCha20Poly1305, XNonce, + aead::KeyInit, +}; use diesel::{ExpressionMethods as _, OptionalExtension as _, QueryDsl, dsl::update}; use diesel_async::{AsyncConnection, RunQueryDsl}; use ed25519_dalek::VerifyingKey; -use kameo::{ - Actor, - actor::{ActorRef, Spawn}, - messages, - prelude::Context, -}; +use kameo::{Actor, actor::ActorRef, messages}; +use memsafe::MemSafe; use tokio::sync::mpsc::Sender; use tonic::Status; use tracing::{error, info}; +use x25519_dalek::{EphemeralSecret, PublicKey}; use crate::{ ServerContext, - actors::bootstrap::{BootstrapActor, ConsumeToken}, + actors::{ + bootstrap::{BootstrapActor, ConsumeToken}, + user_agent::state::{ + AuthRequestContext, ChallengeContext, DummyContext, UnsealContext, UserAgentEvents, + UserAgentStateMachine, UserAgentStates, + }, + }, db::{self, schema}, errors::GrpcStatusExt, }; -/// Context for state machine with validated key and sent challenge -/// Challenge is then transformed to bytes using shared function and verified -#[derive(Clone, Debug)] -pub struct ChallengeContext { - challenge: AuthChallenge, - key: VerifyingKey, -} +mod state; +#[cfg(test)] +mod tests; -// Request context with deserialized public key for state machine. -// This intermediate struct is needed because the state machine branches depending on presence of bootstrap token, -// but we want to have the deserialized key in both branches. -#[derive(Clone, Debug)] -pub struct AuthRequestContext { - pubkey: VerifyingKey, - bootstrap_token: Option, -} - -smlang::statemachine!( - name: UserAgent, - derive_states: [Debug], - custom_error: false, - transitions: { - *Init + AuthRequest(AuthRequestContext) / auth_request_context = ReceivedAuthRequest(AuthRequestContext), - ReceivedAuthRequest(AuthRequestContext) + ReceivedBootstrapToken = Idle, - - ReceivedAuthRequest(AuthRequestContext) + SentChallenge(ChallengeContext) / move_challenge = WaitingForChallengeSolution(ChallengeContext), - - WaitingForChallengeSolution(ChallengeContext) + ReceivedGoodSolution = Idle, - WaitingForChallengeSolution(ChallengeContext) + ReceivedBadSolution = AuthError, // block further transitions, but connection should close anyway - - Idle + UnsealRequest / generate_temp_keypair = UnsealStarted(ed25519_dalek::SigningKey), - UnsealStarted(ed25519_dalek::SigningKey) + SentTempKeypair / move_keypair = WaitingForUnsealKey(ed25519_dalek::SigningKey), - } -); - -pub struct DummyContext; -impl UserAgentStateMachineContext for DummyContext { - #[allow(missing_docs)] - #[allow(clippy::unused_unit)] - fn move_challenge( - &mut self, - _state_data: &AuthRequestContext, - event_data: ChallengeContext, - ) -> Result { - Ok(event_data) - } - - #[allow(missing_docs)] - #[allow(clippy::unused_unit)] - fn auth_request_context( - &mut self, - event_data: AuthRequestContext, - ) -> Result { - Ok(event_data) - } - - #[allow(missing_docs)] - #[allow(clippy::unused_unit)] - fn move_keypair( - &mut self, - state_data: &ed25519_dalek::SigningKey, - ) -> Result { - Ok(state_data.clone()) - } - - #[allow(missing_docs)] - #[allow(clippy::unused_unit)] - fn generate_temp_keypair(&mut self) -> Result { - Ok(ed25519_dalek::SigningKey::generate(&mut rand::rng())) - } -} +mod transport; +pub(crate) use transport::handle_user_agent; #[derive(Actor)] pub struct UserAgentActor { @@ -272,18 +218,93 @@ fn auth_response(payload: ServerAuthPayload) -> UserAgentResponse { } } +fn unseal_response(payload: UserAgentResponsePayload) -> UserAgentResponse { + UserAgentResponse { + payload: Some(payload), + } +} + #[messages] impl UserAgentActor { - #[message(ctx)] - pub async fn handle_auth_challenge_request( - &mut self, - req: AuthChallengeRequest, - ctx: &mut Context, - ) -> Output { + #[message] + pub async fn handle_unseal_request(&mut self, req: UnsealStart) -> Output { + let secret = EphemeralSecret::random(); + let public_key = PublicKey::from(&secret); + + let client_pubkey_bytes: [u8; 32] = req + .client_pubkey + .try_into() + .map_err(|_| Status::invalid_argument("client_pubkey must be 32 bytes"))?; + + let client_public_key = PublicKey::from(client_pubkey_bytes); + + self.transition(UserAgentEvents::UnsealRequest(UnsealContext { + server_public_key: public_key, + secret: Mutex::new(Some(secret)), + client_public_key, + }))?; + + Ok(unseal_response( + UserAgentResponsePayload::UnsealStartResponse(UnsealStartResponse { + server_pubkey: public_key.as_bytes().to_vec(), + }), + )) + } + + #[message] + pub async fn handle_unseal_encrypted_key(&mut self, req: UnsealEncryptedKey) -> Output { + let UserAgentStates::WaitingForUnsealKey(unseal_context) = self.state.state() else { + error!("Received unseal encrypted key in invalid state"); + return Err(Status::failed_precondition( + "Invalid state for unseal encrypted key", + )); + }; + let ephemeral_secret = { + let mut secret_lock = unseal_context.secret.lock().unwrap(); + let secret = secret_lock.take(); + match secret { + Some(secret) => secret, + None => { + drop(secret_lock); + error!("Ephemeral secret already taken"); + self.transition(UserAgentEvents::ReceivedInvalidKey)?; + return Ok(unseal_response(UserAgentResponsePayload::UnsealResult( + UnsealResult::InvalidKey.into(), + ))); + } + } + }; + + let nonce = XNonce::from_slice(&req.nonce); + + let shared_secret = ephemeral_secret.diffie_hellman(&unseal_context.client_public_key); + let cipher = XChaCha20Poly1305::new(shared_secret.as_bytes().into()); + + let mut root_key_buffer = MemSafe::new(req.ciphertext.clone()).unwrap(); + let mut write_handle = root_key_buffer.write().unwrap(); + let write_handle = write_handle.deref_mut(); + + let decryption_result = cipher + .decrypt_in_place(nonce, &req.associated_data, write_handle); + + match decryption_result { + Ok(_) => todo!("Send key to the keyguarding"), + Err(err) => { + error!(?err, "Failed to decrypt unseal key"); + self.transition(UserAgentEvents::ReceivedInvalidKey)?; + return Ok(unseal_response(UserAgentResponsePayload::UnsealResult( + UnsealResult::InvalidKey.into(), + ))); + }, + } + } + + #[message] + pub async fn handle_auth_challenge_request(&mut self, req: AuthChallengeRequest) -> Output { let pubkey = req.pubkey.as_array().ok_or(Status::invalid_argument( "Expected pubkey to have specific length", ))?; - let pubkey = VerifyingKey::from_bytes(pubkey).map_err(|err| { + let pubkey = VerifyingKey::from_bytes(pubkey).map_err(|_err| { error!(?pubkey, "Failed to convert to VerifyingKey"); Status::invalid_argument("Failed to convert pubkey to VerifyingKey") })?; @@ -299,11 +320,10 @@ impl UserAgentActor { } } - #[message(ctx)] + #[message] pub async fn handle_auth_challenge_solution( &mut self, solution: auth::AuthChallengeSolution, - ctx: &mut Context, ) -> Output { let (valid, challenge_context) = self.verify_challenge_solution(&solution)?; @@ -321,211 +341,3 @@ impl UserAgentActor { } } } - -#[cfg(test)] -mod tests { - use arbiter_proto::proto::{ - UserAgentResponse, - auth::{self, AuthChallengeRequest, AuthOk}, - user_agent_response::Payload as UserAgentResponsePayload, - }; - use chrono::format; - use diesel::{ExpressionMethods as _, QueryDsl, insert_into}; - use diesel_async::RunQueryDsl; - use ed25519_dalek::Signer as _; - use kameo::actor::Spawn; - - use crate::{ - actors::{ - bootstrap::BootstrapActor, - user_agent::{HandleAuthChallengeRequest, HandleAuthChallengeSolution}, - }, - db::{self, schema}, - }; - - use super::UserAgentActor; - - #[tokio::test] - #[test_log::test] - pub async fn test_bootstrap_token_auth() { - let db = db::create_test_pool().await; - // explicitly not installing any user_agent pubkeys - let bootstrapper = BootstrapActor::new(&db).await.unwrap(); // this will create bootstrap token - let token = bootstrapper.get_token().unwrap(); - - let bootstrapper_ref = BootstrapActor::spawn(bootstrapper); - let user_agent = UserAgentActor::new_manual( - db.clone(), - bootstrapper_ref, - tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test - ); - let user_agent_ref = UserAgentActor::spawn(user_agent); - - // simulate client sending auth request with bootstrap token - let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); - let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); - - let result = user_agent_ref - .ask(HandleAuthChallengeRequest { - req: AuthChallengeRequest { - pubkey: pubkey_bytes, - bootstrap_token: Some(token), - }, - }) - .await - .expect("Shouldn't fail to send message"); - - // auth succeeded - assert_eq!( - result, - UserAgentResponse { - payload: Some(UserAgentResponsePayload::AuthMessage( - arbiter_proto::proto::auth::ServerMessage { - payload: Some(arbiter_proto::proto::auth::server_message::Payload::AuthOk( - AuthOk {}, - )), - }, - )), - } - ); - - // key is succesfully recorded in database - let mut conn = db.get().await.unwrap(); - let stored_pubkey: Vec = schema::useragent_client::table - .select(schema::useragent_client::public_key) - .first::>(&mut conn) - .await - .unwrap(); - assert_eq!(stored_pubkey, new_key.verifying_key().to_bytes().to_vec()); - } - - #[tokio::test] - #[test_log::test] - pub async fn test_bootstrap_invalid_token_auth() { - let db = db::create_test_pool().await; - // explicitly not installing any user_agent pubkeys - let bootstrapper = BootstrapActor::new(&db).await.unwrap(); // this will create bootstrap token - - let bootstrapper_ref = BootstrapActor::spawn(bootstrapper); - let user_agent = UserAgentActor::new_manual( - db.clone(), - bootstrapper_ref, - tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test - ); - let user_agent_ref = UserAgentActor::spawn(user_agent); - - // simulate client sending auth request with bootstrap token - let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); - let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); - - let result = user_agent_ref - .ask(HandleAuthChallengeRequest { - req: AuthChallengeRequest { - pubkey: pubkey_bytes, - bootstrap_token: Some("invalid_token".to_string()), - }, - }) - .await; - - match result { - Err(kameo::error::SendError::HandlerError(status)) => { - assert_eq!(status.code(), tonic::Code::InvalidArgument); - insta::assert_debug_snapshot!(status, @r#" - Status { - code: InvalidArgument, - message: "Invalid bootstrap token", - source: None, - } - "#); - } - Err(other) => { - panic!("Expected SendError::HandlerError, got {other:?}"); - } - Ok(_) => { - panic!("Expected error due to invalid bootstrap token, but got success"); - } - } - } - - #[tokio::test] - #[test_log::test] - pub async fn test_challenge_auth() { - let db = db::create_test_pool().await; - - let bootstrapper_ref = BootstrapActor::spawn(BootstrapActor::new(&db).await.unwrap()); - let user_agent = UserAgentActor::new_manual( - db.clone(), - bootstrapper_ref, - tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test - ); - let user_agent_ref = UserAgentActor::spawn(user_agent); - - // simulate client sending auth request with bootstrap token - let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); - let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); - - // insert pubkey into database to trigger challenge-response auth flow - { - let mut conn = db.get().await.unwrap(); - insert_into(schema::useragent_client::table) - .values(schema::useragent_client::public_key.eq(pubkey_bytes.clone())) - .execute(&mut conn) - .await - .unwrap(); - } - - let result = user_agent_ref - .ask(HandleAuthChallengeRequest { - req: AuthChallengeRequest { - pubkey: pubkey_bytes, - bootstrap_token: None, - }, - }) - .await - .expect("Shouldn't fail to send message"); - - // auth challenge succeeded - let UserAgentResponse { - payload: - Some(UserAgentResponsePayload::AuthMessage(arbiter_proto::proto::auth::ServerMessage { - payload: - Some(arbiter_proto::proto::auth::server_message::Payload::AuthChallenge( - challenge, - )), - })), - } = result - else { - panic!("Expected auth challenge response, got {result:?}"); - }; - - let formatted_challenge = arbiter_proto::format_challenge(&challenge); - let signature = new_key.sign(&formatted_challenge); - let serialized_signature = signature.to_bytes().to_vec(); - - let result = user_agent_ref - .ask(HandleAuthChallengeSolution { - solution: auth::AuthChallengeSolution { - signature: serialized_signature, - }, - }) - .await - .expect("Shouldn't fail to send message"); - - // auth succeeded - assert_eq!( - result, - UserAgentResponse { - payload: Some(UserAgentResponsePayload::AuthMessage( - arbiter_proto::proto::auth::ServerMessage { - payload: Some(arbiter_proto::proto::auth::server_message::Payload::AuthOk( - AuthOk {}, - )), - }, - )), - } - ); - } -} - -mod transport; -pub(crate) use transport::handle_user_agent; diff --git a/server/crates/arbiter-server/src/actors/user_agent/state.rs b/server/crates/arbiter-server/src/actors/user_agent/state.rs new file mode 100644 index 0000000..866fa6e --- /dev/null +++ b/server/crates/arbiter-server/src/actors/user_agent/state.rs @@ -0,0 +1,76 @@ +use std::sync::Mutex; + +use arbiter_proto::proto::auth::AuthChallenge; +use ed25519_dalek::VerifyingKey; +use x25519_dalek::{EphemeralSecret, PublicKey}; + +/// Context for state machine with validated key and sent challenge +/// Challenge is then transformed to bytes using shared function and verified +#[derive(Clone, Debug)] +pub struct ChallengeContext { + pub challenge: AuthChallenge, + pub key: VerifyingKey, +} + +// Request context with deserialized public key for state machine. +// This intermediate struct is needed because the state machine branches depending on presence of bootstrap token, +// but we want to have the deserialized key in both branches. +#[derive(Clone, Debug)] +pub struct AuthRequestContext { + pub pubkey: VerifyingKey, + pub bootstrap_token: Option, +} + +pub struct UnsealContext { + pub server_public_key: PublicKey, + pub client_public_key: PublicKey, + pub secret: Mutex>, +} + + + +smlang::statemachine!( + name: UserAgent, + custom_error: false, + transitions: { + *Init + AuthRequest(AuthRequestContext) / auth_request_context = ReceivedAuthRequest(AuthRequestContext), + ReceivedAuthRequest(AuthRequestContext) + ReceivedBootstrapToken = Idle, + + ReceivedAuthRequest(AuthRequestContext) + SentChallenge(ChallengeContext) / move_challenge = WaitingForChallengeSolution(ChallengeContext), + + WaitingForChallengeSolution(ChallengeContext) + ReceivedGoodSolution = Idle, + WaitingForChallengeSolution(ChallengeContext) + ReceivedBadSolution = AuthError, // block further transitions, but connection should close anyway + + Idle + UnsealRequest(UnsealContext) / generate_temp_keypair = WaitingForUnsealKey(UnsealContext), + WaitingForUnsealKey(UnsealContext) + ReceivedValidKey = Unsealed, + WaitingForUnsealKey(UnsealContext) + ReceivedInvalidKey = Idle, + } +); + +pub struct DummyContext; +impl UserAgentStateMachineContext for DummyContext { + #[allow(missing_docs)] + #[allow(clippy::unused_unit)] + fn move_challenge( + &mut self, + _state_data: &AuthRequestContext, + event_data: ChallengeContext, + ) -> Result { + Ok(event_data) + } + + #[allow(missing_docs)] + #[allow(clippy::unused_unit)] + fn auth_request_context( + &mut self, + event_data: AuthRequestContext, + ) -> Result { + Ok(event_data) + } + + #[allow(missing_docs)] + #[allow(clippy::unused_unit)] + fn generate_temp_keypair(&mut self, event_data: UnsealContext) -> Result { + Ok(event_data) + } +} diff --git a/server/crates/arbiter-server/src/actors/user_agent/tests.rs b/server/crates/arbiter-server/src/actors/user_agent/tests.rs new file mode 100644 index 0000000..5b4adca --- /dev/null +++ b/server/crates/arbiter-server/src/actors/user_agent/tests.rs @@ -0,0 +1,199 @@ +use arbiter_proto::proto::{ + UserAgentResponse, + auth::{self, AuthChallengeRequest, AuthOk}, + user_agent_response::Payload as UserAgentResponsePayload, +}; +use chrono::format; +use diesel::{ExpressionMethods as _, QueryDsl, insert_into}; +use diesel_async::RunQueryDsl; +use ed25519_dalek::Signer as _; +use kameo::actor::Spawn; + +use crate::{ + actors::{ + bootstrap::BootstrapActor, + user_agent::{HandleAuthChallengeRequest, HandleAuthChallengeSolution}, + }, + db::{self, schema}, +}; + +use super::UserAgentActor; + +#[tokio::test] +#[test_log::test] +pub async fn test_bootstrap_token_auth() { + let db = db::create_test_pool().await; + // explicitly not installing any user_agent pubkeys + let bootstrapper = BootstrapActor::new(&db).await.unwrap(); // this will create bootstrap token + let token = bootstrapper.get_token().unwrap(); + + let bootstrapper_ref = BootstrapActor::spawn(bootstrapper); + let user_agent = UserAgentActor::new_manual( + db.clone(), + bootstrapper_ref, + tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test + ); + let user_agent_ref = UserAgentActor::spawn(user_agent); + + // simulate client sending auth request with bootstrap token + let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); + + let result = user_agent_ref + .ask(HandleAuthChallengeRequest { + req: AuthChallengeRequest { + pubkey: pubkey_bytes, + bootstrap_token: Some(token), + }, + }) + .await + .expect("Shouldn't fail to send message"); + + // auth succeeded + assert_eq!( + result, + UserAgentResponse { + payload: Some(UserAgentResponsePayload::AuthMessage( + arbiter_proto::proto::auth::ServerMessage { + payload: Some(arbiter_proto::proto::auth::server_message::Payload::AuthOk( + AuthOk {}, + )), + }, + )), + } + ); + + // key is succesfully recorded in database + let mut conn = db.get().await.unwrap(); + let stored_pubkey: Vec = schema::useragent_client::table + .select(schema::useragent_client::public_key) + .first::>(&mut conn) + .await + .unwrap(); + assert_eq!(stored_pubkey, new_key.verifying_key().to_bytes().to_vec()); +} + +#[tokio::test] +#[test_log::test] +pub async fn test_bootstrap_invalid_token_auth() { + let db = db::create_test_pool().await; + // explicitly not installing any user_agent pubkeys + let bootstrapper = BootstrapActor::new(&db).await.unwrap(); // this will create bootstrap token + + let bootstrapper_ref = BootstrapActor::spawn(bootstrapper); + let user_agent = UserAgentActor::new_manual( + db.clone(), + bootstrapper_ref, + tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test + ); + let user_agent_ref = UserAgentActor::spawn(user_agent); + + // simulate client sending auth request with bootstrap token + let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); + + let result = user_agent_ref + .ask(HandleAuthChallengeRequest { + req: AuthChallengeRequest { + pubkey: pubkey_bytes, + bootstrap_token: Some("invalid_token".to_string()), + }, + }) + .await; + + match result { + Err(kameo::error::SendError::HandlerError(status)) => { + assert_eq!(status.code(), tonic::Code::InvalidArgument); + insta::assert_debug_snapshot!(status, @r#" + Status { + code: InvalidArgument, + message: "Invalid bootstrap token", + source: None, + } + "#); + } + Err(other) => { + panic!("Expected SendError::HandlerError, got {other:?}"); + } + Ok(_) => { + panic!("Expected error due to invalid bootstrap token, but got success"); + } + } +} + +#[tokio::test] +#[test_log::test] +pub async fn test_challenge_auth() { + let db = db::create_test_pool().await; + + let bootstrapper_ref = BootstrapActor::spawn(BootstrapActor::new(&db).await.unwrap()); + let user_agent = UserAgentActor::new_manual( + db.clone(), + bootstrapper_ref, + tokio::sync::mpsc::channel(1).0, // dummy channel, we won't actually send responses in this test + ); + let user_agent_ref = UserAgentActor::spawn(user_agent); + + // simulate client sending auth request with bootstrap token + let new_key = ed25519_dalek::SigningKey::generate(&mut rand::rng()); + let pubkey_bytes = new_key.verifying_key().to_bytes().to_vec(); + + // insert pubkey into database to trigger challenge-response auth flow + { + let mut conn = db.get().await.unwrap(); + insert_into(schema::useragent_client::table) + .values(schema::useragent_client::public_key.eq(pubkey_bytes.clone())) + .execute(&mut conn) + .await + .unwrap(); + } + + let result = user_agent_ref + .ask(HandleAuthChallengeRequest { + req: AuthChallengeRequest { + pubkey: pubkey_bytes, + bootstrap_token: None, + }, + }) + .await + .expect("Shouldn't fail to send message"); + + // auth challenge succeeded + let UserAgentResponse { + payload: + Some(UserAgentResponsePayload::AuthMessage(arbiter_proto::proto::auth::ServerMessage { + payload: + Some(arbiter_proto::proto::auth::server_message::Payload::AuthChallenge(challenge)), + })), + } = result + else { + panic!("Expected auth challenge response, got {result:?}"); + }; + + let formatted_challenge = arbiter_proto::format_challenge(&challenge); + let signature = new_key.sign(&formatted_challenge); + let serialized_signature = signature.to_bytes().to_vec(); + + let result = user_agent_ref + .ask(HandleAuthChallengeSolution { + solution: auth::AuthChallengeSolution { + signature: serialized_signature, + }, + }) + .await + .expect("Shouldn't fail to send message"); + + // auth succeeded + assert_eq!( + result, + UserAgentResponse { + payload: Some(UserAgentResponsePayload::AuthMessage( + arbiter_proto::proto::auth::ServerMessage { + payload: Some(arbiter_proto::proto::auth::server_message::Payload::AuthOk( + AuthOk {}, + )), + }, + )), + } + ); +} diff --git a/server/crates/arbiter-server/src/actors/user_agent/transport.rs b/server/crates/arbiter-server/src/actors/user_agent/transport.rs index 2114d56..bf54094 100644 --- a/server/crates/arbiter-server/src/actors/user_agent/transport.rs +++ b/server/crates/arbiter-server/src/actors/user_agent/transport.rs @@ -2,12 +2,9 @@ use super::UserAgentActor; use arbiter_proto::proto::{ UserAgentRequest, UserAgentResponse, auth::{ - self, AuthChallenge, AuthChallengeRequest, AuthOk, ClientMessage, - ServerMessage as AuthServerMessage, client_message::Payload as ClientAuthPayload, - server_message::Payload as ServerAuthPayload, + ClientMessage as ClientAuthMessage, client_message::Payload as ClientAuthPayload, }, user_agent_request::Payload as UserAgentRequestPayload, - user_agent_response::Payload as UserAgentResponsePayload, }; use futures::StreamExt; use kameo::{ @@ -19,7 +16,10 @@ use tonic::Status; use tracing::error; use crate::{ - actors::user_agent::{HandleAuthChallengeRequest, HandleAuthChallengeSolution}, + actors::user_agent::{ + HandleAuthChallengeRequest, HandleAuthChallengeSolution, HandleUnsealEncryptedKey, + HandleUnsealRequest, + }, context::ServerContext, }; @@ -59,28 +59,30 @@ async fn process_message( Status::invalid_argument("Expected message with payload") })?; - let UserAgentRequestPayload::AuthMessage(ClientMessage { - payload: Some(client_message), - }) = msg - else { - error!( - actor = "useragent", - "Received unexpected message type during authentication" - ); - return Err(Status::invalid_argument( - "Expected AuthMessage with ClientMessage payload", - )); - }; - - match client_message { - ClientAuthPayload::AuthChallengeRequest(req) => actor + match msg { + UserAgentRequestPayload::AuthMessage(ClientAuthMessage { + payload: Some(ClientAuthPayload::AuthChallengeRequest(req)), + }) => actor .ask(HandleAuthChallengeRequest { req }) .await .map_err(into_status), - ClientAuthPayload::AuthChallengeSolution(solution) => actor + UserAgentRequestPayload::AuthMessage(ClientAuthMessage { + payload: Some(ClientAuthPayload::AuthChallengeSolution(solution)), + }) => actor .ask(HandleAuthChallengeSolution { solution }) .await .map_err(into_status), + UserAgentRequestPayload::UnsealStart(unseal_start) => actor + .ask(HandleUnsealRequest { req: unseal_start }) + .await + .map_err(into_status), + UserAgentRequestPayload::UnsealEncryptedKey(unseal_encrypted_key) => actor + .ask(HandleUnsealEncryptedKey { + req: unseal_encrypted_key, + }) + .await + .map_err(into_status), + _ => Err(Status::invalid_argument("Expected message with payload")), } } diff --git a/server/crates/arbiter-server/src/context.rs b/server/crates/arbiter-server/src/context.rs index fe1d15a..0afffd2 100644 --- a/server/crates/arbiter-server/src/context.rs +++ b/server/crates/arbiter-server/src/context.rs @@ -2,7 +2,6 @@ use std::sync::Arc; use diesel::OptionalExtension as _; use diesel_async::RunQueryDsl as _; -use ed25519_dalek::VerifyingKey; use kameo::actor::{ActorRef, Spawn}; use miette::Diagnostic; use rand::rngs::StdRng; @@ -14,7 +13,7 @@ use crate::{ actors::bootstrap::{self, BootstrapActor}, context::tls::{TlsDataRaw, TlsManager}, db::{ self, models::ArbiterSetting, - schema::{self, arbiter_settings}, + schema::arbiter_settings, } }; diff --git a/server/crates/arbiter-server/src/db.rs b/server/crates/arbiter-server/src/db.rs index c44d489..16567c6 100644 --- a/server/crates/arbiter-server/src/db.rs +++ b/server/crates/arbiter-server/src/db.rs @@ -1,12 +1,11 @@ -use std::sync::Arc; use diesel::{ Connection as _, SqliteConnection, - connection::{SimpleConnection as _, TransactionManager}, + connection::SimpleConnection as _, }; use diesel_async::{ AsyncConnection, SimpleAsyncConnection, - pooled_connection::{AsyncDieselConnectionManager, ManagerConfig, RecyclingMethod}, + pooled_connection::{AsyncDieselConnectionManager, ManagerConfig}, sync_connection_wrapper::SyncConnectionWrapper, }; use diesel_migrations::{EmbeddedMigrations, MigrationHarness, embed_migrations}; diff --git a/server/crates/arbiter-server/src/db/models.rs b/server/crates/arbiter-server/src/db/models.rs index 7417875..4d6a503 100644 --- a/server/crates/arbiter-server/src/db/models.rs +++ b/server/crates/arbiter-server/src/db/models.rs @@ -1,29 +1,55 @@ #![allow(unused)] #![allow(clippy::all)] -use crate::db::schema::{self, aead_encrypted, arbiter_settings}; +use crate::db::schema::{self, aead_encrypted, arbiter_settings, root_key_history}; use diesel::{prelude::*, sqlite::Sqlite}; +use restructed::Models; pub mod types { use chrono::{DateTime, Utc}; pub struct SqliteTimestamp(DateTime); } -#[derive(Queryable, Debug, Insertable)] +#[derive(Models, Queryable, Debug, Insertable, Selectable)] +#[view( + NewAeadEncrypted, + derive(Insertable), + omit(id), + attributes_with = "deriveless" +)] #[diesel(table_name = aead_encrypted, check_for_backend(Sqlite))] pub struct AeadEncrypted { pub id: i32, pub ciphertext: Vec, pub tag: Vec, - pub current_nonce: i32, + pub current_nonce: Vec, pub schema_version: i32, + pub created_at: i32, +} + +#[derive(Models, Queryable, Debug, Insertable, Selectable)] +#[diesel(table_name = root_key_history, check_for_backend(Sqlite))] +#[view( + NewRootKeyHistory, + derive(Insertable), + omit(id), + attributes_with = "deriveless" +)] +pub struct RootKeyHistory { + pub id: i32, + pub ciphertext: Vec, + pub tag: Vec, + pub root_key_encryption_nonce: Vec, + pub data_encryption_nonce: Vec, + pub schema_version: i32, + pub salt: Vec, } #[derive(Queryable, Debug, Insertable)] #[diesel(table_name = arbiter_settings, check_for_backend(Sqlite))] pub struct ArbiterSetting { pub id: i32, - pub root_key_id: Option, // references aead_encrypted.id + pub root_key_id: Option, // references root_key_history.id pub cert_key: Vec, pub cert: Vec, } diff --git a/server/crates/arbiter-server/src/db/schema.rs b/server/crates/arbiter-server/src/db/schema.rs index 38f8afe..a1d7e6e 100644 --- a/server/crates/arbiter-server/src/db/schema.rs +++ b/server/crates/arbiter-server/src/db/schema.rs @@ -3,10 +3,11 @@ diesel::table! { aead_encrypted (id) { id -> Integer, - current_nonce -> Integer, + current_nonce -> Binary, ciphertext -> Binary, tag -> Binary, schema_version -> Integer, + created_at -> Integer, } } @@ -29,6 +30,18 @@ diesel::table! { } } +diesel::table! { + root_key_history (id) { + id -> Integer, + root_key_encryption_nonce -> Binary, + data_encryption_nonce -> Binary, + ciphertext -> Binary, + tag -> Binary, + schema_version -> Integer, + salt -> Binary, + } +} + diesel::table! { useragent_client (id) { id -> Integer, @@ -39,11 +52,12 @@ diesel::table! { } } -diesel::joinable!(arbiter_settings -> aead_encrypted (root_key_id)); +diesel::joinable!(arbiter_settings -> root_key_history (root_key_id)); diesel::allow_tables_to_appear_in_same_query!( aead_encrypted, arbiter_settings, program_client, + root_key_history, useragent_client, );