diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 8a07a3a2b8..ed0ff8ffea 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -1,8 +1,8 @@ name: Checks on: - # workflow_call: - pull_request: + workflow_call: + # pull_request: env: RUST_TEST_TIME_INTEGRATION: "120000,300000" diff --git a/Cargo.lock b/Cargo.lock index f172943f2a..8a6c123c56 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,9 +45,9 @@ dependencies = [ [[package]] name = "age" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fc171f4874fa10887e47088f81a55fcf030cd421aa31ec2b370cafebcc608a" +checksum = "bf640be7658959746f1f0f2faab798f6098a9436a8e18e148d18bc9875e13c4b" dependencies = [ "age-core", "base64 0.21.7", @@ -262,9 +262,9 @@ dependencies = [ [[package]] name = "asn1-rs" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" dependencies = [ "asn1-rs-derive", "asn1-rs-impl", @@ -272,19 +272,19 @@ dependencies = [ "nom", "num-traits", "rusticata-macros", - "thiserror 1.0.69", + "thiserror 2.0.17", "time", ] [[package]] name = "asn1-rs-derive" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", "synstructure", ] @@ -296,14 +296,14 @@ checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] name = "async-compression" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93c1f86859c1af3d514fa19e8323147ff10ea98684e6c7b307912509f50e67b2" +checksum = "0e86f6d3dc9dc4352edeea6b8e499e13e3f5dc3b964d7ca5fd411415a3498473" dependencies = [ "compression-codecs", "compression-core", @@ -335,7 +335,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -358,7 +358,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -685,9 +685,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.46" +version = "1.2.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" dependencies = [ "find-msvc-tools", "shlex", @@ -741,7 +741,7 @@ checksum = "45565fc9416b9896014f5732ac776f810ee53a66730c17e4020c3ec064a8f88f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -807,9 +807,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8" dependencies = [ "clap_builder", "clap_derive", @@ -817,9 +817,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00" dependencies = [ "anstream", "anstyle", @@ -837,7 +837,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -898,9 +898,9 @@ checksum = "55b672471b4e9f9e95499ea597ff64941a309b2cdbffcc46f2cc5e2d971fd335" [[package]] name = "compression-codecs" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680dc087785c5230f8e8843e2e57ac7c1c90488b6a91b88caa265410568f441b" +checksum = "302266479cb963552d11bd042013a58ef1adc56768016c8b82b4199488f2d4ad" dependencies = [ "compression-core", "flate2", @@ -908,9 +908,9 @@ dependencies = [ [[package]] name = "compression-core" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a9b614a5787ef0c8802a55766480563cb3a93b435898c422ed2a359cf811582" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" [[package]] name = "conpty" @@ -1136,7 +1136,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1194,7 +1194,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1208,7 +1208,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1230,7 +1230,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" dependencies = [ "darling_core 0.20.11", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1241,7 +1241,7 @@ checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" dependencies = [ "darling_core 0.21.3", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1283,9 +1283,9 @@ dependencies = [ [[package]] name = "der-parser" -version = "9.0.0" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ "asn1-rs", "displaydoc", @@ -1344,7 +1344,7 @@ dependencies = [ "darling 0.20.11", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1364,7 +1364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" dependencies = [ "derive_builder_core 0.20.2", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1405,7 +1405,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1507,7 +1507,7 @@ checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -1803,7 +1803,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -2009,7 +2009,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.12.0", + "indexmap 2.12.1", "slab", "tokio", "tokio-util", @@ -2043,9 +2043,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "hashlink" @@ -2130,12 +2130,11 @@ dependencies = [ [[package]] name = "http" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" dependencies = [ "bytes", - "fnv", "itoa", ] @@ -2293,7 +2292,7 @@ dependencies = [ "proc-macro2", "quote", "strsim 0.11.1", - "syn 2.0.110", + "syn 2.0.111", "unic-langid", ] @@ -2307,7 +2306,7 @@ dependencies = [ "i18n-config", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -2468,12 +2467,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "hashbrown 0.16.1", "serde", "serde_core", ] @@ -2654,9 +2653,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b011eec8cc36da2aab2d5cff675ec18454fad408585853910a202391cf9f8e65" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", @@ -2694,7 +2693,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "affc90b6daa52dc928055e8f0d5611ee9d15073469f3d2f682498b092747503b" dependencies = [ "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -2871,7 +2870,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -2887,7 +2886,7 @@ dependencies = [ "quote", "regex-syntax", "rustc_version", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -2958,9 +2957,9 @@ dependencies = [ [[package]] name = "metrics" -version = "0.24.2" +version = "0.24.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25dea7ac8057892855ec285c440160265225438c3c45072613c25a4b26e98ef5" +checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" dependencies = [ "ahash", "portable-atomic", @@ -2973,7 +2972,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" dependencies = [ "base64 0.22.1", - "indexmap 2.12.0", + "indexmap 2.12.1", "metrics", "metrics-util", "quanta", @@ -3042,9 +3041,9 @@ dependencies = [ [[package]] name = "moxcms" -version = "0.7.9" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbdd3d7436f8b5e892b8b7ea114271ff0fa00bc5acae845d53b07d498616ef6" +checksum = "80986bbbcf925ebd3be54c26613d861255284584501595cf418320c078945608" dependencies = [ "num-traits", "pxfm", @@ -3319,9 +3318,9 @@ dependencies = [ [[package]] name = "oid-registry" -version = "0.7.1" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" dependencies = [ "asn1-rs", ] @@ -3346,9 +3345,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "open" -version = "5.3.2" +version = "5.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2483562e62ea94312f3576a7aca397306df7990b8d89033e18766744377ef95" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" dependencies = [ "is-wsl", "libc", @@ -3376,7 +3375,7 @@ dependencies = [ "proc-macro2", "proc-macro2-diagnostics", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -3467,7 +3466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" dependencies = [ "fixedbitset", - "indexmap 2.12.0", + "indexmap 2.12.1", ] [[package]] @@ -3525,7 +3524,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -3563,7 +3562,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", - "indexmap 2.12.0", + "indexmap 2.12.1", "quick-xml", "serde", "time", @@ -3677,7 +3676,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -3753,7 +3752,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -3773,7 +3772,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", "version_check", "yansi", ] @@ -3804,7 +3803,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.110", + "syn 2.0.111", "tempfile", ] @@ -3818,7 +3817,7 @@ dependencies = [ "itertools 0.14.0", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -3905,9 +3904,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +checksum = "b3502d6155304a4173a5f2c34b52b7ed0dd085890326cb50fd625fdf39e86b3b" dependencies = [ "num-traits", ] @@ -4118,14 +4117,15 @@ dependencies = [ [[package]] name = "rcgen" -version = "0.13.2" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +checksum = "5fae430c6b28f1ad601274e78b7dffa0546de0b73b4cd32f46723c0c2a16f7a5" dependencies = [ "pem", "ring", "rustls-pki-types", "time", + "x509-parser", "yasna", ] @@ -4161,7 +4161,7 @@ checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -4204,7 +4204,7 @@ dependencies = [ "quote", "refinery-core", "regex", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -4394,7 +4394,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.110", + "syn 2.0.111", "walkdir", ] @@ -4489,9 +4489,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a" +checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c" dependencies = [ "web-time", "zeroize", @@ -4544,7 +4544,7 @@ checksum = "5d66de233f908aebf9cc30ac75ef9103185b4b715c6f2fb7a626aa5e5ede53ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -4745,7 +4745,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -4808,7 +4808,7 @@ checksum = "aafbefbe175fa9bf03ca83ef89beecff7d2a95aaacd5732325b90ac8c3bd7b90" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -4845,15 +4845,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.16.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10574371d41b0d9b2cff89418eda27da52bcaff2cc8741db26382a77c29131f1" +checksum = "4fa237f2807440d238e0364a218270b98f767a00d3dada77b1c53ae88940e2e7" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.12.0", + "indexmap 2.12.1", "schemars 0.9.0", "schemars 1.1.0", "serde_core", @@ -4864,14 +4864,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.16.0" +version = "3.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08a72d8216842fdd57820dc78d840bef99248e35fb2554ff923319e60f2d686b" +checksum = "52a8e3ca0ca629121f70ab50f95249e5a6f925cc0f6ffe8256c45b728875706c" dependencies = [ "darling 0.21.3", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -4950,9 +4950,9 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] @@ -5097,12 +5097,12 @@ dependencies = [ name = "sos-account" version = "0.17.6" dependencies = [ + "age", "async-trait", "futures", "futures-util", "hex", - "indexmap 2.12.0", - "rustc_version", + "indexmap 2.12.1", "secrecy", "serde", "serde_json", @@ -5170,7 +5170,6 @@ dependencies = [ "binary-stream", "bitflags 2.10.0", "futures", - "rustc_version", "serde", "sos-core", "sos-vfs", @@ -5184,8 +5183,7 @@ dependencies = [ "async-trait", "binary-stream", "futures", - "indexmap 2.12.0", - "rustc_version", + "indexmap 2.12.1", "serde", "sos-archive", "sos-audit", @@ -5212,7 +5210,6 @@ version = "0.17.3" dependencies = [ "futures", "interprocess", - "rustc_version", "serde_json", "sos-core", "thiserror 2.0.17", @@ -5239,9 +5236,8 @@ dependencies = [ "binary-stream", "futures", "hex", - "indexmap 2.12.0", + "indexmap 2.12.1", "parking_lot", - "rustc_version", "secrecy", "serde", "sha2", @@ -5298,7 +5294,6 @@ dependencies = [ "pem", "rand 0.8.5", "rs_merkle", - "rustc_version", "secrecy", "serde", "serde_json", @@ -5316,15 +5311,14 @@ dependencies = [ [[package]] name = "sos-database" -version = "0.17.4" +version = "0.17.5" dependencies = [ "async-sqlite", "async-trait", "binary-stream", "futures", - "indexmap 2.12.0", + "indexmap 2.12.1", "refinery", - "rustc_version", "secrecy", "serde", "serde_json", @@ -5355,8 +5349,7 @@ dependencies = [ "async-trait", "binary-stream", "futures", - "indexmap 2.12.0", - "rustc_version", + "indexmap 2.12.1", "secrecy", "serde", "serde_json", @@ -5418,8 +5411,7 @@ dependencies = [ name = "sos-external-files" version = "0.17.1" dependencies = [ - "indexmap 2.12.0", - "rustc_version", + "indexmap 2.12.1", "sos-core", "sos-vault", "sos-vfs", @@ -5436,9 +5428,8 @@ dependencies = [ "futures", "futures-util", "hex", - "indexmap 2.12.0", + "indexmap 2.12.1", "parking_lot", - "rustc_version", "serde", "serde_json", "sha2", @@ -5473,7 +5464,7 @@ dependencies = [ "futures", "hex", "http", - "indexmap 2.12.0", + "indexmap 2.12.1", "maplit2", "parking_lot", "pretty_assertions", @@ -5526,8 +5517,7 @@ dependencies = [ "binary-stream", "futures", "hex", - "indexmap 2.12.0", - "rustc_version", + "indexmap 2.12.1", "sha2", "sos-backend", "sos-core", @@ -5560,7 +5550,6 @@ dependencies = [ "notify", "open", "parking_lot", - "rustc_version", "secrecy", "serde", "serde_json", @@ -5597,7 +5586,6 @@ dependencies = [ "async-trait", "ed25519-dalek", "futures", - "rustc_version", "secrecy", "serde", "sos-backend", @@ -5619,7 +5607,6 @@ name = "sos-logs" version = "0.17.2" dependencies = [ "rev_buf_reader", - "rustc_version", "sos-core", "thiserror 2.0.17", "time", @@ -5638,7 +5625,6 @@ dependencies = [ "futures", "hex", "keychain_parser", - "rustc_version", "secrecy", "security-framework 3.5.1", "serde", @@ -5662,17 +5648,17 @@ dependencies = [ name = "sos-net" version = "0.17.7" dependencies = [ + "age", "async-recursion", "async-trait", "binary-stream", "futures", "hex", "http", - "indexmap 2.12.0", + "indexmap 2.12.1", "prost", "rand 0.8.5", "rs_merkle", - "rustc_version", "secrecy", "serde", "serde_json", @@ -5714,7 +5700,6 @@ version = "0.17.1" dependencies = [ "chbs", "rand 0.8.5", - "rustc_version", "secrecy", "sos-core", "thiserror 2.0.17", @@ -5728,7 +5713,6 @@ dependencies = [ "http", "keyring", "robius-authentication", - "rustc_version", "secrecy", "security-framework 3.5.1", "thiserror 2.0.17", @@ -5740,7 +5724,6 @@ name = "sos-preferences" version = "0.17.5" dependencies = [ "async-trait", - "rustc_version", "serde", "serde_json", "sos-core", @@ -5752,6 +5735,7 @@ dependencies = [ name = "sos-protocol" version = "0.17.5" dependencies = [ + "age", "async-trait", "binary-stream", "bs58", @@ -5759,14 +5743,13 @@ dependencies = [ "futures", "hex", "http", - "indexmap 2.12.0", + "indexmap 2.12.1", "parking_lot", "prost", "prost-build", "protoc-bin-vendored", "reqwest", "rs_merkle", - "rustc_version", "rustls", "rustls-pemfile", "rustls-webpki", @@ -5797,8 +5780,7 @@ name = "sos-reducers" version = "0.17.1" dependencies = [ "futures", - "indexmap 2.12.0", - "rustc_version", + "indexmap 2.12.1", "sos-core", "sos-vault", "tracing", @@ -5809,8 +5791,7 @@ name = "sos-remote-sync" version = "0.17.1" dependencies = [ "async-trait", - "indexmap 2.12.0", - "rustc_version", + "indexmap 2.12.1", "sos-account", "sos-backend", "sos-core", @@ -5826,7 +5807,6 @@ dependencies = [ name = "sos-sdk" version = "0.17.2" dependencies = [ - "rustc_version", "sos-core", "sos-login", "sos-password", @@ -5840,7 +5820,6 @@ name = "sos-search" version = "0.17.2" dependencies = [ "probly-search", - "rustc_version", "serde", "sos-backend", "sos-core", @@ -5887,9 +5866,8 @@ dependencies = [ "futures", "hex", "http", - "indexmap 2.12.0", + "indexmap 2.12.1", "k256", - "rustc_version", "rustls", "serde", "serde_json", @@ -5930,8 +5908,7 @@ dependencies = [ "async-trait", "binary-stream", "futures", - "indexmap 2.12.0", - "rustc_version", + "indexmap 2.12.1", "secrecy", "serde", "sha2", @@ -5964,7 +5941,6 @@ dependencies = [ "hex", "k256", "rand 0.8.5", - "rustc_version", "serde", "sha2", "sha3", @@ -5978,8 +5954,7 @@ name = "sos-sync" version = "0.17.3" dependencies = [ "async-trait", - "indexmap 2.12.0", - "rustc_version", + "indexmap 2.12.1", "serde", "sos-backend", "sos-core", @@ -5995,7 +5970,6 @@ name = "sos-system-messages" version = "0.17.4" dependencies = [ "async-trait", - "rustc_version", "serde", "serde_with", "thiserror 2.0.17", @@ -6064,7 +6038,7 @@ dependencies = [ "bytes", "futures", "hex", - "indexmap 2.12.0", + "indexmap 2.12.1", "k256", "keychain_parser", "pem", @@ -6108,7 +6082,7 @@ dependencies = [ [[package]] name = "sos-vault" -version = "0.17.4" +version = "0.17.5" dependencies = [ "age", "async-trait", @@ -6117,9 +6091,8 @@ dependencies = [ "ed25519-dalek", "futures", "hex", - "indexmap 2.12.0", + "indexmap 2.12.1", "pem", - "rustc_version", "secrecy", "serde", "serde_json", @@ -6154,9 +6127,9 @@ dependencies = [ name = "sos-web" version = "0.17.2" dependencies = [ + "age", "async-trait", - "indexmap 2.12.0", - "rustc_version", + "indexmap 2.12.1", "secrecy", "sos-account", "sos-backend", @@ -6247,9 +6220,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.110" +version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", @@ -6273,7 +6246,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -6367,7 +6340,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -6378,7 +6351,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -6493,7 +6466,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -6508,9 +6481,9 @@ dependencies = [ [[package]] name = "tokio-rustls-acme" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3184e8e292a828dd4bca5b2a60aba830ec5ed873a66c9ebb6e65038fa649e827" +checksum = "cfdba5ab34e36bb015bb2cdfc13a3ee3473bcba240162f96301a3eacef2f769d" dependencies = [ "async-trait", "axum-server", @@ -6531,7 +6504,7 @@ dependencies = [ "time", "tokio", "tokio-rustls", - "webpki-roots 0.26.11", + "webpki-roots 1.0.4", "x509-parser", ] @@ -6612,7 +6585,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "toml_datetime", "winnow 0.5.40", ] @@ -6623,12 +6596,12 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "serde", "serde_spanned", "toml_datetime", "toml_write", - "winnow 0.7.13", + "winnow 0.7.14", ] [[package]] @@ -6673,9 +6646,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" dependencies = [ "bitflags 2.10.0", "bytes", @@ -6704,9 +6677,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "log", "pin-project-lite", @@ -6716,32 +6689,32 @@ dependencies = [ [[package]] name = "tracing-appender" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf" dependencies = [ "crossbeam-channel", - "thiserror 1.0.69", + "thiserror 2.0.17", "time", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", @@ -6770,9 +6743,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -6859,7 +6832,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a615d6c2764852a2e88a4f16e9ce1ea49bb776b5872956309e170d63a042a34f" dependencies = [ "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -6994,7 +6967,7 @@ version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fcc29c80c21c31608227e0912b2d7fddba57ad76b606890627ba8ee7964e993" dependencies = [ - "indexmap 2.12.0", + "indexmap 2.12.1", "serde", "serde_json", "utoipa-gen", @@ -7008,7 +6981,7 @@ checksum = "6d79d08d92ab8af4c5e8a6da20c47ae3f61a0f1dabc1997cdf2d082b757ca08b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", "uuid", ] @@ -7116,9 +7089,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da95793dfc411fbbd93f5be7715b0578ec61fe87cb1a42b12eb625caa5c5ea60" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if 1.0.4", "once_cell", @@ -7129,9 +7102,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.55" +version = "0.4.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" dependencies = [ "cfg-if 1.0.4", "js-sys", @@ -7142,9 +7115,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04264334509e04a7bf8690f2384ef5265f05143a4bff3889ab7a3269adab59c2" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -7152,22 +7125,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420bc339d9f322e562942d52e115d57e950d12d88983a14c79b86859ee6c7ebc" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.105" +version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f218a38c84bcb33c25ec7059b07847d465ce0e0a76b995e134a45adcb6af76" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] @@ -7187,9 +7160,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.82" +version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" dependencies = [ "js-sys", "wasm-bindgen", @@ -7312,7 +7285,7 @@ checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -7323,7 +7296,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -7334,7 +7307,7 @@ checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -7345,7 +7318,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -7666,9 +7639,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] @@ -7716,9 +7689,9 @@ dependencies = [ [[package]] name = "x509-parser" -version = "0.16.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +checksum = "eb3e137310115a65136898d2079f003ce33331a6c4b0d51f1531d1be082b6425" dependencies = [ "asn1-rs", "data-encoding", @@ -7726,8 +7699,9 @@ dependencies = [ "lazy_static", "nom", "oid-registry", + "ring", "rusticata-macros", - "thiserror 1.0.69", + "thiserror 2.0.17", "time", ] @@ -7736,7 +7710,6 @@ name = "xclipboard" version = "0.16.3" dependencies = [ "arboard", - "rustc_version", "thiserror 2.0.17", "tokio", "tracing", @@ -7783,28 +7756,28 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.27" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" +checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.27" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" +checksum = "cf955aa904d6040f70dc8e9384444cb1030aed272ba3cb09bbc4ab9e7c1f34f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -7824,7 +7797,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", "synstructure", ] @@ -7845,7 +7818,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] @@ -7879,7 +7852,7 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.110", + "syn 2.0.111", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 2ad9bd23ea..2e90bca2b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -211,7 +211,6 @@ vsss-rs = "3" getrandom = "0.2" # build -rustc_version = "0.4.1" prost-build = "0.14" protoc-bin-vendored = "3" diff --git a/Makefile.toml b/Makefile.toml index 5c85127875..dbf25a57cd 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -146,7 +146,7 @@ rm -rf target/integration-test [tasks.test] script_runner = "@shell" script = ''' -SOS_TEST_CLIENT_DB=1 cargo nextest run -p sos-integration-tests -p sos-unit-tests +cargo nextest run -p sos-integration-tests -p sos-unit-tests ''' dependencies = ["clean-tests", "build-test"] @@ -155,11 +155,11 @@ script_runner = "@shell" script = ''' cargo nextest run \ -p sos-integration-tests -p sos-unit-tests -SOS_TEST_CLIENT_DB=1 cargo nextest run \ +SOS_TEST_CLIENT_FS=1 cargo nextest run \ -p sos-integration-tests -p sos-unit-tests -SOS_TEST_SERVER_DB=1 cargo nextest run \ +SOS_TEST_SERVER_FS=1 cargo nextest run \ -p sos-integration-tests -p sos-unit-tests -SOS_TEST_CLIENT_DB=1 SOS_TEST_SERVER_DB=1 cargo nextest run \ +SOS_TEST_CLIENT_FS=1 SOS_TEST_SERVER_FS=1 cargo nextest run \ -p sos-integration-tests -p sos-unit-tests ''' dependencies = ["clean-tests", "build-test"] @@ -169,11 +169,11 @@ script_runner = "@shell" script = ''' cargo nextest run --profile ci \ -p sos-integration-tests -p sos-unit-tests -SOS_TEST_CLIENT_DB=1 cargo nextest run --profile ci \ +SOS_TEST_CLIENT_FS=1 cargo nextest run --profile ci \ -p sos-integration-tests -p sos-unit-tests -SOS_TEST_SERVER_DB=1 cargo nextest run --profile ci \ +SOS_TEST_SERVER_FS=1 cargo nextest run --profile ci \ -p sos-integration-tests -p sos-unit-tests -SOS_TEST_CLIENT_DB=1 SOS_TEST_SERVER_DB=1 cargo nextest run --profile ci \ +SOS_TEST_CLIENT_FS=1 SOS_TEST_SERVER_FS=1 cargo nextest run --profile ci \ -p sos-integration-tests -p sos-unit-tests ''' dependencies = ["clean-tests", "build-test"] @@ -184,11 +184,11 @@ script = ''' cargo llvm-cov clean --workspace cargo llvm-cov nextest \ --no-report -p sos-integration-tests -p sos-unit-tests -SOS_TEST_CLIENT_DB=1 cargo llvm-cov nextest \ +SOS_TEST_CLIENT_FS=1 cargo llvm-cov nextest \ --no-report -p sos-integration-tests -p sos-unit-tests -SOS_TEST_SERVER_DB=1 cargo llvm-cov nextest \ +SOS_TEST_SERVER_FS=1 cargo llvm-cov nextest \ --no-report -p sos-integration-tests -p sos-unit-tests -SOS_TEST_CLIENT_DB=1 SOS_TEST_SERVER_DB=1 cargo llvm-cov nextest \ +SOS_TEST_CLIENT_FS=1 SOS_TEST_SERVER_FS=1 cargo llvm-cov nextest \ --no-report -p sos-integration-tests -p sos-unit-tests cargo llvm-cov report --html ''' diff --git a/crates/account/Cargo.toml b/crates/account/Cargo.toml index b743110c99..838fdf7791 100644 --- a/crates/account/Cargo.toml +++ b/crates/account/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-account" version = "0.17.6" -edition = "2021" +edition = "2024" description = "Local accounts for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -68,6 +68,7 @@ uuid.workspace = true time.workspace = true hex.workspace = true serde_json.workspace = true +age.workspace = true sos-search = { workspace = true, optional = true } @@ -81,6 +82,3 @@ tokio = { version = "1", default-features = false, features = ["rt", "sync", "ma futures-util = { workspace = true, optional = true } vcard4 = { workspace = true, optional = true } time-tz = { workspace = true, optional = true } - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/account/build.rs b/crates/account/build.rs deleted file mode 100644 index 5976a1c6d5..0000000000 --- a/crates/account/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel); -} diff --git a/crates/account/src/builder.rs b/crates/account/src/builder.rs index 9cd14e8b5b..7e1a7f1677 100644 --- a/crates/account/src/builder.rs +++ b/crates/account/src/builder.rs @@ -4,17 +4,17 @@ use secrecy::SecretString; use sos_backend::BackendTarget; use sos_client_storage::AccountPack; use sos_core::{ + AccountId, SecretId, VaultFlags, VaultId, constants::{ DEFAULT_ARCHIVE_VAULT_NAME, DEFAULT_AUTHENTICATOR_VAULT_NAME, DEFAULT_CONTACTS_VAULT_NAME, }, crypto::AccessKey, - AccountId, SecretId, VaultFlags, VaultId, }; use sos_login::{DelegatedAccess, FolderKeys, Identity, IdentityFolder}; use sos_vault::{ - secret::{Secret, SecretMeta, SecretRow}, AccessPoint, BuilderCredentials, SecretAccess, Vault, VaultBuilder, + secret::{Secret, SecretMeta, SecretRow}, }; use std::collections::HashMap; diff --git a/crates/account/src/convert.rs b/crates/account/src/convert.rs index 827ca4b5eb..4b3595b9ea 100644 --- a/crates/account/src/convert.rs +++ b/crates/account/src/convert.rs @@ -9,8 +9,8 @@ use sos_core::{ }; use sos_login::DelegatedAccess; use sos_vault::{ - secret::SecretRow, BuilderCredentials, SecretAccess, Summary, Vault, - VaultBuilder, + BuilderCredentials, SecretAccess, Summary, Vault, VaultBuilder, + secret::SecretRow, }; /// Comparison between an existing cipher and a diff --git a/crates/account/src/lib.rs b/crates/account/src/lib.rs index 58a5c5d5ab..a14ff79827 100644 --- a/crates/account/src/lib.rs +++ b/crates/account/src/lib.rs @@ -1,5 +1,6 @@ //! Create and manage local accounts for the //! [Save Our Secrets](https://saveoursecrets.com) SDK. +#![cfg_attr(docsrs, feature(doc_cfg))] #![allow(clippy::len_without_is_empty)] #![allow(clippy::new_without_default)] #![allow(clippy::result_large_err)] @@ -27,9 +28,11 @@ pub use types::{ }; #[cfg(feature = "contacts")] +#[cfg_attr(docsrs, doc(cfg(feature = "contacts")))] pub use types::ContactImportProgress; #[cfg(feature = "clipboard")] +#[cfg_attr(docsrs, doc(cfg(feature = "clipboard")))] pub use { types::{ClipboardCopyRequest, ClipboardTextFormat}, xclipboard, diff --git a/crates/account/src/local_account.rs b/crates/account/src/local_account.rs index 275b4e1b47..4c5fba5c49 100644 --- a/crates/account/src/local_account.rs +++ b/crates/account/src/local_account.rs @@ -1,14 +1,14 @@ //! Local account storage and search index. use crate::{ - convert::CipherComparison, Account, AccountBuilder, AccountChange, - AccountData, Error, FolderChange, FolderCreate, FolderDelete, Result, - SecretChange, SecretDelete, SecretInsert, SecretMove, + Account, AccountBuilder, AccountChange, AccountData, Error, FolderChange, + FolderCreate, FolderDelete, Result, SecretChange, SecretDelete, + SecretInsert, SecretMove, convert::CipherComparison, }; use async_trait::async_trait; use indexmap::IndexSet; use secrecy::SecretString; use sos_backend::{ - compact::compact_folder, AccessPoint, BackendTarget, Folder, StorageError, + AccessPoint, BackendTarget, Folder, StorageError, compact::compact_folder, }; use sos_client_storage::{ AccessOptions, ClientAccountStorage, ClientBaseStorage, @@ -16,28 +16,29 @@ use sos_client_storage::{ ClientStorage, NewFolderOptions, }; use sos_core::{ + AccountId, AccountRef, AuthenticationError, FolderRef, Paths, Recipient, + SecretId, UtcDateTime, VaultCommit, VaultFlags, VaultId, commit::{CommitHash, CommitState}, crypto::{AccessKey, Cipher, KeyDerivation}, decode, device::{DevicePublicKey, TrustedDevice}, encode, events::{ - changes_feed, AccountEvent, DeviceEvent, Event, EventKind, EventLog, - EventRecord, LocalChangeEvent, ReadEvent, WriteEvent, + AccountEvent, DeviceEvent, Event, EventKind, EventLog, EventRecord, + LocalChangeEvent, ReadEvent, WriteEvent, changes_feed, }, - AccountId, AccountRef, AuthenticationError, FolderRef, Paths, SecretId, - UtcDateTime, VaultCommit, VaultFlags, VaultId, }; use sos_filesystem::write_exclusive; use sos_login::{ - device::{DeviceManager, DeviceSigner}, DelegatedAccess, FolderKeys, Identity, PublicIdentity, + device::{DeviceManager, DeviceSigner}, }; use sos_reducers::FolderReducer; use sos_sync::{CreateSet, StorageEventLogs}; use sos_vault::{ + BuilderCredentials, Header, SecretAccess, SharedAccess, Summary, Vault, + VaultBuilder, secret::{Secret, SecretMeta, SecretPath, SecretRow, SecretType}, - BuilderCredentials, Header, SecretAccess, Summary, Vault, VaultBuilder, }; use sos_vfs as vfs; use std::{ @@ -71,16 +72,16 @@ use crate::ContactImportProgress; #[cfg(feature = "migrate")] use sos_migrate::{ + Convert, export::PublicExport, import::{ + ImportFormat, ImportTarget, csv::{ bitwarden::BitwardenCsv, chrome::ChromePasswordCsv, dashlane::DashlaneCsvZip, firefox::FirefoxPasswordCsv, macos::MacPasswordCsv, one_password::OnePasswordCsv, }, - ImportFormat, ImportTarget, }, - Convert, }; #[cfg(feature = "clipboard")] @@ -279,10 +280,10 @@ impl LocalAccount { ) -> Result<()> { // Bail early if the folder is already open { - if let Some(current) = self.storage.current_folder() { - if current.id() == folder_id { - return Ok(()); - } + if let Some(current) = self.storage.current_folder() + && current.id() == folder_id + { + return Ok(()); } } @@ -356,10 +357,10 @@ impl LocalAccount { self.open_folder(folder.id()).await?; - if let Secret::Pem { certificates, .. } = &secret { - if certificates.is_empty() { - return Err(Error::PemEncoding); - } + if let Secret::Pem { certificates, .. } = &secret + && certificates.is_empty() + { + return Err(Error::PemEncoding); } let id = SecretId::new_v4(); @@ -587,6 +588,70 @@ impl LocalAccount { Ok(result) } + + /// Prepare a shared folder. + pub async fn prepare_shared_folder( + &self, + options: NewFolderOptions, + recipients: &[Recipient], + shared_access: Option, + ) -> Result<(Vault, AccessKey)> { + let authenticated_user = self + .storage + .authenticated_user() + .ok_or(AuthenticationError::NotAuthenticated)?; + let shared_private_key = + authenticated_user.shared_private_access_key()?; + let shared_public_key = + authenticated_user.shared_public_access_key()?; + + let shared_access = shared_access.unwrap_or_else(|| { + let owner_key = shared_public_key.to_string(); + let mut public_keys = recipients + .iter() + .map(|r| r.public_key.to_string()) + .collect::>(); + if !public_keys.contains(&owner_key) { + public_keys.push(owner_key); + } + SharedAccess::WriteAccess(public_keys) + }); + + let mut flags = options.flags.unwrap_or(VaultFlags::SHARED); + flags.set(VaultFlags::SHARED, true); + let builder = VaultBuilder::new() + .flags(flags) + .cipher(Cipher::X25519) + .kdf(options.kdf.unwrap_or_default()) + .public_name(options.name); + + let recipient_public_keys = + recipients.iter().map(|r| r.public_key.clone()).collect(); + let read_only = matches!(shared_access, SharedAccess::ReadOnly(_)); + + match &shared_private_key { + AccessKey::Identity(id) => Ok(( + builder + .build(BuilderCredentials::Shared { + owner: id, + recipients: recipient_public_keys, + read_only, + }) + .await?, + shared_private_key, + )), + _ => unreachable!(), + } + } + + #[doc(hidden)] + pub async fn shared_private_access_key(&self) -> Result { + let authenticated_user = self + .storage + .authenticated_user() + .ok_or(AuthenticationError::NotAuthenticated)?; + Ok(authenticated_user.shared_private_access_key()?) + } } impl From<&LocalAccount> for AccountRef { @@ -626,6 +691,16 @@ impl Account for LocalAccount { self.storage.is_authenticated() } + async fn shared_access_public_key( + &self, + ) -> Result { + let authenticated_user = self + .storage + .authenticated_user() + .ok_or(AuthenticationError::NotAuthenticated)?; + Ok(authenticated_user.shared_public_access_key()?) + } + async fn device_signer(&self) -> Result { let authenticated_user = self .storage @@ -1173,13 +1248,11 @@ impl Account for LocalAccount { compact_folder(self.account_id(), identity.id(), &mut log_file) .await?; - let vault = FolderReducer::new() + FolderReducer::new() .reduce(&*log_file) .await? .build(true) - .await?; - - vault + .await? }; let event = { @@ -1487,10 +1560,10 @@ impl Account for LocalAccount { self.open_folder(folder.id()).await?; - if let Some(Secret::Pem { certificates, .. }) = &secret { - if certificates.is_empty() { - return Err(Error::PemEncoding); - } + if let Some(Secret::Pem { certificates, .. }) = &secret + && certificates.is_empty() + { + return Err(Error::PemEncoding); } let result = self diff --git a/crates/account/src/sync.rs b/crates/account/src/sync.rs index 4cccff0117..9ebf48ca0e 100644 --- a/crates/account/src/sync.rs +++ b/crates/account/src/sync.rs @@ -6,11 +6,11 @@ use async_trait::async_trait; use indexmap::IndexSet; use sos_backend::{AccountEventLog, DeviceEventLog, FolderEventLog}; use sos_core::{ + VaultId, events::{ - patch::{AccountDiff, CheckedPatch, DeviceDiff, FolderDiff}, WriteEvent, + patch::{AccountDiff, CheckedPatch, DeviceDiff, FolderDiff}, }, - VaultId, }; use sos_sync::{ ForceMerge, Merge, MergeOutcome, StorageEventLogs, SyncStorage, diff --git a/crates/account/src/traits.rs b/crates/account/src/traits.rs index 16706d8299..c60eb5793c 100644 --- a/crates/account/src/traits.rs +++ b/crates/account/src/traits.rs @@ -1,28 +1,28 @@ //! Account storage and search index. -use crate::{convert::CipherComparison, Error}; use crate::{ AccountChange, AccountData, DetachedView, FolderChange, FolderCreate, FolderDelete, SecretChange, SecretDelete, SecretInsert, SecretMove, }; +use crate::{Error, convert::CipherComparison}; use indexmap::IndexSet; use sos_backend::{BackendTarget, Folder}; use sos_client_storage::{AccessOptions, NewFolderOptions}; use sos_core::{ + AccountId, ErrorExt, FolderRef, Paths, SecretId, UtcDateTime, + VaultCommit, VaultFlags, VaultId, commit::{CommitHash, CommitState}, crypto::{AccessKey, Cipher, KeyDerivation}, device::{DevicePublicKey, TrustedDevice}, events::{AccountEvent, DeviceEvent, EventRecord, ReadEvent, WriteEvent}, - AccountId, ErrorExt, FolderRef, Paths, SecretId, UtcDateTime, - VaultCommit, VaultFlags, VaultId, }; use sos_login::{ - device::{DeviceManager, DeviceSigner}, PublicIdentity, + device::{DeviceManager, DeviceSigner}, }; use sos_sync::CreateSet; use sos_vault::{ - secret::{Secret, SecretMeta, SecretPath, SecretRow, SecretType}, Summary, Vault, + secret::{Secret, SecretMeta, SecretPath, SecretRow, SecretType}, }; use std::{collections::HashMap, path::Path, sync::Arc}; @@ -70,6 +70,11 @@ pub trait Account { /// Determine if the account is authenticated. async fn is_authenticated(&self) -> bool; + /// Public key for shared access. + async fn shared_access_public_key( + &self, + ) -> Result; + /// Import encrypted account events into the client storage. async fn import_account_events( &mut self, diff --git a/crates/account/src/types.rs b/crates/account/src/types.rs index 77bda3279d..cc0ee724ea 100644 --- a/crates/account/src/types.rs +++ b/crates/account/src/types.rs @@ -1,6 +1,6 @@ //! Account management types. use sos_backend::AccessPoint; -use sos_core::{commit::CommitState, events::Event, SecretId}; +use sos_core::{SecretId, commit::CommitState, events::Event}; use sos_login::PublicIdentity; use sos_vault::Summary; use std::sync::Arc; diff --git a/crates/archive/Cargo.toml b/crates/archive/Cargo.toml index 5c059f0032..bdce22da89 100644 --- a/crates/archive/Cargo.toml +++ b/crates/archive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-archive" version = "0.17.2" -edition = "2021" +edition = "2024" description = "ZIP archive reader and writer for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" diff --git a/crates/archive/src/lib.rs b/crates/archive/src/lib.rs index e683271290..c9b0ab6cfe 100644 --- a/crates/archive/src/lib.rs +++ b/crates/archive/src/lib.rs @@ -1,6 +1,7 @@ +//! ZIP archive reader and writer for account backup archives. #![deny(missing_docs)] #![forbid(unsafe_code)] -//! ZIP archive reader and writer for account backup archives. +#![cfg_attr(docsrs, feature(doc_cfg))] mod error; mod reader; diff --git a/crates/archive/src/reader.rs b/crates/archive/src/reader.rs index cda43d2c37..2e05eaab5f 100644 --- a/crates/archive/src/reader.rs +++ b/crates/archive/src/reader.rs @@ -1,4 +1,4 @@ -use crate::{Result, ARCHIVE_MANIFEST}; +use crate::{ARCHIVE_MANIFEST, Result}; use async_zip::tokio::read::seek::ZipFileReader; use serde::de::DeserializeOwned; use tokio::io::{AsyncBufRead, AsyncSeek}; diff --git a/crates/archive/src/writer.rs b/crates/archive/src/writer.rs index 6cfc03b233..93f98789c2 100644 --- a/crates/archive/src/writer.rs +++ b/crates/archive/src/writer.rs @@ -1,7 +1,7 @@ use crate::Result; use async_zip::{ - tokio::write::ZipFileWriter, Compression, ZipDateTimeBuilder, - ZipEntryBuilder, + Compression, ZipDateTimeBuilder, ZipEntryBuilder, + tokio::write::ZipFileWriter, }; use time::OffsetDateTime; use tokio::io::AsyncWrite; diff --git a/crates/artifact/Cargo.toml b/crates/artifact/Cargo.toml index c45ce2a365..97d6ff3290 100644 --- a/crates/artifact/Cargo.toml +++ b/crates/artifact/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-artifact" version = "0.8.11" -edition = "2021" +edition = "2024" description = "Types for release artifact meta data" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" diff --git a/crates/artifact/src/artifact.rs b/crates/artifact/src/artifact.rs index 2c08ac659f..2cb8955ec6 100644 --- a/crates/artifact/src/artifact.rs +++ b/crates/artifact/src/artifact.rs @@ -1,8 +1,8 @@ //! Types for release artifact meta data. use crate::Error; use serde::{ - de::{self, Deserializer, Visitor}, Deserialize, Serialize, Serializer, + de::{self, Deserializer, Visitor}, }; use std::{fmt, str::FromStr}; use time::OffsetDateTime; diff --git a/crates/artifact/src/lib.rs b/crates/artifact/src/lib.rs index 5f0f845c80..21f755e6ad 100644 --- a/crates/artifact/src/lib.rs +++ b/crates/artifact/src/lib.rs @@ -1,6 +1,7 @@ +//! Release artifact meta data for the [Save Our Secrets](https://saveoursecrets.com) SDK. #![deny(missing_docs)] #![forbid(unsafe_code)] -//! Release artifact meta data for the [Save Our Secrets](https://saveoursecrets.com) SDK. +#![cfg_attr(docsrs, feature(doc_cfg))] pub use semver; diff --git a/crates/audit/Cargo.toml b/crates/audit/Cargo.toml index feec661367..1bd3690a43 100644 --- a/crates/audit/Cargo.toml +++ b/crates/audit/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-audit" version = "0.17.1" -edition = "2021" +edition = "2024" description = "Audit trail for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -17,6 +17,3 @@ serde.workspace = true futures.workspace = true binary-stream.workspace = true tokio.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/audit/src/encoding.rs b/crates/audit/src/encoding.rs index 1dd9b62b01..bfdc425492 100644 --- a/crates/audit/src/encoding.rs +++ b/crates/audit/src/encoding.rs @@ -4,8 +4,8 @@ use binary_stream::futures::{ BinaryReader, BinaryWriter, Decodable, Encodable, }; use sos_core::{ - encoding::{decode_uuid, encoding_error}, UtcDateTime, + encoding::{decode_uuid, encoding_error}, }; use std::io::{Error, Result}; use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite}; diff --git a/crates/audit/src/event.rs b/crates/audit/src/event.rs index 6fb6dfa935..9b7a6a66ed 100644 --- a/crates/audit/src/event.rs +++ b/crates/audit/src/event.rs @@ -5,7 +5,7 @@ use sos_core::device::DevicePublicKey; use sos_core::events::{ AccountEvent, Event, EventKind, ReadEvent, WriteEvent, }; -use sos_core::{events::LogEvent, AccountId, SecretId, UtcDateTime, VaultId}; +use sos_core::{AccountId, SecretId, UtcDateTime, VaultId, events::LogEvent}; bitflags! { /// Bit flags for associated data. diff --git a/crates/audit/src/lib.rs b/crates/audit/src/lib.rs index 10625e0215..4dd60a25e0 100644 --- a/crates/audit/src/lib.rs +++ b/crates/audit/src/lib.rs @@ -1,6 +1,8 @@ +//! Core types and traits for audit trail logging. #![deny(missing_docs)] #![forbid(unsafe_code)] -//! Core types and traits for audit trail logging. +#![cfg_attr(docsrs, feature(doc_cfg))] + mod encoding; mod event; diff --git a/crates/backend/Cargo.toml b/crates/backend/Cargo.toml index 7a1faa2ebf..78085a6a71 100644 --- a/crates/backend/Cargo.toml +++ b/crates/backend/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-backend" version = "0.17.2" -edition = "2021" +edition = "2024" description = "Backend storage abstraction for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -76,6 +76,3 @@ serde.workspace = true # system-messages urn = { workspace = true, optional = true } - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/backend/build.rs b/crates/backend/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/backend/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/backend/src/access_point.rs b/crates/backend/src/access_point.rs index 5623fc744b..b9ad3d249e 100644 --- a/crates/backend/src/access_point.rs +++ b/crates/backend/src/access_point.rs @@ -1,15 +1,15 @@ use crate::{BackendTarget, Error, Result}; use async_trait::async_trait; use sos_core::{ + SecretId, VaultCommit, VaultFlags, VaultId, crypto::{AccessKey, AeadPack, PrivateKey}, events::{ReadEvent, WriteEvent}, - SecretId, VaultCommit, VaultFlags, VaultId, }; use sos_database::VaultDatabaseWriter; use sos_filesystem::VaultFileWriter; use sos_vault::{ - secret::{Secret, SecretMeta, SecretRow}, AccessPoint, SecretAccess, Summary, Vault, VaultMeta, + secret::{Secret, SecretMeta, SecretRow}, }; use std::path::Path; diff --git a/crates/backend/src/compact.rs b/crates/backend/src/compact.rs index feb6004737..0346668e5f 100644 --- a/crates/backend/src/compact.rs +++ b/crates/backend/src/compact.rs @@ -1,17 +1,18 @@ //! Compact folders. use crate::{BackendEventLog, Error, FolderEventLog, Result}; use sos_core::{ + AccountId, VaultId, events::{ - patch::{FolderDiff, Patch}, EventLog, EventLogType, EventRecord, + patch::{FolderDiff, Patch}, }, - AccountId, VaultId, }; use sos_database::{ + EventLogOwner, entity::{ AccountEntity, AccountRow, FolderEntity, FolderRecord, FolderRow, }, - open_memory, EventLogOwner, + open_memory, }; use sos_filesystem::FolderEventLog as FsFolderEventLog; use sos_reducers::FolderReducer; diff --git a/crates/backend/src/error.rs b/crates/backend/src/error.rs index 57207e328d..4d269679a9 100644 --- a/crates/backend/src/error.rs +++ b/crates/backend/src/error.rs @@ -19,13 +19,17 @@ pub enum Error { /// Errors generated when a v1 or v2 backup archive /// is used with the v3 backend. #[cfg(feature = "archive")] - #[error("backup archive '{0}' (version {1}) is not compatible with {2} backend (backup archive needs upgrading)")] + #[error( + "backup archive '{0}' (version {1}) is not compatible with {2} backend (backup archive needs upgrading)" + )] BackupArchiveUpgradeRequired(PathBuf, u8, String), /// Errors generated when a v3 backup archive is used with /// the file system backend. #[cfg(feature = "archive")] - #[error("backup archive '{0}' (version {1}) is not compatible with {2} backend (accounts need upgrading)")] + #[error( + "backup archive '{0}' (version {1}) is not compatible with {2} backend (accounts need upgrading)" + )] IncompatibleBackupArchive(PathBuf, u8, String), /// Error generated converting to fixed length slice. diff --git a/crates/backend/src/event_log.rs b/crates/backend/src/event_log.rs index c60cf8c93f..3aecfebd1a 100644 --- a/crates/backend/src/event_log.rs +++ b/crates/backend/src/event_log.rs @@ -3,17 +3,17 @@ use async_trait::async_trait; use binary_stream::futures::{Decodable, Encodable}; use futures::stream::BoxStream; use sos_core::{ + AccountId, VaultId, commit::{CommitHash, CommitProof, CommitTree}, events::{ - patch::{CheckedPatch, Diff, Patch}, AccountEvent, DeviceEvent, EventLog, EventLogType, EventRecord, WriteEvent, + patch::{CheckedPatch, Diff, Patch}, }, - AccountId, VaultId, }; use sos_database::{ - entity::{AccountEntity, FolderEntity, FolderRecord}, DatabaseEventLog, + entity::{AccountEntity, FolderEntity, FolderRecord}, }; use sos_filesystem::FileSystemEventLog; diff --git a/crates/backend/src/folder.rs b/crates/backend/src/folder.rs index fc221fe7d8..05bb17fb26 100644 --- a/crates/backend/src/folder.rs +++ b/crates/backend/src/folder.rs @@ -1,23 +1,23 @@ //! Folder combines an access point with an event log. use crate::{AccessPoint, BackendTarget, Error, FolderEventLog, Result}; use sos_core::{ + AccountId, VaultFlags, VaultId, commit::{CommitHash, CommitState}, crypto::AccessKey, encode, events::{EventLog, EventLogType, EventRecord, ReadEvent, WriteEvent}, - AccountId, VaultFlags, VaultId, }; -use sos_core::{constants::EVENT_LOG_EXT, decode, VaultCommit}; +use sos_core::{VaultCommit, constants::EVENT_LOG_EXT, decode}; use sos_database::{ - entity::{FolderEntity, FolderRecord, SecretRecord}, VaultDatabaseWriter, + entity::{FolderEntity, FolderRecord, SecretRecord}, }; use sos_filesystem::VaultFileWriter; use sos_reducers::FolderReducer; use sos_vault::{ - secret::{Secret, SecretId, SecretMeta, SecretRow}, AccessPoint as VaultAccessPoint, EncryptedEntry, SecretAccess, Vault, VaultMeta, + secret::{Secret, SecretId, SecretMeta, SecretRow}, }; use sos_vfs as vfs; use std::{path::Path, sync::Arc}; diff --git a/crates/backend/src/lib.rs b/crates/backend/src/lib.rs index b73a5832fa..aa1eb6e921 100644 --- a/crates/backend/src/lib.rs +++ b/crates/backend/src/lib.rs @@ -1,7 +1,9 @@ +//! Backend database and file system storage. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -//! Backend database and file system storage. +#![cfg_attr(docsrs, feature(doc_cfg))] +#![allow(clippy::large_enum_variant)] + mod access_point; #[cfg(feature = "archive")] pub mod archive; @@ -40,7 +42,7 @@ pub use event_log::FileEventLog; /// Result type for the library. pub(crate) type Result = std::result::Result; -use sos_core::{decode, AccountId, Paths, PublicIdentity}; +use sos_core::{AccountId, Paths, PublicIdentity, decode}; use sos_database::{ async_sqlite::Client, entity::{ diff --git a/crates/backend/src/vault_writer.rs b/crates/backend/src/vault_writer.rs index 42755e89e8..cc5730e56f 100644 --- a/crates/backend/src/vault_writer.rs +++ b/crates/backend/src/vault_writer.rs @@ -1,10 +1,10 @@ use crate::{BackendTarget, Error, Result}; use async_trait::async_trait; use sos_core::{ + SecretId, VaultCommit, VaultEntry, VaultFlags, VaultId, commit::CommitHash, crypto::AeadPack, events::{ReadEvent, WriteEvent}, - SecretId, VaultCommit, VaultEntry, VaultFlags, VaultId, }; use sos_database::VaultDatabaseWriter; use sos_filesystem::VaultFileWriter; diff --git a/crates/changes/Cargo.toml b/crates/changes/Cargo.toml index 224ec8d72e..c10a78f750 100644 --- a/crates/changes/Cargo.toml +++ b/crates/changes/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-changes" version = "0.17.3" -edition = "2021" +edition = "2024" description = "Local socket change event producer and consumer for the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -24,6 +24,3 @@ tokio.workspace = true serde_json.workspace = true interprocess = { workspace = true, optional = true, features = ["tokio"] } tokio-util = { workspace = true, optional = true, features = ["codec"] } - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/changes/build.rs b/crates/changes/build.rs deleted file mode 100644 index 5976a1c6d5..0000000000 --- a/crates/changes/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel); -} diff --git a/crates/changes/src/consumer.rs b/crates/changes/src/consumer.rs index 73d6020cbe..72a500182e 100644 --- a/crates/changes/src/consumer.rs +++ b/crates/changes/src/consumer.rs @@ -2,9 +2,9 @@ use crate::{Error, Result, SocketFile}; use futures::stream::StreamExt; use interprocess::local_socket::{ - tokio::prelude::*, GenericNamespaced, ListenerOptions, + GenericNamespaced, ListenerOptions, tokio::prelude::*, }; -use sos_core::{events::LocalChangeEvent, Paths}; +use sos_core::{Paths, events::LocalChangeEvent}; use std::{path::PathBuf, sync::Arc}; use tokio::{ select, diff --git a/crates/changes/src/lib.rs b/crates/changes/src/lib.rs index 02cdb4d93a..7bd8fe7eae 100644 --- a/crates/changes/src/lib.rs +++ b/crates/changes/src/lib.rs @@ -1,7 +1,7 @@ //! Local socket change notification producer and consumer. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] mod error; pub use error::Error; diff --git a/crates/changes/src/producer.rs b/crates/changes/src/producer.rs index 8ef6feb29b..7fd9d1fe8a 100644 --- a/crates/changes/src/producer.rs +++ b/crates/changes/src/producer.rs @@ -1,10 +1,10 @@ //! Producer for change notifications on a local socket. use crate::{Error, Result}; use futures::sink::SinkExt; -use interprocess::local_socket::{tokio::prelude::*, GenericNamespaced}; +use interprocess::local_socket::{GenericNamespaced, tokio::prelude::*}; use sos_core::{ - events::{changes_feed, LocalChangeEvent}, Paths, + events::{LocalChangeEvent, changes_feed}, }; use std::{path::PathBuf, sync::Arc, time::Duration}; use tokio::{select, sync::Mutex, time}; @@ -126,16 +126,15 @@ async fn find_active_sockets( ); for entry in read_dir(&socks)? { let entry = entry?; - if let Some(stem) = entry.path().file_stem() { - if let Ok(pid) = + if let Some(stem) = entry.path().file_stem() + && let Ok(pid) = stem.to_string_lossy().as_ref().parse::() - { - tracing::debug!( - sock_file_pid = %pid, - "changes::producer::find_active_sockets", - ); - sockets.push((pid, entry.path().to_owned())); - } + { + tracing::debug!( + sock_file_pid = %pid, + "changes::producer::find_active_sockets", + ); + sockets.push((pid, entry.path().to_owned())); } } } diff --git a/crates/cli_helpers/Cargo.toml b/crates/cli_helpers/Cargo.toml index 20b26eb6cf..6231dda2be 100644 --- a/crates/cli_helpers/Cargo.toml +++ b/crates/cli_helpers/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-cli-helpers" version = "0.1.3" -edition = "2021" +edition = "2024" description = "Shared helpers for command line executables." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" diff --git a/crates/clipboard/Cargo.toml b/crates/clipboard/Cargo.toml index c42154a0ec..e9fae08f8f 100644 --- a/crates/clipboard/Cargo.toml +++ b/crates/clipboard/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "xclipboard" version = "0.16.3" -edition = "2021" +edition = "2024" description = "Cross-platform clipboard with extra features." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -19,6 +19,3 @@ zeroize = { workspace = true } [target.'cfg(all(not(target_os = "android"), not(target_os = "ios")))'.dependencies.arboard] workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/clipboard/build.rs b/crates/clipboard/build.rs deleted file mode 100644 index 5976a1c6d5..0000000000 --- a/crates/clipboard/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel); -} diff --git a/crates/clipboard/src/android.rs b/crates/clipboard/src/android.rs index 8dbf5cf9a7..a868964e72 100644 --- a/crates/clipboard/src/android.rs +++ b/crates/clipboard/src/android.rs @@ -3,7 +3,7 @@ use crate::Result; use std::{borrow::Cow, sync::Arc}; use tokio::{ sync::Mutex, - time::{sleep, Duration}, + time::{Duration, sleep}, }; use zeroize::Zeroize; diff --git a/crates/clipboard/src/desktop.rs b/crates/clipboard/src/desktop.rs index 1050df52c2..300b3175e4 100644 --- a/crates/clipboard/src/desktop.rs +++ b/crates/clipboard/src/desktop.rs @@ -3,7 +3,7 @@ use crate::Result; use std::{borrow::Cow, sync::Arc}; use tokio::{ sync::Mutex, - time::{sleep, Duration}, + time::{Duration, sleep}, }; use zeroize::Zeroize; diff --git a/crates/clipboard/src/ios.rs b/crates/clipboard/src/ios.rs index 8dbf5cf9a7..a868964e72 100644 --- a/crates/clipboard/src/ios.rs +++ b/crates/clipboard/src/ios.rs @@ -3,7 +3,7 @@ use crate::Result; use std::{borrow::Cow, sync::Arc}; use tokio::{ sync::Mutex, - time::{sleep, Duration}, + time::{Duration, sleep}, }; use zeroize::Zeroize; diff --git a/crates/clipboard/src/lib.rs b/crates/clipboard/src/lib.rs index ea5826ff40..cc4b62df34 100644 --- a/crates/clipboard/src/lib.rs +++ b/crates/clipboard/src/lib.rs @@ -2,7 +2,7 @@ //! sensitive data and listen for clipboard change events. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] mod error; diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index b1803c5bc1..dc174c3d07 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-core" version = "0.17.3" -edition = "2021" +edition = "2024" description = "Core types for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -49,6 +49,3 @@ balloon-hash.workspace = true [target.'cfg(not(target_arch = "wasm32"))'.dependencies] etcetera.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/core/build.rs b/crates/core/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/core/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/core/src/account.rs b/crates/core/src/account.rs index a95bbcc72e..6855144fc6 100644 --- a/crates/core/src/account.rs +++ b/crates/core/src/account.rs @@ -17,7 +17,7 @@ impl AccountId { /// Create a random account identifier. pub fn random() -> Self { let mut rng = crate::csprng(); - Self(rng.gen()) + Self(rng.r#gen()) } } diff --git a/crates/core/src/commit/mod.rs b/crates/core/src/commit/mod.rs index 69c6d617a0..0e2edb9b7b 100644 --- a/crates/core/src/commit/mod.rs +++ b/crates/core/src/commit/mod.rs @@ -2,7 +2,7 @@ mod proof; mod tree; -use rs_merkle::{algorithms::Sha256, Hasher}; +use rs_merkle::{Hasher, algorithms::Sha256}; use serde::{Deserialize, Serialize}; /// Type for an Sha256 commit tree hash. diff --git a/crates/core/src/commit/proof.rs b/crates/core/src/commit/proof.rs index 224dccb72d..73c8783a42 100644 --- a/crates/core/src/commit/proof.rs +++ b/crates/core/src/commit/proof.rs @@ -1,6 +1,6 @@ //! Types that encapsulate commit proofs and comparisons. use super::TreeHash; -use rs_merkle::{algorithms::Sha256, MerkleProof}; +use rs_merkle::{MerkleProof, algorithms::Sha256}; use serde::{Deserialize, Serialize}; use std::{ fmt, @@ -74,10 +74,10 @@ pub enum Comparison { } mod proof_serde { - use rs_merkle::{algorithms::Sha256, MerkleProof}; + use rs_merkle::{MerkleProof, algorithms::Sha256}; use serde::{ - de::{Deserialize, Deserializer, Error}, Serializer, + de::{Deserialize, Deserializer, Error}, }; use std::borrow::Cow; diff --git a/crates/core/src/commit/tree.rs b/crates/core/src/commit/tree.rs index 783e0cf5a5..1c4054f6c0 100644 --- a/crates/core/src/commit/tree.rs +++ b/crates/core/src/commit/tree.rs @@ -1,5 +1,5 @@ use crate::{Error, Result}; -use rs_merkle::{algorithms::Sha256, Hasher, MerkleTree}; +use rs_merkle::{Hasher, MerkleTree, algorithms::Sha256}; use super::{CommitHash, CommitProof, CommitState, Comparison, TreeHash}; diff --git a/crates/core/src/crypto/cipher/aes_gcm_256.rs b/crates/core/src/crypto/cipher/aes_gcm_256.rs index f922391261..709eff0f18 100644 --- a/crates/core/src/crypto/cipher/aes_gcm_256.rs +++ b/crates/core/src/crypto/cipher/aes_gcm_256.rs @@ -1,7 +1,7 @@ //! Encrypt and decrypt using 256 bit AES GSM. use crate::crypto::{AeadPack, DerivedPrivateKey, Nonce}; use crate::{Error, Result}; -use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit, Nonce as AesNonce}; +use aes_gcm::{Aes256Gcm, KeyInit, Nonce as AesNonce, aead::Aead}; /// Encrypt plaintext using the given key as 256 bit AES-GCM. /// diff --git a/crates/core/src/crypto/cipher/x25519.rs b/crates/core/src/crypto/cipher/x25519.rs index 5143e6fe32..52342556e3 100644 --- a/crates/core/src/crypto/cipher/x25519.rs +++ b/crates/core/src/crypto/cipher/x25519.rs @@ -1,6 +1,6 @@ //! Encrypt and decrypt using X25519 asymmetric encryption (AGE). -use crate::crypto::{AeadPack, Nonce}; use crate::Result; +use crate::crypto::{AeadPack, Nonce}; use age::x25519::{Identity, Recipient}; use futures::io::{AsyncReadExt, BufReader}; @@ -9,6 +9,10 @@ pub async fn encrypt( plaintext: &[u8], recipients: Vec, ) -> Result { + debug_assert!( + !recipients.is_empty(), + "asymmetric encryption recipients must not be empty" + ); let recipients: Vec<_> = recipients .into_iter() .map(|r| { diff --git a/crates/core/src/crypto/key_derivation.rs b/crates/core/src/crypto/key_derivation.rs index 579347759e..3fff1ac406 100644 --- a/crates/core/src/crypto/key_derivation.rs +++ b/crates/core/src/crypto/key_derivation.rs @@ -1,11 +1,11 @@ //! Constants for supported key derivation functions. use crate::{ - crypto::{csprng, DerivedPrivateKey}, Error, Result, + crypto::{DerivedPrivateKey, csprng}, }; use argon2::{ - password_hash::{PasswordHash, PasswordHasher, SaltString}, Argon2, + password_hash::{PasswordHash, PasswordHasher, SaltString}, }; use balloon_hash::Balloon; use rand::Rng; @@ -70,7 +70,7 @@ impl KeyDerivation { /// Generate new random seed entropy. #[deprecated] pub fn generate_seed() -> Seed { - let bytes: [u8; Seed::SIZE] = csprng().gen(); + let bytes: [u8; Seed::SIZE] = csprng().r#gen(); Seed(bytes) } } diff --git a/crates/core/src/crypto/mod.rs b/crates/core/src/crypto/mod.rs index af9fec065a..31dabdbfd4 100644 --- a/crates/core/src/crypto/mod.rs +++ b/crates/core/src/crypto/mod.rs @@ -10,7 +10,7 @@ mod key_derivation; mod private_key; pub use cipher::Cipher; -pub(crate) use cipher::{AES_GCM_256, X25519, X_CHACHA20_POLY1305}; +pub(crate) use cipher::{AES_GCM_256, X_CHACHA20_POLY1305, X25519}; #[doc(hidden)] pub use key_derivation::Deriver; @@ -33,13 +33,13 @@ pub enum Nonce { impl Nonce { /// Generate a new random 12 byte nonce. pub fn new_random_12() -> Nonce { - let val: [u8; 12] = csprng().gen(); + let val: [u8; 12] = csprng().r#gen(); Nonce::Nonce12(val) } /// Generate a new random 24 byte nonce. pub fn new_random_24() -> Nonce { - let val: [u8; 24] = csprng().gen(); + let val: [u8; 24] = csprng().r#gen(); Nonce::Nonce24(val) } } @@ -53,8 +53,8 @@ impl Default for Nonce { impl AsRef<[u8]> for Nonce { fn as_ref(&self) -> &[u8] { match self { - Nonce::Nonce12(ref val) => val, - Nonce::Nonce24(ref val) => val, + Nonce::Nonce12(val) => val, + Nonce::Nonce24(val) => val, } } } diff --git a/crates/core/src/crypto/private_key.rs b/crates/core/src/crypto/private_key.rs index 8cc5287a47..508ae9bca9 100644 --- a/crates/core/src/crypto/private_key.rs +++ b/crates/core/src/crypto/private_key.rs @@ -6,8 +6,8 @@ use std::convert::AsRef; use std::fmt; use crate::{ - crypto::{KeyDerivation, Seed}, Result, + crypto::{KeyDerivation, Seed}, }; /// Access key used to unlock a vault. @@ -103,7 +103,7 @@ impl DerivedPrivateKey { pub fn generate() -> Self { use crate::crypto::csprng; use rand::Rng; - let bytes: [u8; 32] = csprng().gen(); + let bytes: [u8; 32] = csprng().r#gen(); Self { inner: SecretBox::new(Box::new(bytes.to_vec())), } diff --git a/crates/core/src/date_time.rs b/crates/core/src/date_time.rs index 27a9f5d569..5a5b22d7f2 100644 --- a/crates/core/src/date_time.rs +++ b/crates/core/src/date_time.rs @@ -8,11 +8,11 @@ use crate::Result; use serde::{Deserialize, Serialize}; use std::fmt; use time::{ + Date, Month, OffsetDateTime, Time, UtcOffset, format_description::{ self, well_known::{Rfc2822, Rfc3339}, }, - Date, Month, OffsetDateTime, Time, UtcOffset, }; use time_tz::{OffsetDateTimeExt, TimeZone}; diff --git a/crates/core/src/encoding/mod.rs b/crates/core/src/encoding/mod.rs index 06c557f22a..30e17d188e 100644 --- a/crates/core/src/encoding/mod.rs +++ b/crates/core/src/encoding/mod.rs @@ -9,8 +9,8 @@ pub use v1::VERSION; use crate::Result; use binary_stream::{ - futures::{BinaryReader, Decodable, Encodable}, Endian, Options, + futures::{BinaryReader, Decodable, Encodable}, }; use tokio::io::{AsyncRead, AsyncSeek}; diff --git a/crates/core/src/encoding/v1/commit.rs b/crates/core/src/encoding/v1/commit.rs index 025d795bbb..0d172c551e 100644 --- a/crates/core/src/encoding/v1/commit.rs +++ b/crates/core/src/encoding/v1/commit.rs @@ -6,7 +6,7 @@ use async_trait::async_trait; use binary_stream::futures::{ BinaryReader, BinaryWriter, Decodable, Encodable, }; -use rs_merkle::{algorithms::Sha256, MerkleProof}; +use rs_merkle::{MerkleProof, algorithms::Sha256}; use std::io::{Error, Result}; use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite}; diff --git a/crates/core/src/encoding/v1/crypto.rs b/crates/core/src/encoding/v1/crypto.rs index 6f613d0993..a759abbdae 100644 --- a/crates/core/src/encoding/v1/crypto.rs +++ b/crates/core/src/encoding/v1/crypto.rs @@ -1,7 +1,7 @@ use crate::{ crypto::{ - AeadPack, Cipher, KeyDerivation, Nonce, AES_GCM_256, ARGON_2_ID, - BALLOON_HASH, X25519, X_CHACHA20_POLY1305, + AES_GCM_256, ARGON_2_ID, AeadPack, BALLOON_HASH, Cipher, + KeyDerivation, Nonce, X_CHACHA20_POLY1305, X25519, }, encoding::encoding_error, }; @@ -20,11 +20,11 @@ impl Encodable for AeadPack { writer: &mut BinaryWriter, ) -> Result<()> { match &self.nonce { - Nonce::Nonce12(ref bytes) => { + Nonce::Nonce12(bytes) => { writer.write_u8(12).await?; writer.write_bytes(bytes).await?; } - Nonce::Nonce24(ref bytes) => { + Nonce::Nonce24(bytes) => { writer.write_u8(24).await?; writer.write_bytes(bytes).await?; } diff --git a/crates/core/src/encoding/v1/date_time.rs b/crates/core/src/encoding/v1/date_time.rs index ad76863bb2..ac0aa4c6c3 100644 --- a/crates/core/src/encoding/v1/date_time.rs +++ b/crates/core/src/encoding/v1/date_time.rs @@ -1,4 +1,4 @@ -use crate::{encoding::encoding_error, UtcDateTime}; +use crate::{UtcDateTime, encoding::encoding_error}; use async_trait::async_trait; use binary_stream::futures::{ BinaryReader, BinaryWriter, Decodable, Encodable, diff --git a/crates/core/src/encoding/v1/event_record.rs b/crates/core/src/encoding/v1/event_record.rs index ddd7803dd5..cbce31f7bc 100644 --- a/crates/core/src/encoding/v1/event_record.rs +++ b/crates/core/src/encoding/v1/event_record.rs @@ -1,6 +1,6 @@ use crate::{ - commit::CommitHash, encoding::encoding_error, events::EventRecord, - UtcDateTime, + UtcDateTime, commit::CommitHash, encoding::encoding_error, + events::EventRecord, }; use async_trait::async_trait; use binary_stream::futures::{ diff --git a/crates/core/src/encoding/v1/events.rs b/crates/core/src/encoding/v1/events.rs index 09c47bade7..15994e5929 100644 --- a/crates/core/src/encoding/v1/events.rs +++ b/crates/core/src/encoding/v1/events.rs @@ -1,11 +1,11 @@ +#[cfg(feature = "files")] +use crate::{SecretPath, events::FileEvent}; use crate::{ + VaultCommit, VaultFlags, crypto::AeadPack, encoding::{decode_uuid, encoding_error}, events::{AccountEvent, DeviceEvent, EventKind, LogEvent, WriteEvent}, - VaultCommit, VaultFlags, }; -#[cfg(feature = "files")] -use crate::{events::FileEvent, SecretPath}; use std::io::{Error, Result}; use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite}; diff --git a/crates/core/src/encoding/v1/vault.rs b/crates/core/src/encoding/v1/vault.rs index a937b58138..6956dd3bcd 100644 --- a/crates/core/src/encoding/v1/vault.rs +++ b/crates/core/src/encoding/v1/vault.rs @@ -1,6 +1,6 @@ use crate::{ - commit::CommitHash, crypto::AeadPack, encoding::encoding_error, - VaultCommit, VaultEntry, + VaultCommit, VaultEntry, commit::CommitHash, crypto::AeadPack, + encoding::encoding_error, }; use async_trait::async_trait; use binary_stream::futures::{ diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index cfc53fce43..f575c25f74 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -73,6 +73,10 @@ pub enum Error { #[error("event log create vault event must only be the first record")] CreateEventOnlyFirst, + /// Encountered an unknown folder invite status. + #[error("unknown folder invite status code: {0}")] + UnknownInviteStatus(i64), + /// Generic boxed error. #[error(transparent)] Boxed(#[from] Box), diff --git a/crates/core/src/events/change.rs b/crates/core/src/events/change.rs index 675c433d1f..9bff1d2c51 100644 --- a/crates/core/src/events/change.rs +++ b/crates/core/src/events/change.rs @@ -1,4 +1,4 @@ -use crate::{commit::CommitSpan, events::EventLogType, AccountId}; +use crate::{AccountId, commit::CommitSpan, events::EventLogType}; use serde::{Deserialize, Serialize}; use std::sync::OnceLock; use tokio::sync::watch; diff --git a/crates/core/src/events/event_log.rs b/crates/core/src/events/event_log.rs index 2bd9d5e3fc..a193c533e5 100644 --- a/crates/core/src/events/event_log.rs +++ b/crates/core/src/events/event_log.rs @@ -1,10 +1,10 @@ use crate::{ + Error, commit::{CommitHash, CommitProof, CommitTree}, events::{ - patch::{CheckedPatch, Diff, Patch}, EventRecord, + patch::{CheckedPatch, Diff, Patch}, }, - Error, }; use async_trait::async_trait; use binary_stream::futures::{Decodable, Encodable}; diff --git a/crates/core/src/events/mod.rs b/crates/core/src/events/mod.rs index e3aa51355e..791f0bd0fe 100644 --- a/crates/core/src/events/mod.rs +++ b/crates/core/src/events/mod.rs @@ -22,7 +22,7 @@ mod record; mod write; pub use account::AccountEvent; -pub use change::{changes_feed, LocalChangeEvent}; +pub use change::{LocalChangeEvent, changes_feed}; pub use device::DeviceEvent; pub use event::Event; pub use event_kind::EventKind; diff --git a/crates/core/src/events/patch.rs b/crates/core/src/events/patch.rs index 22d35d69ab..482fac7e58 100644 --- a/crates/core/src/events/patch.rs +++ b/crates/core/src/events/patch.rs @@ -1,8 +1,8 @@ //! Patch and diff types for events. use crate::{ + Result, commit::{CommitHash, CommitProof}, events::{AccountEvent, DeviceEvent, EventRecord, WriteEvent}, - Result, }; use binary_stream::futures::{Decodable, Encodable}; use std::marker::PhantomData; diff --git a/crates/core/src/events/record.rs b/crates/core/src/events/record.rs index 371d526f47..d18cf677fc 100644 --- a/crates/core/src/events/record.rs +++ b/crates/core/src/events/record.rs @@ -1,6 +1,7 @@ use crate::{ + Result, UtcDateTime, commit::{CommitHash, CommitTree}, - decode, encode, Result, UtcDateTime, + decode, encode, }; use binary_stream::futures::{Decodable, Encodable}; diff --git a/crates/core/src/events/write.rs b/crates/core/src/events/write.rs index d97a691b61..8ee694bfdc 100644 --- a/crates/core/src/events/write.rs +++ b/crates/core/src/events/write.rs @@ -1,7 +1,7 @@ //! Folder write operations. use super::{EventKind, LogEvent}; use crate::SecretId; -use crate::{crypto::AeadPack, VaultCommit, VaultFlags}; +use crate::{VaultCommit, VaultFlags, crypto::AeadPack}; use serde::{Deserialize, Serialize}; /// Write operations. diff --git a/crates/core/src/file.rs b/crates/core/src/file.rs index bcae226664..02a826da7d 100644 --- a/crates/core/src/file.rs +++ b/crates/core/src/file.rs @@ -1,5 +1,5 @@ use crate::{ - commit::CommitHash, Error, Result, SecretId, SecretPath, VaultId, + Error, Result, SecretId, SecretPath, VaultId, commit::CommitHash, }; use serde::{Deserialize, Serialize}; use std::{fmt, str::FromStr}; @@ -91,12 +91,12 @@ impl ExternalFile { /// Vault identifier. pub fn vault_id(&self) -> &VaultId { - &self.0 .0 + &self.0.0 } /// Secret identifier. pub fn secret_id(&self) -> &SecretId { - &self.0 .1 + &self.0.1 } /// File name. diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index ebd5988d5a..bb5ff2ab24 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -1,7 +1,7 @@ +//! Core types and constants for the [Save Our Secrets](https://saveoursecrets.com) SDK. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -//! Core types and constants for the [Save Our Secrets](https://saveoursecrets.com) SDK. +#![cfg_attr(docsrs, feature(doc_cfg))] mod account; pub mod commit; @@ -17,6 +17,7 @@ pub mod file_identity; mod identity; mod origin; mod paths; +mod sharing; pub use account::AccountId; // pub use crypto::*; @@ -29,12 +30,13 @@ pub use identity::{AccountRef, PublicIdentity}; pub use origin::{Origin, RemoteOrigins}; pub use paths::Paths; pub use rs_merkle as merkle; +pub use sharing::{FolderInvite, InviteStatus, Recipient}; /// Result type for the library. pub(crate) type Result = std::result::Result; use bitflags::bitflags; -use rand::{rngs::OsRng, CryptoRng, Rng}; +use rand::{CryptoRng, Rng, rngs::OsRng}; use serde::{Deserialize, Serialize}; use std::{fmt, path::Path, str::FromStr}; use uuid::Uuid; diff --git a/crates/core/src/origin.rs b/crates/core/src/origin.rs index 8783236501..a54653a011 100644 --- a/crates/core/src/origin.rs +++ b/crates/core/src/origin.rs @@ -67,7 +67,7 @@ pub trait RemoteOrigins { /// Add a server origin to the backing storage. async fn add_server(&mut self, origin: Origin) - -> Result<(), Self::Error>; + -> Result<(), Self::Error>; /// Update a server origin in the backing storage. async fn replace_server( diff --git a/crates/core/src/paths.rs b/crates/core/src/paths.rs index 5633c61a89..4af787a3cd 100644 --- a/crates/core/src/paths.rs +++ b/crates/core/src/paths.rs @@ -1,17 +1,17 @@ //! File system paths for applications. use crate::{ + AccountId, ExternalFile, ExternalFileName, Result, SecretId, VaultId, constants::{ ACCOUNT_EVENTS, APP_AUTHOR, APP_NAME, AUDIT_FILE_NAME, BLOBS_DIR, - DATABASE_FILE, DEVICE_EVENTS, DEVICE_FILE, EVENT_LOG_EXT, FILES_DIR, - FILE_EVENTS, IDENTITY_DIR, JSON_EXT, LOCAL_DIR, LOGS_DIR, - PREFERENCES_FILE, REMOTES_FILE, REMOTE_DIR, SYSTEM_MESSAGES_FILE, - VAULTS_DIR, VAULT_EXT, + DATABASE_FILE, DEVICE_EVENTS, DEVICE_FILE, EVENT_LOG_EXT, + FILE_EVENTS, FILES_DIR, IDENTITY_DIR, JSON_EXT, LOCAL_DIR, LOGS_DIR, + PREFERENCES_FILE, REMOTE_DIR, REMOTES_FILE, SYSTEM_MESSAGES_FILE, + VAULT_EXT, VAULTS_DIR, }, - AccountId, ExternalFile, ExternalFileName, Result, SecretId, VaultId, }; #[cfg(not(target_arch = "wasm32"))] use etcetera::{ - app_strategy::choose_native_strategy, AppStrategy, AppStrategyArgs, + AppStrategy, AppStrategyArgs, app_strategy::choose_native_strategy, }; use serde::{Deserialize, Serialize}; use sos_vfs as vfs; diff --git a/crates/core/src/sharing.rs b/crates/core/src/sharing.rs new file mode 100644 index 0000000000..50d6d054be --- /dev/null +++ b/crates/core/src/sharing.rs @@ -0,0 +1,70 @@ +//! Types for public key infrastructure (PKI) and folder sharing. +use crate::{Error, Result, UtcDateTime, VaultId}; +use serde::{Deserialize, Serialize}; + +/// Recipient is a participant in a shared folder. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Recipient { + /// Recipient name. + pub name: String, + /// Optional email. + pub email: Option, + /// Public key. + pub public_key: age::x25519::Recipient, +} + +/// Status of a folder invite. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[repr(u8)] +pub enum InviteStatus { + /// Pending invite. + Pending = 0, + /// Accepted invite. + Accepted = 1, + /// Declined invite. + Declined = 2, +} + +// For database INTEGER type. +impl TryFrom for InviteStatus { + type Error = Error; + + fn try_from(value: i64) -> Result { + Ok(match value { + 0 => Self::Pending, + 1 => Self::Accepted, + 2 => Self::Declined, + _ => return Err(Error::UnknownInviteStatus(value)), + }) + } +} + +// For protobuf enum type. +impl TryFrom for InviteStatus { + type Error = Error; + + fn try_from(value: i32) -> Result { + (value as i64).try_into() + } +} + +/// Invite to share a folder. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FolderInvite { + /// Created date and time. + pub created_at: UtcDateTime, + /// Modified date and time. + pub modified_at: UtcDateTime, + /// Invite status. + pub invite_status: InviteStatus, + /// Folder identifier. + pub folder_id: VaultId, + /// Folder name. + pub folder_name: String, + /// Recipient name (from/to depending on context). + pub recipient_name: String, + /// Recipient email (from/to depending on context). + pub recipient_email: Option, + /// Recipient public key (from/to depending on context). + pub recipient_public_key: age::x25519::Recipient, +} diff --git a/crates/database/Cargo.toml b/crates/database/Cargo.toml index 3198dba8fd..fab2452e0c 100644 --- a/crates/database/Cargo.toml +++ b/crates/database/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-database" -version = "0.17.4" -edition = "2021" +version = "0.17.5" +edition = "2024" description = "Database backend for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -59,6 +59,3 @@ sql_query_builder = { workspace = true } # system-messages urn = { workspace = true, optional = true } - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/database/build.rs b/crates/database/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/database/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/database/sql_migrations/V3__shared_folders.sql b/crates/database/sql_migrations/V3__shared_folders.sql new file mode 100644 index 0000000000..0bc334c9ea --- /dev/null +++ b/crates/database/sql_migrations/V3__shared_folders.sql @@ -0,0 +1,134 @@ +-- Folders that may be shared between accounts. +-- +-- Unlike login and device folders which use a 1:1 +-- relationship via a join table this is a many to many +-- relationship. +-- +-- Each folder should be using asymmetric encryption +-- to provide access control; the use of an asymmetric cipher +-- should be enforced at the API level. +-- +-- The owner account_id can be determined by looking at the +-- account_id in the folders table. +CREATE TABLE IF NOT EXISTS shared_folders +( + -- Shared folder id. + shared_folder_id INTEGER PRIMARY KEY NOT NULL, + -- Account id. + account_id INTEGER NOT NULL, + -- Folder id. + folder_id INTEGER NOT NULL, + + FOREIGN KEY (account_id) + REFERENCES accounts (account_id) ON DELETE CASCADE, + FOREIGN KEY (folder_id) + REFERENCES folders (folder_id) ON DELETE CASCADE, + + UNIQUE (account_id, folder_id) +); + +-- Recipients with access to a shared folder. +CREATE TABLE IF NOT EXISTS shared_folder_recipients +( + -- Shared folder id. + shared_folder_id INTEGER NOT NULL, + -- Recipient id. + recipient_id INTEGER NOT NULL, + -- Whether this recipient created the shared folder. + is_creator INTEGER NOT NULL DEFAULT 0, + + FOREIGN KEY (shared_folder_id) + REFERENCES shared_folders (shared_folder_id) ON DELETE CASCADE, + FOREIGN KEY (recipient_id) + REFERENCES recipients (recipient_id) ON DELETE CASCADE, + + UNIQUE (shared_folder_id, recipient_id) +); + +-- Store folder shared access. +ALTER TABLE folders ADD COLUMN shared_access BLOB; + +-- Recipients are publicly accessible user-chosen +-- names mapped to public keys allowing discovery of +-- people's public keys for shared folder asymmetric encryption. +CREATE TABLE IF NOT EXISTS recipients +( + -- Recipient id. + recipient_id INTEGER PRIMARY KEY NOT NULL, + -- Account id. + account_id INTEGER NOT NULL, + -- Created date and time. + created_at DATETIME NOT NULL, + -- Last modiifed date and time. + modified_at DATETIME NOT NULL, + -- Recipient name. + -- + -- Should be a username or handle that would allow other + -- people to identify the recipient. + recipient_name TEXT NOT NULL, + -- Optional email address. + recipient_email TEXT, + -- Public key for the recipient. + recipient_public_key TEXT NOT NULL, + -- Indicate the key has been revoked. + revoked INTEGER NOT NULL DEFAULT 0, + + FOREIGN KEY (account_id) + REFERENCES accounts (account_id) ON DELETE CASCADE +); + +CREATE INDEX IF NOT EXISTS recipients_public_key_idx ON recipients(recipient_public_key); + +CREATE VIRTUAL TABLE recipients_fts USING fts5( + recipient_name, + recipient_email, + content='recipients', + content_rowid='recipient_id', + tokenize='trigram' +); + +CREATE TRIGGER recipients_ai AFTER INSERT ON recipients BEGIN + INSERT INTO recipients_fts(rowid, recipient_name, recipient_email) + VALUES (new.recipient_id, new.recipient_name, new.recipient_email); +END; + +CREATE TRIGGER recipients_ad AFTER DELETE ON recipients BEGIN + INSERT INTO recipients_fts(recipients_fts, rowid, recipient_name, recipient_email) + VALUES('delete', old.recipient_id, old.recipient_name, old.recipient_email); +END; + +CREATE TRIGGER recipients_au AFTER UPDATE ON recipients BEGIN + INSERT INTO recipients_fts(recipients_fts, rowid, recipient_name, recipient_email) + VALUES('delete', old.recipient_id, old.recipient_name, old.recipient_email); + + INSERT INTO recipients_fts(rowid, recipient_name, recipient_email) + VALUES (new.recipient_id, new.recipient_name, new.recipient_email); +END; + +-- Invite a recipient to join a shared folder. +CREATE TABLE IF NOT EXISTS folder_invites +( + -- Folder invite id. + folder_invite_id INTEGER PRIMARY KEY NOT NULL, + -- Created date and time. + created_at DATETIME NOT NULL, + -- Modified date and time. + modified_at DATETIME NOT NULL, + -- Recipient sending the invite. + from_recipient_id INTEGER NOT NULL, + -- Recipient receiving the invite. + to_recipient_id INTEGER NOT NULL, + -- Folder being shared. + folder_id INTEGER NOT NULL, + -- Status of the invite. + invite_status INTEGER NOT NULL DEFAULT 0, + + FOREIGN KEY (from_recipient_id) + REFERENCES recipients (recipient_id) ON DELETE CASCADE, + FOREIGN KEY (to_recipient_id) + REFERENCES recipients (recipient_id) ON DELETE CASCADE, + FOREIGN KEY (folder_id) + REFERENCES folders (folder_id) ON DELETE CASCADE, + + UNIQUE (from_recipient_id, to_recipient_id, folder_id) +); diff --git a/crates/database/src/archive/error.rs b/crates/database/src/archive/error.rs index 5318b41c12..46295caf62 100644 --- a/crates/database/src/archive/error.rs +++ b/crates/database/src/archive/error.rs @@ -1,4 +1,4 @@ -use sos_core::{commit::CommitHash, AccountId}; +use sos_core::{AccountId, commit::CommitHash}; use std::path::PathBuf; use thiserror::Error; @@ -37,7 +37,9 @@ pub enum Error { /// Error generated when the checksum for a database does not /// match the manifest. - #[error("database checksum is invalid, expected '{0}' but computed '{1}' (archive may be corrupt?)")] + #[error( + "database checksum is invalid, expected '{0}' but computed '{1}' (archive may be corrupt?)" + )] DatabaseChecksum(CommitHash, CommitHash), /// Error generated converting to fixed size slice. diff --git a/crates/database/src/archive/export.rs b/crates/database/src/archive/export.rs index 0ee3417d96..3560d20b66 100644 --- a/crates/database/src/archive/export.rs +++ b/crates/database/src/archive/export.rs @@ -1,12 +1,12 @@ -use super::{types::ManifestVersion3, Error, Result}; +use super::{Error, Result, types::ManifestVersion3}; use crate::entity::{AccountEntity, AccountRecord, AccountRow}; -use async_sqlite::rusqlite::{backup, Connection}; +use async_sqlite::rusqlite::{Connection, backup}; use sha2::{Digest, Sha256}; -use sos_archive::{ZipWriter, ARCHIVE_MANIFEST}; +use sos_archive::{ARCHIVE_MANIFEST, ZipWriter}; use sos_core::{ + Paths, commit::CommitHash, constants::{BLOBS_DIR, DATABASE_FILE}, - Paths, }; use sos_external_files::list_external_files; use sos_vfs as vfs; diff --git a/crates/database/src/archive/import.rs b/crates/database/src/archive/import.rs index b9a640ee6d..3f77bc46f0 100644 --- a/crates/database/src/archive/import.rs +++ b/crates/database/src/archive/import.rs @@ -1,18 +1,17 @@ -use super::{types::ManifestVersion3, Error, Result}; +use super::{Error, Result, types::ManifestVersion3}; use crate::entity::{ AccountEntity, AccountRecord, AccountRow, EventEntity, EventRecordRow, - FolderEntity, FolderRow, PreferenceEntity, PreferenceRow, SecretRow, - ServerEntity, ServerRow, SystemMessageEntity, SystemMessageRow, + FolderEntity, FolderRow, SecretRow, ServerEntity, ServerRow, }; use async_sqlite::rusqlite::Connection; use sha2::{Digest, Sha256}; -use sos_archive::{sanitize_file_path, ZipReader}; +use sos_archive::{ZipReader, sanitize_file_path}; use sos_core::{ + AccountId, ExternalFile, ExternalFileName, Paths, SecretId, SecretPath, + VaultId, commit::CommitHash, constants::{BLOBS_DIR, DATABASE_FILE}, events::EventLogType, - AccountId, ExternalFile, ExternalFileName, Paths, SecretId, SecretPath, - VaultId, }; use sos_vfs as vfs; use std::{ @@ -23,6 +22,12 @@ use std::{ use tempfile::NamedTempFile; use tokio::io::BufReader; +#[cfg(feature = "preferences")] +use crate::entity::{PreferenceEntity, PreferenceRow}; + +#[cfg(feature = "system-messages")] +use crate::entity::{SystemMessageEntity, SystemMessageRow}; + struct HashingWriter { inner: W, hasher: H, @@ -48,7 +53,9 @@ struct ImportDataSource { user_folders: Vec<(FolderRow, Vec, Vec)>, file_events: Vec, servers: Vec, + #[cfg(feature = "preferences")] account_preferences: Vec, + #[cfg(feature = "system-messages")] system_messages: Vec, } @@ -183,7 +190,11 @@ impl BackupImport { let folder_entity = FolderEntity::new(&self.source_db); let event_entity = EventEntity::new(&self.source_db); let server_entity = ServerEntity::new(&self.source_db); + + #[cfg(feature = "preferences")] let preference_entity = PreferenceEntity::new(&self.source_db); + + #[cfg(feature = "system-messages")] let system_messages_entity = SystemMessageEntity::new(&self.source_db); @@ -242,8 +253,10 @@ impl BackupImport { // Servers, preferences and system messages let servers = server_entity.load_servers(account_id)?; + #[cfg(feature = "preferences")] let account_preferences = preference_entity.load_preferences(Some(account_id))?; + #[cfg(feature = "system-messages")] let system_messages = system_messages_entity.load_system_messages(account_id)?; @@ -256,7 +269,9 @@ impl BackupImport { user_folders, file_events, servers, + #[cfg(feature = "preferences")] account_preferences, + #[cfg(feature = "system-messages")] system_messages, }; @@ -274,7 +289,9 @@ impl BackupImport { let folder_entity = FolderEntity::new(&tx); let event_entity = EventEntity::new(&tx); let server_entity = ServerEntity::new(&tx); + #[cfg(feature = "preferences")] let preference_entity = PreferenceEntity::new(&tx); + #[cfg(feature = "system-messages")] let system_messages_entity = SystemMessageEntity::new(&tx); // Insert the account @@ -320,10 +337,13 @@ impl BackupImport { // Servers, preferences and system messages server_entity.insert_servers(account_id, &data.servers)?; + + #[cfg(feature = "preferences")] preference_entity.insert_preferences( Some(account_id), &data.account_preferences, )?; + #[cfg(feature = "preferences")] system_messages_entity .insert_system_messages(account_id, &data.system_messages)?; @@ -427,27 +447,21 @@ fn find_blobs( Some(fourth), Some(fifth), ) = (it.next(), it.next(), it.next(), it.next(), it.next()) + && first == BLOBS_DIR + && let Ok(account_id) = + second.to_string_lossy().parse::() { - if first == BLOBS_DIR { - if let Ok(account_id) = - second.to_string_lossy().parse::() - { - let files = - out.entry(account_id).or_insert(Vec::new()); - - if let (Ok(folder_id), Ok(secret_id), Ok(file_name)) = ( - third.to_string_lossy().parse::(), - fourth.to_string_lossy().parse::(), - fifth - .to_string_lossy() - .parse::(), - ) { - files.push(ExternalFile::new( - SecretPath(folder_id, secret_id), - file_name, - )); - } - } + let files = out.entry(account_id).or_insert(Vec::new()); + + if let (Ok(folder_id), Ok(secret_id), Ok(file_name)) = ( + third.to_string_lossy().parse::(), + fourth.to_string_lossy().parse::(), + fifth.to_string_lossy().parse::(), + ) { + files.push(ExternalFile::new( + SecretPath(folder_id, secret_id), + file_name, + )); } } } diff --git a/crates/database/src/archive/types.rs b/crates/database/src/archive/types.rs index 0f375dabfe..1302c4239b 100644 --- a/crates/database/src/archive/types.rs +++ b/crates/database/src/archive/types.rs @@ -1,5 +1,5 @@ use serde::{Deserialize, Serialize}; -use sos_core::{commit::CommitHash, ArchiveManifestVersion}; +use sos_core::{ArchiveManifestVersion, commit::CommitHash}; /// Version 3 manifest. #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/database/src/audit_provider.rs b/crates/database/src/audit_provider.rs index 677535f31e..2d294a9999 100644 --- a/crates/database/src/audit_provider.rs +++ b/crates/database/src/audit_provider.rs @@ -1,7 +1,7 @@ //! Database audit log provider. use crate::{ - entity::{AuditEntity, AuditRecord, AuditRow}, Error, + entity::{AuditEntity, AuditRecord, AuditRow}, }; use async_sqlite::Client; use async_trait::async_trait; diff --git a/crates/database/src/entity/account.rs b/crates/database/src/entity/account.rs index f238cb4db5..cb04724e12 100644 --- a/crates/database/src/entity/account.rs +++ b/crates/database/src/entity/account.rs @@ -1,12 +1,12 @@ use crate::{ - entity::{FolderEntity, FolderRecord, SecretRow}, Error, Result, + entity::{FolderEntity, FolderRecord, SecretRow}, }; use async_sqlite::{ + Client, rusqlite::{ Connection, Error as SqlError, OptionalExtension, Row, Transaction, }, - Client, }; use sos_core::{AccountId, PublicIdentity, UtcDateTime, VaultCommit}; use sos_vault::Vault; @@ -22,13 +22,13 @@ pub struct AccountRow { /// Row identifier. pub row_id: i64, /// RFC3339 date and time. - created_at: String, + pub(crate) created_at: String, /// RFC3339 date and time. - modified_at: String, + pub(crate) modified_at: String, /// Account identifier. - identifier: String, + pub(crate) identifier: String, /// Account name. - name: String, + pub(crate) name: String, } impl AccountRow { @@ -231,7 +231,8 @@ impl<'conn> AccountEntity<'conn, Transaction<'conn>> { let folder_entity = FolderEntity::new(&tx); // Delete the old folder - folder_entity.delete_folder(&login_folder_id)?; + folder_entity + .delete_folder(account.row_id, &login_folder_id)?; // Create the new folder let folder_row_id = folder_entity diff --git a/crates/database/src/entity/audit.rs b/crates/database/src/entity/audit.rs index 31cf589800..b2dca29911 100644 --- a/crates/database/src/entity/audit.rs +++ b/crates/database/src/entity/audit.rs @@ -1,7 +1,7 @@ use crate::Error; use async_sqlite::rusqlite::{Connection, Error as SqlError, Row}; use sos_audit::AuditEvent; -use sos_core::{events::EventKind, AccountId, UtcDateTime}; +use sos_core::{AccountId, UtcDateTime, events::EventKind}; use sql_query_builder as sql; use std::ops::Deref; diff --git a/crates/database/src/entity/event.rs b/crates/database/src/entity/event.rs index d9c4a4fb3a..aa4954a542 100644 --- a/crates/database/src/entity/event.rs +++ b/crates/database/src/entity/event.rs @@ -3,9 +3,9 @@ use async_sqlite::rusqlite::{ CachedStatement, Connection, Error as SqlError, Row, }; use sos_core::{ + UtcDateTime, commit::CommitHash, events::{EventLogType, EventRecord}, - UtcDateTime, }; use sql_query_builder as sql; use std::ops::Deref; diff --git a/crates/database/src/entity/folder.rs b/crates/database/src/entity/folder.rs index 07dabdcc61..a43d9912f1 100644 --- a/crates/database/src/entity/folder.rs +++ b/crates/database/src/entity/folder.rs @@ -1,15 +1,15 @@ use crate::{Error, Result}; +use async_sqlite::Client; use async_sqlite::rusqlite::{ CachedStatement, Connection, Error as SqlError, OptionalExtension, Row, Transaction, }; -use async_sqlite::Client; use sos_core::crypto::Seed; use sos_core::{ - commit::CommitHash, crypto::AeadPack, decode, encode, SecretId, - UtcDateTime, VaultCommit, VaultEntry, VaultFlags, VaultId, + InviteStatus, SecretId, UtcDateTime, VaultCommit, VaultEntry, VaultFlags, + VaultId, commit::CommitHash, crypto::AeadPack, decode, encode, }; -use sos_vault::{Summary, Vault}; +use sos_vault::{SharedAccess, Summary, Vault}; use sql_query_builder as sql; use std::collections::HashMap; use std::ops::Deref; @@ -29,7 +29,8 @@ fn folder_select_columns(sql: sql::Select) -> sql::Select { folders.version, folders.cipher, folders.kdf, - folders.flags + folders.flags, + folders.shared_access "#, ) } @@ -53,17 +54,18 @@ fn secret_select_columns(sql: sql::Select) -> sql::Select { #[derive(Debug, Default)] pub struct FolderRow { pub row_id: i64, - created_at: String, - modified_at: String, - identifier: String, - name: String, - salt: Option, - meta: Option>, - seed: Option>, - version: i64, - cipher: String, - kdf: String, - flags: Vec, + pub(crate) created_at: String, + pub(crate) modified_at: String, + pub(crate) identifier: String, + pub(crate) name: String, + pub(crate) salt: Option, + pub(crate) meta: Option>, + pub(crate) seed: Option>, + pub(crate) version: i64, + pub(crate) cipher: String, + pub(crate) kdf: String, + pub(crate) flags: Vec, + pub(crate) shared_access: Option>, } impl FolderRow { @@ -76,7 +78,7 @@ impl FolderRow { }; let salt = vault.salt().cloned(); let seed = vault.seed().map(|s| s.as_ref().to_vec()); - Self::new_insert_parts(vault.summary(), salt, meta, seed) + Self::new_insert_parts(vault.summary(), salt, meta, seed, None) } /// Create a new folder row to be inserted from parts. @@ -85,6 +87,7 @@ impl FolderRow { salt: Option, meta: Option>, seed: Option>, + shared_access: Option>, ) -> Result { Ok(Self { created_at: UtcDateTime::default().to_rfc3339()?, @@ -98,10 +101,36 @@ impl FolderRow { cipher: summary.cipher().to_string(), kdf: summary.kdf().to_string(), flags: summary.flags().bits().to_le_bytes().to_vec(), + shared_access, ..Default::default() }) } + /// Create a folder row from a vault. + pub async fn new_insert_from_vault(vault: &Vault) -> Result { + let meta = if let Some(meta) = vault.header().meta() { + Some(encode(meta).await?) + } else { + None + }; + let salt = vault.salt().cloned(); + let seed = vault.seed().map(|s| s.as_ref().to_vec()); + + let shared_access = if !vault.shared_access().is_empty() { + Some(encode(vault.shared_access()).await?) + } else { + None + }; + + FolderRow::new_insert_parts( + vault.summary(), + salt, + meta, + seed, + shared_access, + ) + } + /// Create a new folder row to update. pub async fn new_update(vault: &Vault) -> Result { let summary = vault.summary(); @@ -144,6 +173,7 @@ impl<'a> TryFrom<&Row<'a>> for FolderRow { cipher: row.get(9)?, kdf: row.get(10)?, flags: row.get(11)?, + shared_access: row.get(12)?, }) } } @@ -165,6 +195,8 @@ pub struct FolderRecord { pub seed: Option, /// Folder summary. pub summary: Summary, + /// Shared access permissions. + pub shared_access: Option, } impl FolderRecord { @@ -199,6 +231,13 @@ impl FolderRecord { let summary = Summary::new(version, folder_id, value.name, cipher, kdf, flags); + let shared_access = if let Some(shared_access) = &value.shared_access + { + Some(decode(shared_access).await?) + } else { + None + }; + Ok(FolderRecord { row_id: value.row_id, created_at, @@ -207,6 +246,7 @@ impl FolderRecord { meta, seed, summary, + shared_access, }) } @@ -216,6 +256,9 @@ impl FolderRecord { vault.header_mut().set_meta(self.meta.clone()); vault.header_mut().set_salt(self.salt.clone()); vault.header_mut().set_seed(self.seed); + if let Some(shared_access) = &self.shared_access { + vault.header_mut().set_shared_access(shared_access.clone()); + } Ok(vault) } } @@ -378,16 +421,7 @@ impl<'conn> FolderEntity<'conn, Transaction<'conn>> { ) -> Result<(i64, HashMap)> { let folder_id = *vault.id(); - let meta = if let Some(meta) = vault.header().meta() { - Some(encode(meta).await?) - } else { - None - }; - let salt = vault.salt().cloned(); - let seed = vault.seed().map(|s| s.as_ref().to_vec()); - - let folder_row = - FolderRow::new_insert_parts(vault.summary(), salt, meta, seed)?; + let folder_row = FolderRow::new_insert_from_vault(vault).await?; let mut secret_rows = Vec::new(); for (secret_id, commit) in vault.iter() { @@ -570,7 +604,19 @@ where .left_join( "account_device_folder device ON folders.folder_id = device.folder_id", ) - .where_clause("folders.account_id=?1") + .left_join( + "shared_folders shared ON folders.folder_id = shared.folder_id", + ) + .left_join( + "recipients r ON shared.account_id = r.account_id", + ) + .left_join( + "shared_folder_recipients sfr ON shared.shared_folder_id = sfr.shared_folder_id AND sfr.recipient_id = r.recipient_id", + ) + .left_join( + "folder_invites fi ON fi.to_recipient_id = r.recipient_id AND fi.folder_id = folders.folder_id", + ) + .where_clause("(folders.account_id = ?1 OR (shared.account_id = ?1 AND (sfr.is_creator = 1 OR fi.invite_status = ?2)))") .where_and("login.folder_id IS NULL") .where_and("device.folder_id IS NULL"); @@ -580,7 +626,10 @@ where Ok(row.try_into()?) } - let rows = stmt.query_and_then([account_id], convert_row)?; + let rows = stmt.query_and_then( + (account_id, InviteStatus::Accepted as u8), + convert_row, + )?; let mut folders = Vec::new(); for row in rows { folders.push(row?); @@ -655,11 +704,14 @@ where version, cipher, kdf, - flags + flags, + shared_access ) "#, ) - .values("(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)"); + .values( + "(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)", + ); let mut stmt = self.conn.prepare_cached(&query.as_string())?; stmt.execute(( @@ -675,6 +727,7 @@ where &folder_row.cipher, &folder_row.kdf, &folder_row.flags, + &folder_row.shared_access, ))?; Ok(self.conn.last_insert_rowid()) @@ -699,10 +752,11 @@ where version = ?7, cipher = ?8, kdf = ?9, - flags = ?10 + flags = ?10, + shared_access = ?11 "#, ) - .where_clause("identifier=?11"); + .where_clause("identifier=?12"); let mut stmt = self.conn.prepare_cached(&query.as_string())?; stmt.execute(( &folder_row.modified_at, @@ -715,6 +769,7 @@ where &folder_row.cipher, &folder_row.kdf, &folder_row.flags, + &folder_row.shared_access, folder_id.to_string(), ))?; @@ -883,14 +938,16 @@ where /// Delete a folder. pub fn delete_folder( &self, + account_id: i64, folder_id: &VaultId, ) -> StdResult { let row = self.find_one(folder_id)?; let query = sql::Delete::new() .delete_from("folders") - .where_clause("folder_id = ?1"); + .where_clause("account_id = ?1") + .where_and("folder_id = ?2"); let mut stmt = self.conn.prepare_cached(&query.as_string())?; - let affected_rows = stmt.execute([row.row_id])?; + let affected_rows = stmt.execute([account_id, row.row_id])?; Ok(affected_rows > 0) } diff --git a/crates/database/src/entity/mod.rs b/crates/database/src/entity/mod.rs index 2e028d75f9..cc2f507054 100644 --- a/crates/database/src/entity/mod.rs +++ b/crates/database/src/entity/mod.rs @@ -9,6 +9,7 @@ mod folder; #[cfg(feature = "preferences")] mod preference; mod server; +mod shared_folder; #[cfg(feature = "system-messages")] mod system_message; @@ -24,5 +25,9 @@ pub use folder::{ #[cfg(feature = "preferences")] pub use preference::{PreferenceEntity, PreferenceRow}; pub use server::{ServerEntity, ServerRow}; +pub use shared_folder::{ + DeleteSharedFolderOutcome, RecipientEntity, RecipientRecord, + SharedFolderEntity, +}; #[cfg(feature = "system-messages")] pub use system_message::{SystemMessageEntity, SystemMessageRow}; diff --git a/crates/database/src/entity/shared_folder/folder_invites.rs b/crates/database/src/entity/shared_folder/folder_invites.rs new file mode 100644 index 0000000000..59ba0562fe --- /dev/null +++ b/crates/database/src/entity/shared_folder/folder_invites.rs @@ -0,0 +1,112 @@ +use crate::{Error, Result}; +use async_sqlite::rusqlite::{Error as SqlError, Row}; +use sos_core::{FolderInvite, InviteStatus, UtcDateTime, VaultId}; +use std::result::Result as StdResult; + +/// Represents an invite to a shared folder. +pub(super) struct FolderInviteRow { + folder_invite_id: i64, + created_at: String, + modified_at: String, + from_recipient_id: i64, + to_recipient_id: i64, + folder_row_id: i64, + invite_status: i64, + folder_identifier: String, + folder_name: String, + recipient_name: String, + recipient_email: Option, + recipient_public_key: String, +} + +impl<'a> TryFrom<&Row<'a>> for FolderInviteRow { + type Error = SqlError; + fn try_from(row: &Row<'a>) -> StdResult { + Ok(FolderInviteRow { + folder_invite_id: row.get(0)?, + created_at: row.get(1)?, + modified_at: row.get(2)?, + from_recipient_id: row.get(3)?, + to_recipient_id: row.get(4)?, + folder_row_id: row.get(5)?, + invite_status: row.get(6)?, + folder_identifier: row.get(7)?, + folder_name: row.get(8)?, + recipient_name: row.get(9)?, + recipient_email: row.get(10)?, + recipient_public_key: row.get(11)?, + }) + } +} + +#[derive(Debug)] +pub struct FolderInviteRecord { + /// Row identifier. + pub row_id: i64, + /// Created date and time. + pub created_at: UtcDateTime, + /// Modified date and time. + pub modified_at: UtcDateTime, + /// From recipient id. + #[allow(dead_code)] + pub(super) from_recipient_id: i64, + /// To recipient id. + #[allow(dead_code)] + pub(super) to_recipient_id: i64, + /// Folder row id. + #[allow(dead_code)] + pub(super) folder_row_id: i64, + /// Invite status. + pub invite_status: InviteStatus, + /// Folder identifier. + pub folder_id: VaultId, + /// Folder name. + pub folder_name: String, + /// Recipient name (from/to depending on context). + pub recipient_name: String, + /// Recipient email (from/to depending on context). + pub recipient_email: Option, + /// Recipient public key (from/to depending on context). + pub recipient_public_key: String, +} + +impl TryFrom for FolderInviteRecord { + type Error = Error; + + fn try_from(value: FolderInviteRow) -> Result { + Ok(Self { + row_id: value.folder_invite_id, + created_at: UtcDateTime::parse_rfc3339(&value.created_at)?, + modified_at: UtcDateTime::parse_rfc3339(&value.modified_at)?, + from_recipient_id: value.from_recipient_id, + to_recipient_id: value.to_recipient_id, + folder_row_id: value.folder_row_id, + invite_status: value.invite_status.try_into()?, + folder_id: value.folder_identifier.parse()?, + folder_name: value.folder_name, + recipient_name: value.recipient_name, + recipient_email: value.recipient_email, + recipient_public_key: value.recipient_public_key, + }) + } +} + +impl TryFrom for FolderInvite { + type Error = Error; + + fn try_from(value: FolderInviteRecord) -> Result { + Ok(Self { + created_at: value.created_at, + modified_at: value.modified_at, + invite_status: value.invite_status, + folder_id: value.folder_id, + folder_name: value.folder_name, + recipient_name: value.recipient_name, + recipient_email: value.recipient_email, + recipient_public_key: value + .recipient_public_key + .parse() + .map_err(Error::AgeX25519Parse)?, + }) + } +} diff --git a/crates/database/src/entity/shared_folder/mod.rs b/crates/database/src/entity/shared_folder/mod.rs new file mode 100644 index 0000000000..0dad07aafd --- /dev/null +++ b/crates/database/src/entity/shared_folder/mod.rs @@ -0,0 +1,772 @@ +use crate::entity::{ + AccountEntity, AccountRecord, AccountRow, FolderEntity, FolderRecord, + FolderRow, +}; +use crate::{Result, SharingError}; +use async_sqlite::Client; +use async_sqlite::rusqlite::{Connection, OptionalExtension, Row}; +use sos_core::{AccountId, InviteStatus, Recipient, UtcDateTime, VaultId}; +use sos_vault::Vault; +use sql_query_builder as sql; + +mod folder_invites; +mod recipient; + +pub use folder_invites::FolderInviteRecord; +use recipient::RecipientRow; +pub use recipient::{RecipientEntity, RecipientRecord}; + +/// Information about a shared folder deletion. +#[derive(Debug)] +pub struct DeleteSharedFolderOutcome { + /// Whether the account that performed + /// the deletion was the folder creator. + pub is_creator: bool, + /// Public key of the account that + /// requested the deletion. + pub caller_public_key: String, + /// All shared folder participant account identifiers and public keys. + pub participants: Vec<(AccountId, String)>, +} + +/// Record for a shared folder. +#[derive(Debug)] +pub struct SharedFolderRecord { + /// Account information. + pub account: AccountRecord, + /// Folder information. + pub folder: FolderRecord, +} + +/// Shared folder entity. +pub struct SharedFolderEntity<'conn> { + conn: &'conn mut Connection, +} + +impl<'conn> SharedFolderEntity<'conn> { + /// Create a new shared folder entity. + pub fn new(conn: &'conn mut Connection) -> Self { + Self { conn } + } + + /// Create or update recipient information for an account. + pub fn upsert_recipient( + &mut self, + account_id: AccountId, + recipient: Recipient, + ) -> Result { + let recipient_name = recipient.name; + let recipient_email = recipient.email; + let recipient_public_key = recipient.public_key.to_string(); + + let tx = self.conn.transaction()?; + + let account = AccountEntity::new(&tx); + let account_row = account.find_one(&account_id)?; + + let recipient_entity = RecipientEntity::new(&tx); + let recipient_id = if let Some(recipient_row) = + recipient_entity.find_optional(account_row.row_id)? + { + let recipient_row = recipient_row.new_update( + recipient_name, + recipient_email, + recipient_public_key, + )?; + recipient_entity.update_recipient(&recipient_row)?; + recipient_row.recipient_id + } else { + let recipient_row = RecipientRow::new_insert( + account_row.row_id, + recipient_name, + recipient_email, + recipient_public_key, + )?; + recipient_entity.insert_recipient(&recipient_row)? + }; + tx.commit()?; + Ok(recipient_id) + } + + /// Try to find recipient information for an account. + pub fn find_recipient( + &mut self, + account_id: AccountId, + ) -> Result> { + let account = AccountEntity::new(&self.conn); + if let Some(account_row) = account.find_optional(&account_id)? { + let recipient_entity = RecipientEntity::new(&self.conn); + if let Some(recipient) = + recipient_entity.find_optional(account_row.row_id)? + { + Ok(Some(recipient.try_into()?)) + } else { + Ok(None) + } + } else { + Ok(None) + } + } + + /// Invite a recipient to a folder. + pub fn invite_recipient( + &mut self, + account_id: &AccountId, + recipient_public_key: &str, + folder_id: &VaultId, + ) -> Result { + let tx = self.conn.transaction()?; + + let account = AccountEntity::new(&tx); + let account = account + .find_optional(account_id)? + .ok_or(SharingError::InviteNoAccount(*account_id))?; + + let recipient = RecipientEntity::new(&tx); + let from_recipient = recipient + .find_optional(account.row_id)? + .ok_or(SharingError::RecipientNotCreated(*account_id))?; + let to_recipient = recipient + .find_by_public_key(recipient_public_key)? + .ok_or(SharingError::InviteNoRecipient( + recipient_public_key.to_owned(), + ))?; + + let folder = FolderEntity::new(&tx); + let folder = folder + .find_optional(folder_id)? + .ok_or(SharingError::InviteNoFolder(*folder_id))?; + + let query = sql::Insert::new() + .insert_into( + r#" + folder_invites + ( + created_at, + modified_at, + from_recipient_id, + to_recipient_id, + folder_id, + invite_status + ) + "#, + ) + .values("(?1, ?2, ?3, ?4, ?5, ?6)"); + + let params = ( + UtcDateTime::default().to_rfc3339()?, + UtcDateTime::default().to_rfc3339()?, + from_recipient.recipient_id, + to_recipient.recipient_id, + folder.row_id, + 0, + ); + + let row_id = { + let mut stmt = tx.prepare_cached(&query.as_string())?; + stmt.execute(params)?; + tx.last_insert_rowid() + }; + + tx.commit()?; + Ok(row_id) + } + + /// Folder invites received by this account. + pub fn received_folder_invites( + &mut self, + account_id: &AccountId, + invite_status: Option, + limit: Option, + ) -> Result> { + self.list_folder_invites(account_id, false, invite_status, limit) + } + + /// Folder invites sent from this account. + pub fn sent_folder_invites( + &mut self, + account_id: &AccountId, + invite_status: Option, + limit: Option, + ) -> Result> { + self.list_folder_invites(account_id, true, invite_status, limit) + } + + /// Folder invites sent from this account. + fn list_folder_invites( + &mut self, + account_id: &AccountId, + sent: bool, + invite_status: Option, + limit: Option, + ) -> Result> { + let limit = + limit.map(|l| l.to_string()).unwrap_or(String::from("10")); + + let mut query = sql::Select::new() + .select( + r#" + fi.folder_invite_id, + fi.created_at, + fi.modified_at, + fi.from_recipient_id, + fi.to_recipient_id, + fi.folder_id, + fi.invite_status, + f.identifier, + f.name, + r.recipient_name, + r.recipient_email, + r.recipient_public_key + "#, + ) + .from("folder_invites AS fi"); + + if sent { + // For sent invites, join to_recipient to get their info + query = query + .inner_join( + "recipients AS r ON fi.to_recipient_id = + r.recipient_id", + ) + .inner_join( + "recipients AS sender ON fi.from_recipient_id = + sender.recipient_id", + ) + .inner_join( + "accounts AS a ON sender.account_id = a.account_id", + ); + } else { + // For received invites, join from_recipient to get their info + query = query + .inner_join( + "recipients AS r ON fi.from_recipient_id = + r.recipient_id", + ) + .inner_join( + "recipients AS receiver ON fi.to_recipient_id = + receiver.recipient_id", + ) + .inner_join( + "accounts AS a ON receiver.account_id = a.account_id", + ); + } + + query = query + .inner_join("folders AS f ON fi.folder_id = f.folder_id") + .where_clause("a.identifier = ?1"); + + if invite_status.is_some() { + query = query.where_and("fi.invite_status = ?2") + } + + query = query.limit(&limit).order_by("fi.modified_at DESC"); + + let mut stmt = self.conn.prepare_cached(&query.as_string())?; + + fn convert_row( + row: &Row<'_>, + ) -> Result { + Ok(row.try_into()?) + } + + let rows = match invite_status { + Some(invite_status) => stmt.query_and_then( + (account_id.to_string(), invite_status as u8), + convert_row, + )?, + None => { + stmt.query_and_then([account_id.to_string()], convert_row)? + } + }; + + let mut invites = Vec::new(); + for row in rows { + invites.push(row?.try_into()?); + } + Ok(invites) + } + + /// Update folder invite with a new status. + pub fn update_folder_invite( + &mut self, + account_id: &AccountId, + invite_status: InviteStatus, + from_recipient_public_key: &str, + folder_id: &VaultId, + ) -> Result<()> { + if !matches!( + invite_status, + InviteStatus::Accepted | InviteStatus::Declined + ) { + return Err(SharingError::PendingInviteStatusNotAllowed.into()); + } + + let tx = self.conn.transaction()?; + + let subquery = sql::Select::new() + .select("fi.folder_invite_id") + .from("folder_invites AS fi") + .inner_join( + "recipients AS from_r ON fi.from_recipient_id = from_r.recipient_id", + ) + .inner_join( + "recipients AS to_r ON fi.to_recipient_id = to_r.recipient_id", + ) + .inner_join("accounts AS a ON to_r.account_id = a.account_id") + .inner_join("folders AS f ON fi.folder_id = f.folder_id") + .where_clause("from_r.recipient_public_key = ?3") + .where_and("a.identifier = ?4") + .where_and("f.identifier = ?5"); + + let query = sql::Update::new() + .update("folder_invites") + .set( + r#" + modified_at = ?1, + invite_status = ?2 + "#, + ) + .where_clause(&format!( + "folder_invite_id IN ({})", + subquery.as_string() + )); + + { + let mut stmt = tx.prepare_cached(&query.as_string())?; + stmt.execute(( + UtcDateTime::default().to_rfc3339()?, + invite_status as u8, + from_recipient_public_key, + account_id.to_string(), + folder_id.to_string(), + ))?; + } + + tx.commit()?; + Ok(()) + } + + #[doc(hidden)] + pub fn list_shared_folders( + &self, + account_id: &AccountId, + ) -> Result> { + let query = sql::Select::new() + .select( + r#" + a.account_id, + a.created_at, + a.modified_at, + a.identifier, + a.name, + f.folder_id, + f.created_at, + f.modified_at, + f.identifier, + f.name, + f.salt, + f.meta, + f.seed, + f.version, + f.cipher, + f.kdf, + f.flags, + f.shared_access + "#, + ) + .from("shared_folders AS asf") + .inner_join("accounts AS a ON asf.account_id = a.account_id") + .inner_join("folders AS f ON asf.folder_id = f.folder_id") + .inner_join("recipients AS r ON a.account_id = r.account_id") + .inner_join( + "shared_folder_recipients AS sfr ON asf.shared_folder_id = sfr.shared_folder_id AND sfr.recipient_id = r.recipient_id", + ) + .left_join( + "folder_invites AS fi ON fi.to_recipient_id = r.recipient_id AND fi.folder_id = f.folder_id", + ) + .where_clause("a.identifier = ?1 AND (sfr.is_creator = 1 OR fi.invite_status = ?2)"); + + let mut stmt = self.conn.prepare_cached(&query.as_string())?; + + fn convert_row(row: &Row<'_>) -> Result<(AccountRow, FolderRow)> { + let account = AccountRow { + row_id: row.get(0)?, + created_at: row.get(1)?, + modified_at: row.get(2)?, + identifier: row.get(3)?, + name: row.get(4)?, + }; + let folder = FolderRow { + row_id: row.get(5)?, + created_at: row.get(6)?, + modified_at: row.get(7)?, + identifier: row.get(8)?, + name: row.get(9)?, + salt: row.get(10)?, + meta: row.get(11)?, + seed: row.get(12)?, + version: row.get(13)?, + cipher: row.get(14)?, + kdf: row.get(15)?, + flags: row.get(16)?, + shared_access: row.get(17)?, + }; + Ok((account, folder)) + } + + let rows = stmt.query_and_then( + (account_id.to_string(), InviteStatus::Accepted as u8), + convert_row, + )?; + + let mut shared_folders = Vec::new(); + for row in rows { + shared_folders.push(row?); + } + Ok(shared_folders) + } + + #[doc(hidden)] + /// Convert shared folders rows. + pub async fn from_rows( + rows: Vec<(AccountRow, FolderRow)>, + ) -> Result> { + // TODO: if FolderRecord::from_row() was sync we could use + // TODO: a much cleaner API; this would require + // TODO: using sync versions of encode() and decode() + // TODO: which is a big refactor so deferred for another time + + let mut shared_folders = Vec::new(); + for (account_row, folder_row) in rows { + let account: AccountRecord = account_row.try_into()?; + let folder = FolderRecord::from_row(folder_row).await?; + shared_folders.push(SharedFolderRecord { account, folder }); + } + Ok(shared_folders) + } + + /// Create a shared folder. + pub async fn create_shared_folder( + client: &Client, + account_id: &AccountId, + vault: &Vault, + recipients: &[Recipient], + ) -> Result<()> { + // Validate owner account exists + let account_check_id = *account_id; + let account_row = client + .conn_and_then(move |conn| { + let account = AccountEntity::new(&conn); + account + .find_one(&account_check_id) + .map_err(async_sqlite::Error::Rusqlite) + }) + .await?; + + // Validate owner has recipient record + let owner_recipient = client + .conn_and_then(move |conn| { + let recipient_entity = RecipientEntity::new(&conn); + recipient_entity + .find_optional(account_row.row_id) + .map_err(async_sqlite::Error::Rusqlite) + }) + .await? + .ok_or(SharingError::RecipientNotCreated(*account_id))?; + + // Find owner in recipients slice + if !recipients.iter().any(|r| { + r.public_key.to_string() == owner_recipient.recipient_public_key + }) { + return Err( + SharingError::OwnerNotInRecipients(*account_id).into() + ); + } + + let owner_public_key_str = + owner_recipient.recipient_public_key.clone(); + let owner_recipient_id = owner_recipient.recipient_id; + + // Collect and validate all recipient records + let recipient_keys = recipients + .iter() + .map(|r| r.public_key.to_string()) + .collect::>(); + let num_recipients = recipient_keys.len(); + let recipient_records = client + .conn_and_then(move |conn| { + let recipient_entity = RecipientEntity::new(&conn); + recipient_entity + .find_all_by_public_keys(recipient_keys.as_slice()) + .map_err(async_sqlite::Error::Rusqlite) + }) + .await?; + if recipient_records.len() != num_recipients { + return Err(SharingError::MissingRecipients.into()); + } + + // Prepare vault data + let folder_row = FolderRow::new_insert_from_vault(vault).await?; + + // Insert folder, create shared folder and shared folder recipients + // and insert folder invites for recipients other than the owner + client + .conn_mut_and_then(move |conn| { + let tx = conn.transaction()?; + + // Insert folder + let folder_entity = FolderEntity::new(&tx); + let folder_row_id = + folder_entity.insert_folder(account_row.row_id, &folder_row)?; + + // Insert shared_folder join for each recipient + let mut shared_folder_ids = std::collections::HashMap::new(); + for row in &recipient_records + { + let query = sql::Insert::new() + .insert_into("shared_folders (account_id, folder_id)") + .values("(?1, ?2)"); + + let mut stmt = tx.prepare_cached(&query.as_string())?; + stmt.execute((row.account_id, folder_row_id))?; + let shared_folder_id = tx.last_insert_rowid(); + + shared_folder_ids.insert( + row.recipient_public_key.clone(), + (shared_folder_id, row.recipient_id), + ); + } + + // Insert shared_folder_recipients + for (public_key, (shared_folder_id, recipient_id)) in + &shared_folder_ids + { + let is_creator = + if public_key == &owner_public_key_str { 1 } else { 0 }; + + let query = sql::Insert::new() + .insert_into("shared_folder_recipients (shared_folder_id, recipient_id, is_creator)") + .values("(?1, ?2, ?3)"); + + let mut stmt = tx.prepare_cached(&query.as_string())?; + stmt.execute((shared_folder_id, recipient_id, is_creator))?; + } + + // Create invites for all recipients except owner + for (public_key, (_, recipient_id)) in shared_folder_ids { + // Skip owner + if public_key == owner_public_key_str { + continue; + } + + // println!("{owner_recipient_id} {recipient_id} {folder_row_id}"); + + let query = sql::Insert::new() + .insert_into( + r#" + folder_invites + ( + created_at, + modified_at, + from_recipient_id, + to_recipient_id, + folder_id + ) + "#, + ) + .values("(?1, ?2, ?3, ?4, ?5)"); + + let params = ( + UtcDateTime::default().to_rfc3339()?, + UtcDateTime::default().to_rfc3339()?, + owner_recipient_id, + recipient_id, + folder_row_id, + ); + + let mut stmt = tx.prepare_cached(&query.as_string())?; + stmt.execute(params)?; + } + + tx.commit()?; + Ok::<_, crate::Error>(()) + }) + .await?; + + Ok(()) + } + + /// Delete a shared folder. + pub async fn delete_shared_folder( + client: &Client, + account_id: &AccountId, + folder_id: &VaultId, + ) -> Result { + // Validate account exists and get recipient + let check_account_id = *account_id; + let (account_row, recipient_row) = client + .conn_and_then(move |conn| { + let account = AccountEntity::new(&conn); + let account_row = account.find_optional(&check_account_id)?; + let recipient_row = if let Some(account_row) = &account_row { + let recipient = RecipientEntity::new(&conn); + recipient.find_optional(account_row.row_id)? + } else { + None + }; + Ok::<_, async_sqlite::Error>((account_row, recipient_row)) + }) + .await?; + + let account_row = + account_row.ok_or(SharingError::DeleteNoAccount(*account_id))?; + let recipient_row = recipient_row + .ok_or(SharingError::RecipientNotCreated(*account_id))?; + + // Store caller's public key for the outcome + let caller_public_key = recipient_row.recipient_public_key.clone(); + + // Validate folder exists and get shared folder join + let check_folder_id = *folder_id; + let account_row_id = account_row.row_id; + let recipient_id = recipient_row.recipient_id; + + let (folder_row, shared_folder) = client + .conn_and_then(move |conn| { + let folder_entity = FolderEntity::new(&conn); + let folder_row = + folder_entity.find_optional(&check_folder_id)?; + + let res = if let Some(folder_row) = &folder_row { + // Find shared_folder join + let query = sql::Select::new() + .select("shared_folder_id") + .from("shared_folders") + .where_clause("account_id = ?1 AND folder_id = ?2"); + + let mut stmt = conn.prepare_cached(&query.as_string())?; + if let Some(shared_folder_id) = stmt + .query_row( + (account_row_id, folder_row.row_id), + |row| row.get::(0), + ) + .optional()? + { + // Step 3: Get creator status + let query = sql::Select::new() + .select("is_creator") + .from("shared_folder_recipients") + .where_clause( + "shared_folder_id = ?1 AND recipient_id = ?2", + ); + + let mut stmt = + conn.prepare_cached(&query.as_string())?; + let is_creator: i64 = stmt.query_row( + (shared_folder_id, recipient_id), + |row| row.get(0), + )?; + + Some((shared_folder_id, is_creator > 0)) + } else { + None + } + } else { + None + }; + + Ok::<_, async_sqlite::Error>((folder_row, res)) + }) + .await?; + + let folder_row = + folder_row.ok_or(SharingError::DeleteNoFolder(*folder_id))?; + let (shared_folder_id, is_creator) = + shared_folder.ok_or(SharingError::DeleteNotShared(*folder_id))?; + + // Get all recipients for this shared folder BEFORE deletion + let folder_row_id = folder_row.row_id; + let participants = client + .conn_and_then(move |conn| { + let query = sql::Select::new() + .select( + r#" + a.identifier, + r.recipient_public_key + "#, + ) + .from("shared_folders AS sf") + .inner_join( + "shared_folder_recipients AS sfr ON sf.shared_folder_id = sfr.shared_folder_id", + ) + .inner_join("recipients AS r ON sfr.recipient_id = r.recipient_id") + .inner_join("accounts AS a ON r.account_id = a.account_id") + .where_clause("sf.folder_id = ?1"); + + let mut stmt = conn.prepare_cached(&query.as_string())?; + + fn convert_row(row: &Row<'_>) -> Result<(AccountId, String)> { + let account_id: String = row.get(0)?; + let public_key: String = row.get(1)?; + Ok((account_id.parse()?, public_key)) + } + + let rows = stmt.query_and_then([folder_row_id], convert_row)?; + + let mut recipients = Vec::new(); + for row in rows { + recipients.push(row?); + } + Ok::<_, crate::Error>(recipients) + }) + .await?; + + // Delete based on creator status + client + .conn_mut_and_then(move |conn| { + let tx = conn.transaction()?; + if is_creator { + // Owner: delete entire folder (cascades to all related tables) + let query = sql::Delete::new() + .delete_from("folders") + .where_clause("folder_id = ?1"); + + let mut stmt = tx.prepare_cached(&query.as_string())?; + stmt.execute([folder_row.row_id])?; + } else { + // Non-owner: delete specific joins and invites + + // Delete from shared_folders join table + // this will cascade to the shared_folder_recipients + let query = sql::Delete::new() + .delete_from("shared_folders") + .where_clause("shared_folder_id = ?1"); + + let mut stmt = tx.prepare_cached(&query.as_string())?; + stmt.execute([shared_folder_id])?; + + // Delete folder invites for this recipient and folder + // which would allow inviting the recipient again if necessary + let query = sql::Delete::new() + .delete_from("folder_invites") + .where_clause( + "to_recipient_id = ?1 AND folder_id = ?2", + ); + + let mut stmt = tx.prepare_cached(&query.as_string())?; + stmt.execute((recipient_id, folder_row.row_id))?; + } + + tx.commit()?; + Ok::<_, crate::Error>(()) + }) + .await?; + + Ok(DeleteSharedFolderOutcome { + is_creator, + caller_public_key, + participants, + }) + } +} diff --git a/crates/database/src/entity/shared_folder/recipient.rs b/crates/database/src/entity/shared_folder/recipient.rs new file mode 100644 index 0000000000..6a698203d9 --- /dev/null +++ b/crates/database/src/entity/shared_folder/recipient.rs @@ -0,0 +1,347 @@ +use crate::{Error, Result}; +use async_sqlite::rusqlite::{ + CachedStatement, Connection, Error as SqlError, OptionalExtension, Row, +}; +use sos_core::{Recipient, UtcDateTime}; +use sql_query_builder as sql; +use std::{ops::Deref, result::Result as StdResult}; + +fn select_columns(sql: sql::Select) -> sql::Select { + sql.select( + r#" + recipients.recipient_id, + recipients.account_id, + recipients.created_at, + recipients.modified_at, + recipients.recipient_name, + recipients.recipient_email, + recipients.recipient_public_key, + recipients.revoked + "#, + ) +} + +/// Represents a recipient. +#[doc(hidden)] +#[derive(Debug)] +pub struct RecipientRow { + pub recipient_id: i64, + pub account_id: i64, + pub created_at: String, + pub modified_at: String, + pub recipient_name: String, + pub recipient_email: Option, + pub recipient_public_key: String, + pub revoked: i64, +} + +impl<'a> TryFrom<&Row<'a>> for RecipientRow { + type Error = SqlError; + fn try_from(row: &Row<'a>) -> StdResult { + Ok(RecipientRow { + recipient_id: row.get(0)?, + account_id: row.get(1)?, + created_at: row.get(2)?, + modified_at: row.get(3)?, + recipient_name: row.get(4)?, + recipient_email: row.get(5)?, + recipient_public_key: row.get(6)?, + revoked: row.get(7)?, + }) + } +} + +impl RecipientRow { + /// Create a new recipient row to be inserted. + pub fn new_insert( + account_id: i64, + recipient_name: String, + recipient_email: Option, + recipient_public_key: String, + ) -> Result { + Ok(Self { + recipient_id: 0, + account_id, + created_at: UtcDateTime::default().to_rfc3339()?, + modified_at: UtcDateTime::default().to_rfc3339()?, + recipient_name, + recipient_email, + recipient_public_key, + revoked: 0, + }) + } + + /// Create a new recipient row to update. + pub fn new_update( + &self, + recipient_name: String, + recipient_email: Option, + recipient_public_key: String, + ) -> Result { + Ok(RecipientRow { + recipient_id: self.recipient_id, + account_id: self.account_id, + created_at: self.created_at.clone(), + modified_at: UtcDateTime::default().to_rfc3339()?, + recipient_name, + recipient_email, + recipient_public_key, + revoked: self.revoked, + }) + } +} + +/// Record for a recipient. +#[derive(Debug)] +pub struct RecipientRecord { + /// Row identifier. + pub row_id: i64, + /// Created date and time. + pub created_at: UtcDateTime, + /// Modified date and time. + pub modified_at: UtcDateTime, + /// Recipient public name. + pub recipient_name: String, + /// Recipient email address. + pub recipient_email: Option, + /// Recipient public key. + pub recipient_public_key: String, + /// Whether the recipient public key has been revoked. + pub revoked: bool, +} + +impl TryFrom for Recipient { + type Error = Error; + + fn try_from(value: RecipientRecord) -> Result { + Ok(Recipient { + name: value.recipient_name, + email: value.recipient_email, + public_key: value + .recipient_public_key + .parse() + .map_err(Error::AgeX25519Parse)?, + }) + } +} + +impl TryFrom for RecipientRecord { + type Error = Error; + + fn try_from(value: RecipientRow) -> Result { + Ok(Self { + row_id: value.recipient_id, + created_at: UtcDateTime::parse_rfc3339(&value.created_at)?, + modified_at: UtcDateTime::parse_rfc3339(&value.modified_at)?, + recipient_name: value.recipient_name, + recipient_email: value.recipient_email, + recipient_public_key: value.recipient_public_key, + revoked: value.revoked > 0, + }) + } +} + +/// Recipient entity. +pub struct RecipientEntity<'conn, C> +where + C: Deref, +{ + conn: &'conn C, +} + +impl<'conn, C> RecipientEntity<'conn, C> +where + C: Deref, +{ + /// Create a new shared folder recipient. + pub fn new(conn: &'conn C) -> Self { + Self { conn } + } + + fn select_recipient<'a>( + &'a self, + ) -> StdResult, SqlError> { + let query = select_columns(sql::Select::new()) + .from("recipients") + .where_clause("account_id = ?1"); + self.conn.prepare_cached(&query.as_string()) + } + + /// Find a recipient in the database. + pub fn find_one( + &self, + account_id: i64, + ) -> StdResult { + let mut stmt = self.select_recipient()?; + stmt.query_row([account_id], |row| row.try_into()) + } + + /// Find an optional recipient in the database. + pub fn find_optional( + &self, + account_id: i64, + ) -> StdResult, SqlError> { + let mut stmt = self.select_recipient()?; + stmt.query_row([account_id], |row| { + let row: RecipientRow = row.try_into()?; + Ok(row) + }) + .optional() + } + + /// Find an optional recipient by public key. + pub fn find_by_public_key( + &self, + public_key: &str, + ) -> StdResult, SqlError> { + let query = select_columns(sql::Select::new()) + .from("recipients") + .where_clause("recipient_public_key = ?1"); + let mut stmt = self.conn.prepare_cached(&query.as_string())?; + stmt.query_row([public_key], |row| { + let row: RecipientRow = row.try_into()?; + Ok(row) + }) + .optional() + } + + /// Find an recipients by public keys. + pub fn find_all_by_public_keys( + &self, + public_keys: &[String], + ) -> StdResult, SqlError> { + if public_keys.is_empty() { + return Ok(Vec::new()); + } + + // Build placeholders for the IN clause + let placeholders = (1..=public_keys.len()) + .map(|i| format!("?{}", i)) + .collect::>() + .join(", "); + + let query = select_columns(sql::Select::new()) + .from("recipients") + .where_clause(&format!( + "recipient_public_key IN ({})", + placeholders + )); + + let mut stmt = self.conn.prepare(&query.as_string())?; + let rows = stmt.query_map( + async_sqlite::rusqlite::params_from_iter(public_keys.iter()), + |row| row.try_into(), + )?; + rows.collect() + } + + /// Create the recipient entity in the database. + pub fn insert_recipient( + &self, + recipient_row: &RecipientRow, + ) -> StdResult { + let query = sql::Insert::new() + .insert_into( + r#" + recipients + ( + account_id, + created_at, + modified_at, + recipient_name, + recipient_email, + recipient_public_key + ) + "#, + ) + .values("(?1, ?2, ?3, ?4, ?5, ?6)"); + + let mut stmt = self.conn.prepare_cached(&query.as_string())?; + stmt.execute(( + &recipient_row.account_id, + &recipient_row.created_at, + &recipient_row.modified_at, + &recipient_row.recipient_name, + &recipient_row.recipient_email, + &recipient_row.recipient_public_key, + ))?; + Ok(self.conn.last_insert_rowid()) + } + + /// Update the recipient entity in the database. + pub fn update_recipient( + &self, + recipient_row: &RecipientRow, + ) -> StdResult<(), SqlError> { + let query = sql::Update::new() + .update("recipients") + .set( + r#" + modified_at = ?1, + recipient_name = ?2, + recipient_email = ?3, + recipient_public_key = ?4 + "#, + ) + .where_clause("recipient_id=?5"); + let mut stmt = self.conn.prepare_cached(&query.as_string())?; + stmt.execute(( + &recipient_row.modified_at, + &recipient_row.recipient_name, + &recipient_row.recipient_email, + &recipient_row.recipient_public_key, + recipient_row.recipient_id, + ))?; + + Ok(()) + } + + /// Search for recipients + pub fn search_recipients( + &mut self, + search_query: &str, + limit: Option, + ) -> Result> { + let limit = limit + .map(|l| l.to_string()) + .unwrap_or_else(|| String::from("25")); + let search_query = search_query + .split_whitespace() + .map(|word| format!("\"{}\"", word)) + .collect::>() + .join(" OR "); + + let query = sql::Select::new() + .select( + r#" + r.recipient_id, + r.account_id, + r.created_at, + r.modified_at, + r.recipient_name, + r.recipient_email, + r.recipient_public_key, + r.revoked, + fts.rowid, + fts.rank + "#, + ) + .from("recipients_fts AS fts") + .inner_join("recipients AS r ON fts.rowid = r.recipient_id") + .where_clause("recipients_fts MATCH ?1") + .order_by("fts.rank DESC") + .limit(&limit); + + let mut stmt = self.conn.prepare_cached(&query.as_string())?; + fn convert_row(row: &Row<'_>) -> Result { + Ok(row.try_into()?) + } + + let rows = stmt.query_and_then([search_query], convert_row)?; + let mut recipients = Vec::new(); + for row in rows { + recipients.push(row?.try_into()?); + } + Ok(recipients) + } +} diff --git a/crates/database/src/error.rs b/crates/database/src/error.rs index 6ade93e3f1..6d04178036 100644 --- a/crates/database/src/error.rs +++ b/crates/database/src/error.rs @@ -1,6 +1,55 @@ -use sos_core::{commit::CommitHash, VaultId}; +use sos_core::{AccountId, VaultId, commit::CommitHash}; use thiserror::Error; +/// Errors generated managing shared folders. +#[derive(Debug, Error)] +pub enum SharingError { + /// Error when an account has not yet published a recipient public key. + #[error("account {0} has not published a recipient public key")] + RecipientNotCreated(AccountId), + + /// Attempt to create a shared folder invite for an account + /// that does not exist. + #[error("cannot create a folder invite with missing account: {0}")] + InviteNoAccount(AccountId), + + /// Attempt to create a shared folder invite for a recipient + /// public key that does not exist, + #[error( + "cannot create a folder invite with missing recipient public key: {0}" + )] + InviteNoRecipient(String), + + /// Attempt to create a shared folder invite for a folder + /// that does not exist. + #[error("cannot create a folder invite with missing folder: {0}")] + InviteNoFolder(VaultId), + + /// Attempt to update a folder invite to pending. + #[error("pending invite status is not allowed")] + PendingInviteStatusNotAllowed, + + /// Account owner not found in recipients list. + #[error("account {0} not found in recipients list")] + OwnerNotInRecipients(AccountId), + + /// Missing some recipient records for a collection of public keys. + #[error("missing some recipients for collection of public keys")] + MissingRecipients, + + /// Attempt to delete a shared folder for an account that does not exist. + #[error("cannot delete shared folder with missing account: {0}")] + DeleteNoAccount(AccountId), + + /// Attempt to delete a shared folder that does not exist. + #[error("cannot delete missing folder: {0}")] + DeleteNoFolder(VaultId), + + /// Attempt to delete a shared folder that the account is not a member of. + #[error("account is not a member of shared folder: {0}")] + DeleteNotShared(VaultId), +} + /// Errors generated by the database library. #[derive(Debug, Error)] pub enum Error { @@ -19,7 +68,9 @@ pub enum Error { /// Error generated when replacing events in an event log /// does not compute the same root hash as the expected /// checkpoint. - #[error("checkpoint verification failed, expected root hash '{checkpoint}' but computed '{computed}')")] + #[error( + "checkpoint verification failed, expected root hash '{checkpoint}' but computed '{computed}')" + )] CheckpointVerification { /// Checkpoint root hash. checkpoint: CommitHash, @@ -27,6 +78,10 @@ pub enum Error { computed: CommitHash, }, + /// Failed to parse AGE X25519 public key. + #[error("unable to parse AGE X25519 public key: {0}")] + AgeX25519Parse(&'static str), + /// Errors generated by the core library. #[error(transparent)] Core(#[from] sos_core::Error), @@ -75,4 +130,8 @@ pub enum Error { /// Errors generated by the URN library. #[error(transparent)] Urn(#[from] urn::Error), + + /// Errors generated by the shared folder management. + #[error(transparent)] + Sharing(#[from] SharingError), } diff --git a/crates/database/src/event_log.rs b/crates/database/src/event_log.rs index 597a9a9d4f..51535b0693 100644 --- a/crates/database/src/event_log.rs +++ b/crates/database/src/event_log.rs @@ -8,13 +8,13 @@ //! re-owner an event log you must create a new event log so //! the owner reference is updated. use crate::{ + Error, entity::{ AccountEntity, CommitRecord, EventEntity, EventRecordRow, FolderEntity, FolderRecord, }, - Error, }; -use async_sqlite::{rusqlite::Row, Client}; +use async_sqlite::{Client, rusqlite::Row}; use async_trait::async_trait; use binary_stream::futures::{Decodable, Encodable}; use futures::{ @@ -22,15 +22,14 @@ use futures::{ stream::{BoxStream, StreamExt, TryStreamExt}, }; use sos_core::{ + AccountId, VaultId, commit::{CommitHash, CommitProof, CommitSpan, CommitTree, Comparison}, encoding::VERSION1, events::{ - changes_feed, - patch::{CheckedPatch, Diff, Patch}, AccountEvent, DeviceEvent, EventLog, EventLogType, EventRecord, - LocalChangeEvent, WriteEvent, + LocalChangeEvent, WriteEvent, changes_feed, + patch::{CheckedPatch, Diff, Patch}, }, - AccountId, VaultId, }; /// Owner of an event log. @@ -640,10 +639,10 @@ where while let Some(record) = stream.next().await { let record = record?; - if let Some(commit) = commit { - if record.commit() == commit { - return Ok(events); - } + if let Some(commit) = commit + && record.commit() == commit + { + return Ok(events); } // Iterating in reverse order as we would typically // be looking for commits near the end of the event log diff --git a/crates/database/src/lib.rs b/crates/database/src/lib.rs index 9493ef8270..5698f71293 100644 --- a/crates/database/src/lib.rs +++ b/crates/database/src/lib.rs @@ -1,6 +1,6 @@ #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #![allow(clippy::large_enum_variant)] //! Database storage layer for the [Save Our Secrets](https://saveoursecrets.com) SDK. @@ -37,7 +37,7 @@ pub use event_log::FileEventLog; mod error; pub use async_sqlite; -pub use error::Error; +pub use error::{Error, SharingError}; /// Result type for the library. pub(crate) type Result = std::result::Result; diff --git a/crates/database/src/migrations.rs b/crates/database/src/migrations.rs index 9ac19eb099..60423fa069 100644 --- a/crates/database/src/migrations.rs +++ b/crates/database/src/migrations.rs @@ -1,6 +1,6 @@ //! Run database migrations. use crate::Result; -use async_sqlite::{rusqlite::Connection, Client}; +use async_sqlite::{Client, rusqlite::Connection}; use refinery::Report; use tokio::sync::oneshot; diff --git a/crates/database/src/preferences.rs b/crates/database/src/preferences.rs index e2069840d5..1764bf7055 100644 --- a/crates/database/src/preferences.rs +++ b/crates/database/src/preferences.rs @@ -1,6 +1,6 @@ use crate::{ - entity::{AccountEntity, PreferenceEntity, PreferenceRow}, Error, + entity::{AccountEntity, PreferenceEntity, PreferenceRow}, }; use async_sqlite::Client; use async_trait::async_trait; diff --git a/crates/database/src/server_origins.rs b/crates/database/src/server_origins.rs index a7016a0398..d27f36bd14 100644 --- a/crates/database/src/server_origins.rs +++ b/crates/database/src/server_origins.rs @@ -1,6 +1,6 @@ use crate::{ - entity::{AccountEntity, ServerEntity, ServerRow}, Error, + entity::{AccountEntity, ServerEntity, ServerRow}, }; use async_sqlite::Client; use async_trait::async_trait; diff --git a/crates/database/src/system_messages.rs b/crates/database/src/system_messages.rs index 38997cce17..6ad988a106 100644 --- a/crates/database/src/system_messages.rs +++ b/crates/database/src/system_messages.rs @@ -2,8 +2,8 @@ use std::collections::HashMap; use crate::{ - entity::{AccountEntity, SystemMessageEntity, SystemMessageRow}, Error, + entity::{AccountEntity, SystemMessageEntity, SystemMessageRow}, }; use async_sqlite::Client; use async_trait::async_trait; diff --git a/crates/database/src/vault_writer.rs b/crates/database/src/vault_writer.rs index bb36fce880..91d314e2bd 100644 --- a/crates/database/src/vault_writer.rs +++ b/crates/database/src/vault_writer.rs @@ -1,16 +1,16 @@ //! Write vault changes to a database. use crate::{ - entity::{FolderEntity, FolderRecord, SecretRecord, SecretRow}, Error, + entity::{FolderEntity, FolderRecord, SecretRecord, SecretRow}, }; use async_sqlite::Client; use async_trait::async_trait; use sos_core::{ + SecretId, VaultCommit, VaultEntry, VaultFlags, VaultId, commit::CommitHash, crypto::AeadPack, encode, events::{ReadEvent, WriteEvent}, - SecretId, VaultCommit, VaultEntry, VaultFlags, VaultId, }; use sos_vault::{EncryptedEntry, Summary, Vault}; use std::borrow::Cow; diff --git a/crates/database_upgrader/Cargo.toml b/crates/database_upgrader/Cargo.toml index a38682ee60..5cfe9d641b 100644 --- a/crates/database_upgrader/Cargo.toml +++ b/crates/database_upgrader/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-database-upgrader" version = "0.17.3" -edition = "2021" +edition = "2024" description = "Upgrade from file system to database storage for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -45,6 +45,3 @@ tokio.workspace = true url.workspace = true tokio-stream.workspace = true tempfile.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/database_upgrader/build.rs b/crates/database_upgrader/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/database_upgrader/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/database_upgrader/src/archive.rs b/crates/database_upgrader/src/archive.rs index c280b67c8f..941e9d9961 100644 --- a/crates/database_upgrader/src/archive.rs +++ b/crates/database_upgrader/src/archive.rs @@ -1,11 +1,11 @@ //! Upgrade backup archives. -use crate::{upgrade_accounts, Error, Result, UpgradeOptions}; +use crate::{Error, Result, UpgradeOptions, upgrade_accounts}; use sos_backend::{ + BackendTarget, archive::{ - export_backup_archive, import_backup_archive, - read_backup_archive_manifest, ArchiveManifest, + ArchiveManifest, export_backup_archive, import_backup_archive, + read_backup_archive_manifest, }, - BackendTarget, }; use sos_core::Paths; use sos_database::open_file; diff --git a/crates/database_upgrader/src/error.rs b/crates/database_upgrader/src/error.rs index 7055b641ea..6f0840bc47 100644 --- a/crates/database_upgrader/src/error.rs +++ b/crates/database_upgrader/src/error.rs @@ -1,4 +1,4 @@ -use sos_core::{commit::CommitHash, AccountId}; +use sos_core::{AccountId, commit::CommitHash}; use std::path::PathBuf; use thiserror::Error; diff --git a/crates/database_upgrader/src/lib.rs b/crates/database_upgrader/src/lib.rs index 0db90a7666..4cdcc576b4 100644 --- a/crates/database_upgrader/src/lib.rs +++ b/crates/database_upgrader/src/lib.rs @@ -1,10 +1,10 @@ +//! Database upgrader for the [Save Our Secrets](https://saveoursecrets.com) SDK. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #![allow(clippy::large_enum_variant)] #![allow(clippy::result_large_err)] -//! Database upgrader for the [Save Our Secrets](https://saveoursecrets.com) SDK. #[cfg(feature = "archive")] pub mod archive; mod upgrader; diff --git a/crates/database_upgrader/src/upgrader/db_import.rs b/crates/database_upgrader/src/upgrader/db_import.rs index abe9709dc6..70d87b5b31 100644 --- a/crates/database_upgrader/src/upgrader/db_import.rs +++ b/crates/database_upgrader/src/upgrader/db_import.rs @@ -1,6 +1,6 @@ use crate::{Error, Result, UpgradeOptions}; use async_trait::async_trait; -use futures::{pin_mut, StreamExt}; +use futures::{StreamExt, pin_mut}; use indexmap::IndexSet; use sos_audit::AuditStreamSink; use sos_backend::{ @@ -8,15 +8,14 @@ use sos_backend::{ FolderEventLog, }; use sos_client_storage::ClientStorage; +use sos_core::{Origin, VaultCommit}; +use sos_core::{Paths, PublicIdentity, SecretId}; use sos_core::{ - decode, encode, + VaultId, decode, encode, events::{EventLog, EventRecord}, - VaultId, }; -use sos_core::{Origin, VaultCommit}; -use sos_core::{Paths, PublicIdentity, SecretId}; use sos_database::{ - async_sqlite::{rusqlite::Transaction, Client}, + async_sqlite::{Client, rusqlite::Transaction}, entity::{ AccountEntity, AccountRow, AuditEntity, EventEntity, EventRecordRow, FolderEntity, FolderRow, PreferenceEntity, PreferenceRow, SecretRow, @@ -28,10 +27,10 @@ use sos_preferences::PreferenceMap; use sos_server_storage::ServerStorage; use sos_sync::{StorageEventLogs, SyncStatus, SyncStorage}; use sos_system_messages::SystemMessageMap; -use sos_vault::{list_local_folders, Summary, Vault}; +use sos_vault::{Summary, Vault, list_local_folders}; use sos_vfs as vfs; use std::{collections::HashMap, sync::Arc}; -use tokio::sync::RwLock; +use tokio::sync::{Mutex, RwLock}; pub(crate) enum AccountStorage { Server(ServerStorage), @@ -424,6 +423,7 @@ pub(crate) async fn import_account( ServerStorage::new( BackendTarget::Database(paths.clone(), client.clone()), account.account_id(), + Arc::new(Mutex::new(Default::default())), ) .await?, ) @@ -518,7 +518,15 @@ fn create_folder( let folder_entity = FolderEntity::new(tx); let folder_id = folder_entity.insert_folder( account_id, - &FolderRow::new_insert_parts(vault.summary(), salt, meta, seed)?, + &FolderRow::new_insert_parts( + vault.summary(), + salt, + meta, + seed, + // File system accounts never used + // shared access so we don't need to handle it here + None, + )?, )?; let secret_ids = folder_entity.insert_folder_secrets(folder_id, rows.as_slice())?; diff --git a/crates/database_upgrader/src/upgrader/mod.rs b/crates/database_upgrader/src/upgrader/mod.rs index a841bf97f9..af472c6911 100644 --- a/crates/database_upgrader/src/upgrader/mod.rs +++ b/crates/database_upgrader/src/upgrader/mod.rs @@ -3,7 +3,7 @@ use crate::{Error, Result}; use serde::{Deserialize, Serialize}; use sos_backend::BackendTarget; use sos_client_storage::ClientStorage; -use sos_core::{constants::JSON_EXT, Paths, PublicIdentity}; +use sos_core::{Paths, PublicIdentity, constants::JSON_EXT}; use sos_database::{ async_sqlite::JournalMode, migrations::migrate_client, open_file_with_journal_mode, open_memory, @@ -19,6 +19,7 @@ use std::{ sync::Arc, }; use tempfile::NamedTempFile; +use tokio::sync::Mutex; use url::Url; mod db_import; @@ -142,6 +143,7 @@ async fn import_accounts( ServerStorage::new( BackendTarget::FileSystem(account_paths.clone()), account.account_id(), + Arc::new(Mutex::new(Default::default())), ) .await?, ) @@ -414,10 +416,10 @@ async fn copy_file_blobs( dest = ?dest, "upgrade_accounts::copy_file"); - if let Some(parent) = dest.parent() { - if !vfs::try_exists(parent).await? { - vfs::create_dir_all(parent).await?; - } + if let Some(parent) = dest.parent() + && !vfs::try_exists(parent).await? + { + vfs::create_dir_all(parent).await?; } let mut input = File::open(&source).await?; diff --git a/crates/debug_snapshot/Cargo.toml b/crates/debug_snapshot/Cargo.toml index 4f75d1e7ce..4eb3c8c5f1 100644 --- a/crates/debug_snapshot/Cargo.toml +++ b/crates/debug_snapshot/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-debug-snapshot" version = "0.17.1" -edition = "2021" +edition = "2024" description = "Create debug snapshot ZIP archives for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -11,6 +11,9 @@ repository = "https://github.com/saveoursecrets/sdk" audit = [ "sos-backend/audit", ] +files = [ + "sos-backend/files", +] [dependencies] thiserror.workspace = true diff --git a/crates/debug_snapshot/src/lib.rs b/crates/debug_snapshot/src/lib.rs index cd4f693b81..7be7c68e55 100644 --- a/crates/debug_snapshot/src/lib.rs +++ b/crates/debug_snapshot/src/lib.rs @@ -1,3 +1,7 @@ +//! Create a debug snapshot of events. +#![deny(missing_docs)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] use sos_archive::ZipWriter; use sos_client_storage::{ ClientBaseStorage, ClientFolderStorage, ClientStorage, @@ -11,7 +15,7 @@ mod error; pub use error::Error; #[cfg(feature = "audit")] -use futures::{pin_mut, StreamExt}; +use futures::{StreamExt, pin_mut}; /// Options for debug snapshots. #[derive(Debug)] @@ -70,44 +74,44 @@ pub async fn export_debug_snapshot( let mut dir = vfs::read_dir(logs).await?; while let Some(entry) = dir.next_entry().await? { let path = entry.path(); - if let Some(name) = path.file_name() { - if name.to_string_lossy().starts_with(LOG_FILE_NAME) { - let buffer = vfs::read(&path).await?; - zip_writer - .add_file( - &format!("logs/{}.jsonl", name.to_string_lossy()), - &buffer, - ) - .await?; - } + if let Some(name) = path.file_name() + && name.to_string_lossy().starts_with(LOG_FILE_NAME) + { + let buffer = vfs::read(&path).await?; + zip_writer + .add_file( + &format!("logs/{}.jsonl", name.to_string_lossy()), + &buffer, + ) + .await?; } } } #[cfg(feature = "audit")] - if options.include_audit_trail { - if let Some(providers) = sos_backend::audit::providers() { - for (index, provider) in providers.iter().enumerate() { - let stream = provider.audit_stream(false).await?; - pin_mut!(stream); - - let events = stream - .filter_map(|e| async move { e.ok() }) - .filter_map(|e| async move { - if e.account_id() == &account_id { - Some(e) - } else { - None - } - }) - .collect::>() - .await; - - let buffer = serde_json::to_vec_pretty(&events)?; - zip_writer - .add_file(&format!("audit/{}.json", index), &buffer) - .await?; - } + if options.include_audit_trail + && let Some(providers) = sos_backend::audit::providers() + { + for (index, provider) in providers.iter().enumerate() { + let stream = provider.audit_stream(false).await?; + pin_mut!(stream); + + let events = stream + .filter_map(|e| async move { e.ok() }) + .filter_map(|e| async move { + if e.account_id() == &account_id { + Some(e) + } else { + None + } + }) + .collect::>() + .await; + + let buffer = serde_json::to_vec_pretty(&events)?; + zip_writer + .add_file(&format!("audit/{}.json", index), &buffer) + .await?; } } diff --git a/crates/extension_service/Cargo.toml b/crates/extension_service/Cargo.toml index 0264c21367..d8b914390b 100644 --- a/crates/extension_service/Cargo.toml +++ b/crates/extension_service/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-extension-service" version = "0.17.1" -edition = "2021" +edition = "2024" description = "Browser extension service for the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" diff --git a/crates/extension_service/src/lib.rs b/crates/extension_service/src/lib.rs index 7d48f1709d..44cec930be 100644 --- a/crates/extension_service/src/lib.rs +++ b/crates/extension_service/src/lib.rs @@ -1,11 +1,11 @@ use sos_account::AccountSwitcherOptions; use sos_backend::{BackendTarget, InferOptions}; -use sos_core::{events::changes_feed, Paths}; +use sos_core::{Paths, events::changes_feed}; use sos_ipc::{ + ServiceAppInfo, extension_helper::server::{ ExtensionHelperOptions, ExtensionHelperServer, }, - ServiceAppInfo, }; use sos_net::{ NetworkAccount, NetworkAccountOptions, NetworkAccountSwitcher, diff --git a/crates/external_files/Cargo.toml b/crates/external_files/Cargo.toml index f14e7e4195..f9bdb970d8 100644 --- a/crates/external_files/Cargo.toml +++ b/crates/external_files/Cargo.toml @@ -1,18 +1,18 @@ [package] name = "sos-external-files" version = "0.17.1" -edition = "2021" +edition = "2024" description = "External file blob support for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" repository = "https://github.com/saveoursecrets/sdk" [package.metadata.docs.rs] -features = [ "full" ] +all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] -full = ["files"] +default = ["files"] files = ["sos-core/files"] [dependencies] @@ -22,6 +22,3 @@ sos-vfs.workspace = true indexmap.workspace = true tracing.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/external_files/build.rs b/crates/external_files/build.rs deleted file mode 100644 index 5976a1c6d5..0000000000 --- a/crates/external_files/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel); -} diff --git a/crates/external_files/src/file_helpers.rs b/crates/external_files/src/file_helpers.rs index 62ef0079b1..dc3f894b59 100644 --- a/crates/external_files/src/file_helpers.rs +++ b/crates/external_files/src/file_helpers.rs @@ -53,24 +53,19 @@ where let mut dir = vfs::read_dir(path.as_ref()).await?; while let Some(entry) = dir.next_entry().await? { let path = entry.path(); - if path.is_dir() { - if let Some(file_name) = path.file_name() { - if let Ok(folder_id) = - file_name.to_string_lossy().as_ref().parse::() - { - let mut folder_files = - list_folder(func(folder_id)).await?; + if path.is_dir() + && let Some(file_name) = path.file_name() + && let Ok(folder_id) = + file_name.to_string_lossy().as_ref().parse::() + { + let mut folder_files = list_folder(func(folder_id)).await?; - for (secret_id, mut external_files) in - folder_files.drain(..) - { - for file_name in external_files.drain(..) { - files.insert(ExternalFile::new( - SecretPath(folder_id, secret_id), - file_name, - )); - } - } + for (secret_id, mut external_files) in folder_files.drain(..) { + for file_name in external_files.drain(..) { + files.insert(ExternalFile::new( + SecretPath(folder_id, secret_id), + file_name, + )); } } } @@ -86,18 +81,16 @@ async fn list_folder( let mut folder_dir = vfs::read_dir(path.as_ref()).await?; while let Some(entry) = folder_dir.next_entry().await? { let path = entry.path(); - if path.is_dir() { - if let Some(file_name) = path.file_name() { - tracing::debug!(file_name = ?file_name); - if let Ok(secret_id) = file_name - .to_string_lossy() - .as_ref() - .parse::() - { - let external_files = list_secret_files(path).await?; - // tracing::debug!(files_len = external_files.len()); - files.push((secret_id, external_files)); - } + if path.is_dir() + && let Some(file_name) = path.file_name() + { + tracing::debug!(file_name = ?file_name); + if let Ok(secret_id) = + file_name.to_string_lossy().as_ref().parse::() + { + let external_files = list_secret_files(path).await?; + // tracing::debug!(files_len = external_files.len()); + files.push((secret_id, external_files)); } } } @@ -113,20 +106,20 @@ async fn list_secret_files( let mut dir = vfs::read_dir(path.as_ref()).await?; while let Some(entry) = dir.next_entry().await? { let path = entry.path(); - if path.is_file() { - if let Some(file_name) = path.file_name() { - if let Ok(name) = file_name - .to_string_lossy() - .as_ref() - .parse::() - { - files.insert(name); - } else { - tracing::warn!( - file_name = %file_name.to_string_lossy().as_ref(), - "skip file (invalid file name)", - ); - } + if path.is_file() + && let Some(file_name) = path.file_name() + { + if let Ok(name) = file_name + .to_string_lossy() + .as_ref() + .parse::() + { + files.insert(name); + } else { + tracing::warn!( + file_name = %file_name.to_string_lossy().as_ref(), + "skip file (invalid file name)", + ); } } } diff --git a/crates/external_files/src/lib.rs b/crates/external_files/src/lib.rs index b440aa41cc..c506208dd2 100644 --- a/crates/external_files/src/lib.rs +++ b/crates/external_files/src/lib.rs @@ -1,3 +1,8 @@ +//! Support for external encypted file blobs. +#![deny(missing_docs)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] + #[cfg(feature = "files")] mod file_helpers; #[cfg(feature = "files")] diff --git a/crates/filesystem/Cargo.toml b/crates/filesystem/Cargo.toml index c974387ade..7a3bd3c27b 100644 --- a/crates/filesystem/Cargo.toml +++ b/crates/filesystem/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-filesystem" version = "0.17.2" -edition = "2021" +edition = "2024" description = "Filesystem backend for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -34,7 +34,7 @@ sos-archive = { workspace = true, optional = true } sos-audit = { workspace = true, optional = true } sos-system-messages = { workspace = true, optional = true } sos-preferences = { workspace = true, optional = true } -sos-external-files = { workspace = true, optional = true, features = ["full"] } +sos-external-files = { workspace = true, optional = true } thiserror.workspace = true async-trait.workspace = true @@ -59,6 +59,3 @@ uuid = { workspace = true, optional = true } time = { workspace = true, optional = true } hex = { workspace = true, optional = true } futures-util = { workspace = true, optional = true } - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/filesystem/build.rs b/crates/filesystem/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/filesystem/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/filesystem/src/archive/export.rs b/crates/filesystem/src/archive/export.rs index 06b07e8566..68c5f4e1aa 100644 --- a/crates/filesystem/src/archive/export.rs +++ b/crates/filesystem/src/archive/export.rs @@ -2,13 +2,13 @@ use crate::archive::{Error, ManifestVersion1, Result}; use hex; use sha2::{Digest, Sha256}; -use sos_archive::{ZipWriter, ARCHIVE_MANIFEST}; +use sos_archive::{ARCHIVE_MANIFEST, ZipWriter}; use sos_core::{ + AccountId, Paths, VaultId, constants::{ ACCOUNT_EVENTS, DEVICE_FILE, EVENT_LOG_EXT, FILE_EVENTS, JSON_EXT, PREFERENCES_FILE, REMOTES_FILE, VAULT_EXT, }, - AccountId, Paths, VaultId, }; use sos_vault::list_local_folders; use sos_vfs as vfs; diff --git a/crates/filesystem/src/archive/import.rs b/crates/filesystem/src/archive/import.rs index ea127ef6f2..8df44335da 100644 --- a/crates/filesystem/src/archive/import.rs +++ b/crates/filesystem/src/archive/import.rs @@ -2,23 +2,23 @@ use crate::archive::{ ArchiveItem, Error, ManifestVersion1, RestoreTargets, Result, }; -use crate::{write_exclusive, FolderEventLog, VaultFileWriter}; +use crate::{FolderEventLog, VaultFileWriter, write_exclusive}; use hex; use sha2::{Digest, Sha256}; -use sos_archive::{sanitize_file_path, ZipReader}; -use sos_core::events::EventLogType; +use sos_archive::{ZipReader, sanitize_file_path}; use sos_core::AccountId; +use sos_core::events::EventLogType; use sos_core::{ + Paths, PublicIdentity, VaultId, constants::{ - ACCOUNT_EVENTS, DEVICE_FILE, EVENT_LOG_EXT, FILES_DIR, FILE_EVENTS, + ACCOUNT_EVENTS, DEVICE_FILE, EVENT_LOG_EXT, FILE_EVENTS, FILES_DIR, JSON_EXT, PREFERENCES_FILE, REMOTES_FILE, VAULT_EXT, }, decode, events::EventLog, - Paths, PublicIdentity, VaultId, }; use sos_reducers::FolderReducer; -use sos_vault::{list_accounts, EncryptedEntry, Header, Vault}; +use sos_vault::{EncryptedEntry, Header, Vault, list_accounts}; use sos_vfs::{self as vfs, File}; use std::path::{Path, PathBuf}; use tokio::io::BufReader; @@ -247,40 +247,38 @@ async fn extract_files( file_name.as_str().map_err(sos_archive::Error::from)?, ); let mut it = path.iter(); - if let (Some(first), Some(second)) = (it.next(), it.next()) { - if first == FILES_DIR { - if let Ok(_vault_id) = - second.to_string_lossy().parse::() - { - // Only restore files for the selected vaults - // - // The given target path should already - // include any files/ prefix so we need - // to skip it - let mut relative = PathBuf::new(); - for part in path.iter().skip(1) { - relative = relative.join(part); - } - let destination = paths.files_dir().join(relative); - if let Some(parent) = destination.parent() { - if !vfs::try_exists(&parent).await? { - vfs::create_dir_all(parent).await?; - } - } - - let mut reader = reader - .inner_mut() - .reader_without_entry(index) - .await - .map_err(sos_archive::Error::from)?; - let output = File::create(destination).await?; - futures_util::io::copy( - &mut reader, - &mut output.compat_write(), - ) - .await?; - } + if let (Some(first), Some(second)) = (it.next(), it.next()) + && first == FILES_DIR + && let Ok(_vault_id) = + second.to_string_lossy().parse::() + { + // Only restore files for the selected vaults + // + // The given target path should already + // include any files/ prefix so we need + // to skip it + let mut relative = PathBuf::new(); + for part in path.iter().skip(1) { + relative = relative.join(part); } + let destination = paths.files_dir().join(relative); + if let Some(parent) = destination.parent() + && !vfs::try_exists(&parent).await? + { + vfs::create_dir_all(parent).await?; + } + + let mut reader = reader + .inner_mut() + .reader_without_entry(index) + .await + .map_err(sos_archive::Error::from)?; + let output = File::create(destination).await?; + futures_util::io::copy( + &mut reader, + &mut output.compat_write(), + ) + .await?; } } } diff --git a/crates/filesystem/src/audit_provider.rs b/crates/filesystem/src/audit_provider.rs index 25b5c55b0f..bf01808745 100644 --- a/crates/filesystem/src/audit_provider.rs +++ b/crates/filesystem/src/audit_provider.rs @@ -1,15 +1,15 @@ //! File system audit log file and provider. +use crate::Result; use crate::formats::{ - read_file_identity_bytes, FileItem, FileRecord, FormatStream, - FormatStreamIterator, + FileItem, FileRecord, FormatStream, FormatStreamIterator, + read_file_identity_bytes, }; -use crate::Result; use async_fd_lock::{LockRead, LockWrite}; use async_trait::async_trait; use binary_stream::futures::{ BinaryReader, BinaryWriter, Decodable, Encodable, }; -use futures::{stream::BoxStream, StreamExt}; +use futures::{StreamExt, stream::BoxStream}; use sos_audit::{AuditEvent, AuditStreamSink}; use sos_core::{constants::AUDIT_IDENTITY, encoding::encoding_options}; use sos_vfs::{self as vfs, File}; @@ -24,7 +24,7 @@ use tokio::{ AsyncRead, AsyncReadExt, AsyncSeek, AsyncSeekExt, AsyncWrite, AsyncWriteExt, BufReader, BufWriter, }, - sync::{mpsc, Mutex}, + sync::{Mutex, mpsc}, }; use tokio_stream::wrappers::ReceiverStream; diff --git a/crates/filesystem/src/error.rs b/crates/filesystem/src/error.rs index 1ed55041c3..c77931e14d 100644 --- a/crates/filesystem/src/error.rs +++ b/crates/filesystem/src/error.rs @@ -21,7 +21,9 @@ pub enum Error { /// Error generated when replacing events in an event log /// does not compute the same root hash as the expected /// checkpoint. - #[error("checkpoint verification failed, expected root hash '{checkpoint}' but computed '{computed}', snapshot rollback completed: '{rollback_completed}' (snapshot: '{snapshot:?}')")] + #[error( + "checkpoint verification failed, expected root hash '{checkpoint}' but computed '{computed}', snapshot rollback completed: '{rollback_completed}' (snapshot: '{snapshot:?}')" + )] CheckpointVerification { /// Checkpoint root hash. checkpoint: CommitHash, @@ -34,7 +36,9 @@ pub enum Error { }, /// Error generated trying to rewind an event log. - #[error("rewind failed as pruned commits is greater than the length of the in-memory tree")] + #[error( + "rewind failed as pruned commits is greater than the length of the in-memory tree" + )] RewindLeavesLength, /// Errors generated by the core library. diff --git a/crates/filesystem/src/event_log.rs b/crates/filesystem/src/event_log.rs index 655b0e075a..994fab522e 100644 --- a/crates/filesystem/src/event_log.rs +++ b/crates/filesystem/src/event_log.rs @@ -14,27 +14,26 @@ //! The first row must always contain a last commit hash that is all zero. //! use crate::{ + Error, Result, formats::{ - read_file_identity_bytes, EventLogRecord, FileItem, FormatStream, - FormatStreamIterator, + EventLogRecord, FileItem, FormatStream, FormatStreamIterator, + read_file_identity_bytes, }, - Error, Result, }; use async_fd_lock::{LockRead, LockWrite}; use async_trait::async_trait; use binary_stream::futures::{BinaryReader, Decodable, Encodable}; -use futures::{stream::BoxStream, StreamExt, TryStreamExt}; +use futures::{StreamExt, TryStreamExt, stream::BoxStream}; use sos_core::{ + AccountId, commit::{CommitHash, CommitProof, CommitSpan, CommitTree, Comparison}, encode, - encoding::{encoding_options, VERSION1}, + encoding::{VERSION1, encoding_options}, events::{ - changes_feed, - patch::{CheckedPatch, Diff, Patch}, AccountEvent, DeviceEvent, EventLogType, EventRecord, - LocalChangeEvent, WriteEvent, + LocalChangeEvent, WriteEvent, changes_feed, + patch::{CheckedPatch, Diff, Patch}, }, - AccountId, }; use sos_vfs::{self as vfs, File, OpenOptions}; use std::result::Result as StdResult; @@ -457,10 +456,10 @@ where // let file = self.file(); let mut it = self.iter(true).await?; while let Some(record) = it.next().await? { - if let Some(commit) = commit { - if &record.commit() == commit.as_ref() { - return Ok(events); - } + if let Some(commit) = commit + && &record.commit() == commit.as_ref() + { + return Ok(events); } let buffer = read_event_buffer(&self.data, &record).await?; // Iterating in reverse order as we would typically diff --git a/crates/filesystem/src/formats/records.rs b/crates/filesystem/src/formats/records.rs index dd5ebb7603..378c58c8f2 100644 --- a/crates/filesystem/src/formats/records.rs +++ b/crates/filesystem/src/formats/records.rs @@ -1,6 +1,6 @@ //! File format iterators. use binary_stream::futures::Decodable; -use sos_core::{commit::CommitHash, events::EventRecord, UtcDateTime}; +use sos_core::{UtcDateTime, commit::CommitHash, events::EventRecord}; use std::ops::Range; /// Trait for types yielded by the file iterator. diff --git a/crates/filesystem/src/formats/stream.rs b/crates/filesystem/src/formats/stream.rs index d789b02db5..95f90ce912 100644 --- a/crates/filesystem/src/formats/stream.rs +++ b/crates/filesystem/src/formats/stream.rs @@ -1,7 +1,7 @@ //! File streams. -use crate::{formats::FileItem, Result}; +use crate::{Result, formats::FileItem}; use async_trait::async_trait; -use binary_stream::futures::{stream_length, BinaryReader}; +use binary_stream::futures::{BinaryReader, stream_length}; use sos_core::encoding::encoding_options; use sos_vfs::File; use std::{io::SeekFrom, ops::Range}; @@ -204,19 +204,19 @@ where async fn next_forward(&mut self) -> Result> { let offset = self.header_offset; - if let (Some(lpos), Some(rpos)) = (self.forward, self.backward) { - if lpos == rpos { - return Ok(None); - } + if let (Some(lpos), Some(rpos)) = (self.forward, self.backward) + && lpos == rpos + { + return Ok(None); } let len = stream_length(&mut self.read_stream).await?; if len > offset { // Got to EOF - if let Some(lpos) = self.forward { - if lpos == len { - return Ok(None); - } + if let Some(lpos) = self.forward + && lpos == len + { + return Ok(None); } if self.forward.is_none() { @@ -232,19 +232,19 @@ where async fn next_back(&mut self) -> Result> { let offset: u64 = self.header_offset; - if let (Some(lpos), Some(rpos)) = (self.forward, self.backward) { - if lpos == rpos { - return Ok(None); - } + if let (Some(lpos), Some(rpos)) = (self.forward, self.backward) + && lpos == rpos + { + return Ok(None); } let len = stream_length(&mut self.read_stream).await?; if len > offset { // Got to EOF - if let Some(rpos) = self.backward { - if rpos == offset { - return Ok(None); - } + if let Some(rpos) = self.backward + && rpos == offset + { + return Ok(None); } if self.backward.is_none() { diff --git a/crates/filesystem/src/lib.rs b/crates/filesystem/src/lib.rs index e760410865..6890311a8a 100644 --- a/crates/filesystem/src/lib.rs +++ b/crates/filesystem/src/lib.rs @@ -1,7 +1,8 @@ +//! File system backend. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -//! File system backend. +#![cfg_attr(docsrs, feature(doc_cfg))] + mod advisory_locks; #[cfg(feature = "archive")] pub mod archive; diff --git a/crates/filesystem/src/preferences.rs b/crates/filesystem/src/preferences.rs index 89ac1eaa3c..f6b1791215 100644 --- a/crates/filesystem/src/preferences.rs +++ b/crates/filesystem/src/preferences.rs @@ -1,4 +1,4 @@ -use crate::{write_exclusive, Error}; +use crate::{Error, write_exclusive}; use async_fd_lock::LockRead; use async_trait::async_trait; use sos_core::{AccountId, Paths}; diff --git a/crates/filesystem/src/server_origins.rs b/crates/filesystem/src/server_origins.rs index 17a5193977..74a4670b2c 100644 --- a/crates/filesystem/src/server_origins.rs +++ b/crates/filesystem/src/server_origins.rs @@ -1,4 +1,4 @@ -use crate::{write_exclusive, Error}; +use crate::{Error, write_exclusive}; use async_fd_lock::LockRead; use async_trait::async_trait; use sos_core::{Origin, Paths, RemoteOrigins}; diff --git a/crates/filesystem/src/system_messages.rs b/crates/filesystem/src/system_messages.rs index aafebda550..a18701f683 100644 --- a/crates/filesystem/src/system_messages.rs +++ b/crates/filesystem/src/system_messages.rs @@ -1,5 +1,5 @@ //! System messages provider for the file system. -use crate::{write_exclusive, Error}; +use crate::{Error, write_exclusive}; use async_fd_lock::LockRead; use async_trait::async_trait; use sos_core::Paths; diff --git a/crates/filesystem/src/vault_writer.rs b/crates/filesystem/src/vault_writer.rs index 10ff0317f6..ddb3f1e69f 100644 --- a/crates/filesystem/src/vault_writer.rs +++ b/crates/filesystem/src/vault_writer.rs @@ -3,12 +3,12 @@ use async_fd_lock::{LockRead, LockWrite}; use async_trait::async_trait; use binary_stream::futures::{BinaryReader, BinaryWriter}; use sos_core::{ + SecretId, VaultCommit, VaultEntry, VaultFlags, commit::CommitHash, crypto::AeadPack, encode, encoding::encoding_options, events::{ReadEvent, WriteEvent}, - SecretId, VaultCommit, VaultEntry, VaultFlags, }; use sos_vault::{Contents, EncryptedEntry, Header, Summary, Vault}; use sos_vfs::{self as vfs, OpenOptions}; diff --git a/crates/integrity/Cargo.toml b/crates/integrity/Cargo.toml index 607fe43dce..b66f22ec20 100644 --- a/crates/integrity/Cargo.toml +++ b/crates/integrity/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-integrity" version = "0.17.2" -edition = "2021" +edition = "2024" description = "Integrity checks for the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -34,6 +34,3 @@ hex.workspace = true uuid.workspace = true sha2.workspace = true tokio-stream.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/integrity/build.rs b/crates/integrity/build.rs deleted file mode 100644 index 5976a1c6d5..0000000000 --- a/crates/integrity/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel); -} diff --git a/crates/integrity/src/account_integrity.rs b/crates/integrity/src/account_integrity.rs index 0a22ebbd45..872664e3c2 100644 --- a/crates/integrity/src/account_integrity.rs +++ b/crates/integrity/src/account_integrity.rs @@ -1,19 +1,20 @@ //! Check integrity of the folders in an account. use crate::{ - event_integrity, vault_integrity, Error, IntegrityFailure, Result, + Error, IntegrityFailure, Result, event_integrity, vault_integrity, }; -use futures::{pin_mut, StreamExt}; +use futures::{StreamExt, pin_mut}; use sos_backend::BackendTarget; use sos_core::{ - commit::CommitHash, events::EventRecord, AccountId, SecretId, VaultId, + AccountId, SecretId, VaultId, commit::CommitHash, events::EventRecord, }; use sos_database::entity::FolderEntity; use sos_vault::Summary; use sos_vfs as vfs; use std::sync::Arc; use tokio::sync::{ + Mutex, Semaphore, mpsc::{self, Receiver, Sender}, - watch, Mutex, Semaphore, + watch, }; /// Event dispatched whilst generating an integrity report. diff --git a/crates/integrity/src/error.rs b/crates/integrity/src/error.rs index f5e42d8c1f..b89e2288bc 100644 --- a/crates/integrity/src/error.rs +++ b/crates/integrity/src/error.rs @@ -7,7 +7,9 @@ use uuid::Uuid; pub enum Error { /// Error generated when event log row data does /// not match the commit hash. - #[error("row '{id}' checksums do not match, expected {commit} but got {value}")] + #[error( + "row '{id}' checksums do not match, expected {commit} but got {value}" + )] VaultHashMismatch { /// Expected commit hash. commit: CommitHash, diff --git a/crates/integrity/src/event_integrity.rs b/crates/integrity/src/event_integrity.rs index ca42aedf33..37393ff1f0 100644 --- a/crates/integrity/src/event_integrity.rs +++ b/crates/integrity/src/event_integrity.rs @@ -1,11 +1,11 @@ //! Run integrity checks on event logs. use crate::{Error, Result}; -use futures::{pin_mut, stream::BoxStream, StreamExt}; +use futures::{StreamExt, pin_mut, stream::BoxStream}; use sos_backend::{BackendTarget, FolderEventLog}; use sos_core::{ + AccountId, VaultId, commit::{CommitHash, CommitTree}, events::{EventLog, EventRecord}, - AccountId, VaultId, }; use tokio_stream::wrappers::ReceiverStream; diff --git a/crates/integrity/src/file_integrity.rs b/crates/integrity/src/file_integrity.rs index 2cc27d31b6..65a650d018 100644 --- a/crates/integrity/src/file_integrity.rs +++ b/crates/integrity/src/file_integrity.rs @@ -5,12 +5,13 @@ use futures::StreamExt; use indexmap::IndexSet; use sha2::{Digest, Sha256}; use sos_backend::BackendTarget; -use sos_core::{commit::CommitHash, ExternalFile}; +use sos_core::{ExternalFile, commit::CommitHash}; use sos_vfs as vfs; use std::{path::PathBuf, sync::Arc}; use tokio::sync::{ + Mutex, Semaphore, mpsc::{self, Receiver, Sender}, - watch, Mutex, Semaphore, + watch, }; use tokio_util::io::ReaderStream; diff --git a/crates/integrity/src/lib.rs b/crates/integrity/src/lib.rs index d2013a5619..19402b9ff0 100644 --- a/crates/integrity/src/lib.rs +++ b/crates/integrity/src/lib.rs @@ -1,7 +1,8 @@ +//! Integrity checks for vaults, event logs and external files. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -//! Integrity checks for vaults, event logs and external files. +#![cfg_attr(docsrs, feature(doc_cfg))] + mod account_integrity; mod error; mod event_integrity; @@ -9,19 +10,19 @@ mod event_integrity; mod file_integrity; mod vault_integrity; -pub use account_integrity::{account_integrity, FolderIntegrityEvent}; +pub use account_integrity::{FolderIntegrityEvent, account_integrity}; pub use event_integrity::event_integrity; pub use vault_integrity::vault_integrity; #[cfg(feature = "files")] -pub use file_integrity::{file_integrity, FileIntegrityEvent}; +pub use file_integrity::{FileIntegrityEvent, file_integrity}; pub use error::Error; /// Result type for the library. pub(crate) type Result = std::result::Result; -use sos_core::{commit::CommitHash, ExternalFile, VaultId}; +use sos_core::{ExternalFile, VaultId, commit::CommitHash}; /// Reasons why an integrity check can fail. #[derive(Debug)] diff --git a/crates/integrity/src/vault_integrity.rs b/crates/integrity/src/vault_integrity.rs index 1c4b4198cf..a7e5792496 100644 --- a/crates/integrity/src/vault_integrity.rs +++ b/crates/integrity/src/vault_integrity.rs @@ -1,23 +1,23 @@ //! Run integrity checks on vault files. use crate::{Error, Result}; use binary_stream::futures::BinaryReader; -use futures::{stream::BoxStream, StreamExt, TryStreamExt}; +use futures::{StreamExt, TryStreamExt, stream::BoxStream}; use sos_backend::{ + BackendTarget, database::{ async_sqlite::rusqlite::Row, entity::{FolderEntity, SecretRow}, }, - BackendTarget, }; use sos_core::{ + AccountId, SecretId, VaultId, commit::{CommitHash, CommitTree}, constants::VAULT_IDENTITY, encoding::encoding_options, - AccountId, SecretId, VaultId, }; use sos_filesystem::formats::{ - read_file_identity_bytes, FileItem, FormatStream, FormatStreamIterator, - VaultRecord, + FileItem, FormatStream, FormatStreamIterator, VaultRecord, + read_file_identity_bytes, }; use sos_vault::Header; use sos_vfs as vfs; diff --git a/crates/ipc/Cargo.toml b/crates/ipc/Cargo.toml index 19f8536589..5e393936f0 100644 --- a/crates/ipc/Cargo.toml +++ b/crates/ipc/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-ipc" version = "0.17.1" -edition = "2021" +edition = "2024" description = "Inter-process communication for the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -29,7 +29,7 @@ files = [ "sos-sync/files", ] migrate = ["sos-account/migrate"] -search = ["sos-search", "sos-protocol/search"] +search = ["sos-search", "sos-protocol/search", "sos-account/search"] extension-helper-server = [ "memory-http-server", "open", @@ -105,6 +105,3 @@ matchit = { version = "0.7", optional = true } # native bridge open = { version = "5", optional = true } -[build-dependencies] -rustc_version.workspace = true - diff --git a/crates/ipc/build.rs b/crates/ipc/build.rs deleted file mode 100644 index 5976a1c6d5..0000000000 --- a/crates/ipc/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel); -} diff --git a/crates/ipc/src/extension_helper/client.rs b/crates/ipc/src/extension_helper/client.rs index afaf79863d..55dae73bb5 100644 --- a/crates/ipc/src/extension_helper/client.rs +++ b/crates/ipc/src/extension_helper/client.rs @@ -3,8 +3,8 @@ //! //! Used to test the browser native messaging API integration. -use crate::local_transport::{HttpMessage, LocalRequest, LocalResponse}; use crate::Result; +use crate::local_transport::{HttpMessage, LocalRequest, LocalResponse}; use futures_util::{SinkExt, StreamExt}; use std::process::Stdio; use std::sync::atomic::{AtomicU64, Ordering}; diff --git a/crates/ipc/src/extension_helper/server.rs b/crates/ipc/src/extension_helper/server.rs index a42a9f88aa..cd4643770a 100644 --- a/crates/ipc/src/extension_helper/server.rs +++ b/crates/ipc/src/extension_helper/server.rs @@ -1,25 +1,25 @@ //! Server for the native messaging API extension helper. use crate::{ + Result, ServiceAppInfo, local_transport::{HttpMessage, LocalRequest, LocalResponse}, memory_server::{LocalMemoryClient, LocalMemoryServer}, web_service::WebAccounts, - Result, ServiceAppInfo, }; use futures_util::{SinkExt, StreamExt}; use http::{ - header::{CONTENT_LENGTH, CONTENT_TYPE}, StatusCode, + header::{CONTENT_LENGTH, CONTENT_TYPE}, }; use sos_account::{Account, AccountSwitcher}; use sos_changes::consumer::ChangeConsumer; -use sos_core::{events::LocalChangeEvent, ErrorExt, Paths}; +use sos_core::{ErrorExt, Paths, events::LocalChangeEvent}; use sos_login::DelegatedAccess; use sos_logs::Logger; -use sos_protocol::{constants::MIME_TYPE_JSON, ErrorReply}; +use sos_protocol::{ErrorReply, constants::MIME_TYPE_JSON}; use sos_sync::SyncStorage; use std::sync::Arc; -use tokio::sync::{mpsc, RwLock}; +use tokio::sync::{RwLock, mpsc}; use tokio_util::codec::{FramedRead, LengthDelimitedCodec}; use super::{CHUNK_LIMIT, CHUNK_SIZE}; diff --git a/crates/ipc/src/lib.rs b/crates/ipc/src/lib.rs index ae6aa2bf2a..d5a87d99bf 100644 --- a/crates/ipc/src/lib.rs +++ b/crates/ipc/src/lib.rs @@ -1,12 +1,11 @@ -#![deny(missing_docs)] -#![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -#![allow(clippy::result_large_err)] - //! Inter-process communication library for the //! [Save Our Secrets](https://saveoursecrets.com) SDK. //! //! Supports the native messaging API for browser extensions. +#![deny(missing_docs)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![allow(clippy::result_large_err)] mod error; diff --git a/crates/ipc/src/local_transport.rs b/crates/ipc/src/local_transport.rs index 0bbbdba162..04a99917d0 100644 --- a/crates/ipc/src/local_transport.rs +++ b/crates/ipc/src/local_transport.rs @@ -16,11 +16,11 @@ use sos_protocol::constants::{MIME_TYPE_JSON, X_SOS_REQUEST_ID}; use async_trait::async_trait; use bytes::Bytes; use http::{ - header::{CONTENT_ENCODING, CONTENT_TYPE}, Method, Request, Response, StatusCode, Uri, + header::{CONTENT_ENCODING, CONTENT_TYPE}, }; use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, DisplayFromStr}; +use serde_with::{DisplayFromStr, serde_as}; use std::{collections::HashMap, fmt, time::Duration}; use typeshare::typeshare; diff --git a/crates/ipc/src/memory_server.rs b/crates/ipc/src/memory_server.rs index bca2976cf3..cb59fad4e8 100644 --- a/crates/ipc/src/memory_server.rs +++ b/crates/ipc/src/memory_server.rs @@ -1,11 +1,11 @@ //! HTTP server that listens for connections //! using in-memory duplex streams. use crate::{ - local_transport::{LocalRequest, LocalResponse}, LocalWebService, Result, ServiceAppInfo, WebAccounts, + local_transport::{LocalRequest, LocalResponse}, }; use bytes::Bytes; -use http::{header::CONNECTION, Request, Response}; +use http::{Request, Response, header::CONNECTION}; use http_body_util::{BodyExt, Full}; use hyper::client::conn::http1::handshake; use hyper::server::conn::http1::Builder; diff --git a/crates/ipc/src/web_service/account.rs b/crates/ipc/src/web_service/account.rs index 540770308a..1ffa87829a 100644 --- a/crates/ipc/src/web_service/account.rs +++ b/crates/ipc/src/web_service/account.rs @@ -5,14 +5,14 @@ use secrecy::SecretString; use serde::Deserialize; use sos_account::Account; use sos_core::AccountId; -use sos_core::{crypto::AccessKey, ErrorExt}; +use sos_core::{ErrorExt, crypto::AccessKey}; use sos_login::DelegatedAccess; use sos_sync::SyncStorage; use std::collections::HashMap; use crate::web_service::{ - internal_server_error, json, parse_account_id, parse_json_body, status, - Body, Incoming, WebAccounts, + Body, Incoming, WebAccounts, internal_server_error, json, + parse_account_id, parse_json_body, status, }; #[derive(Deserialize)] @@ -285,13 +285,14 @@ where return internal_server_error(e); } - if save_password && keyring_password::supported() { - if let Err(e) = keyring_password::save_account_password( + if save_password + && keyring_password::supported() + && let Err(e) = keyring_password::save_account_password( &account_id.to_string(), password, - ) { - return internal_server_error(e); - } + ) + { + return internal_server_error(e); } } diff --git a/crates/ipc/src/web_service/common.rs b/crates/ipc/src/web_service/common.rs index 1e96294ce4..df6bb3f2e1 100644 --- a/crates/ipc/src/web_service/common.rs +++ b/crates/ipc/src/web_service/common.rs @@ -1,12 +1,12 @@ use super::{Body, Incoming}; use bytes::Bytes; -use http::{header::CONTENT_TYPE, Request, Response, StatusCode, Uri}; +use http::{Request, Response, StatusCode, Uri, header::CONTENT_TYPE}; use http_body_util::{BodyExt, Full}; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; use sos_core::AccountId; use sos_protocol::{ - constants::{MIME_TYPE_JSON, X_SOS_ACCOUNT_ID}, ErrorReply, + constants::{MIME_TYPE_JSON, X_SOS_ACCOUNT_ID}, }; use std::collections::HashMap; use url::form_urlencoded; diff --git a/crates/ipc/src/web_service/helpers.rs b/crates/ipc/src/web_service/helpers.rs index 9ba66ee554..2134c5c4a6 100644 --- a/crates/ipc/src/web_service/helpers.rs +++ b/crates/ipc/src/web_service/helpers.rs @@ -1,6 +1,6 @@ //! Helper routes for utility functions. -use crate::web_service::{parse_query, status, Body, Incoming}; +use crate::web_service::{Body, Incoming, parse_query, status}; use http::{Request, Response, StatusCode}; /// Open a URL. diff --git a/crates/ipc/src/web_service/mod.rs b/crates/ipc/src/web_service/mod.rs index 695f86be39..053cfcd230 100644 --- a/crates/ipc/src/web_service/mod.rs +++ b/crates/ipc/src/web_service/mod.rs @@ -9,9 +9,9 @@ use sos_core::ErrorExt; use sos_login::DelegatedAccess; use sos_sync::SyncStorage; use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc}; +use tower::Service as _; use tower::service_fn; use tower::util::BoxCloneService; -use tower::Service as _; use crate::ServiceAppInfo; diff --git a/crates/ipc/src/web_service/search.rs b/crates/ipc/src/web_service/search.rs index fcb5dae60f..d03a01b5e7 100644 --- a/crates/ipc/src/web_service/search.rs +++ b/crates/ipc/src/web_service/search.rs @@ -9,8 +9,8 @@ use sos_sync::SyncStorage; use std::collections::HashMap; use crate::web_service::{ - internal_server_error, json, parse_json_body, status, Body, Incoming, - WebAccounts, + Body, Incoming, WebAccounts, internal_server_error, json, + parse_json_body, status, }; #[derive(Deserialize)] diff --git a/crates/ipc/src/web_service/secret.rs b/crates/ipc/src/web_service/secret.rs index 03b84ae449..ba71c86e53 100644 --- a/crates/ipc/src/web_service/secret.rs +++ b/crates/ipc/src/web_service/secret.rs @@ -7,8 +7,8 @@ use sos_core::{ErrorExt, SecretPath}; use sos_sync::SyncStorage; use crate::web_service::{ - internal_server_error, json, parse_account_id, parse_json_body, status, - Body, Incoming, WebAccounts, + Body, Incoming, WebAccounts, internal_server_error, json, + parse_account_id, parse_json_body, status, }; #[derive(Deserialize)] diff --git a/crates/ipc/src/web_service/web_accounts.rs b/crates/ipc/src/web_service/web_accounts.rs index acceada772..54abc89bff 100644 --- a/crates/ipc/src/web_service/web_accounts.rs +++ b/crates/ipc/src/web_service/web_accounts.rs @@ -4,16 +4,16 @@ use sos_account::{Account, AccountSwitcher}; use sos_backend::BackendTarget; use sos_changes::consumer::ConsumerHandle; use sos_core::{ + AccountId, ErrorExt, Paths, VaultId, events::{ AccountEvent, EventLog, EventLogType, LocalChangeEvent, WriteEvent, }, - AccountId, ErrorExt, Paths, VaultId, }; use sos_login::DelegatedAccess; use sos_sync::SyncStorage; use sos_vault::SecretAccess; use std::{collections::HashSet, sync::Arc}; -use tokio::sync::{broadcast, RwLock}; +use tokio::sync::{RwLock, broadcast}; /// Event broadcast when an account changes. #[typeshare::typeshare] diff --git a/crates/keychain_parser/Cargo.toml b/crates/keychain_parser/Cargo.toml index 3a09d9691b..519c45e77e 100644 --- a/crates/keychain_parser/Cargo.toml +++ b/crates/keychain_parser/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "keychain_parser" version = "0.1.2" -edition = "2021" +edition = "2024" description = "Parse the output of security(1) dump-keychain." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" diff --git a/crates/keychain_parser/src/parser.rs b/crates/keychain_parser/src/parser.rs index 3f02922862..71f2a306c7 100644 --- a/crates/keychain_parser/src/parser.rs +++ b/crates/keychain_parser/src/parser.rs @@ -1,5 +1,5 @@ //! Parser for keychain access dumps. -use super::{error::LexError, Error, Result}; +use super::{Error, Result, error::LexError}; use logos::{Lexer, Logos}; use std::{borrow::Cow, collections::HashMap, ops::Range}; @@ -64,10 +64,10 @@ pub fn plist_secure_note<'a>( Cow::Borrowed(value) }; let value: plist::Value = plist::from_bytes(plist.as_bytes())?; - if let plist::Value::Dictionary(map) = value { - if let Some(plist::Value::String(data)) = map.get(NOTE_PLIST_KEY) { - return Ok(Some(Cow::Owned(data.to_owned()))); - } + if let plist::Value::Dictionary(map) = value + && let Some(plist::Value::String(data)) = map.get(NOTE_PLIST_KEY) + { + return Ok(Some(Cow::Owned(data.to_owned()))); } Ok(None) } @@ -382,7 +382,7 @@ impl<'s> KeychainParser<'s> { _ => { return Err(Error::ParseValue( source[lex.span()].to_owned(), - )) + )); } } } @@ -433,22 +433,20 @@ impl<'s> KeychainList<'s> { account: &str, ) -> Option<&KeychainEntry<'_>> { self.entries.iter().find(|entry| { - if let Some(EntryClass::GenericPassword) = entry.class { - if let (Some((_, attr_service)), Some((_, attr_account))) = ( + if let Some(EntryClass::GenericPassword) = entry.class + && let (Some((_, attr_service)), Some((_, attr_account))) = ( entry.find_attribute_by_name( AttributeName::SecServiceItemAttr, ), entry.find_attribute_by_name( AttributeName::SecAccountItemAttr, ), - ) { - if attr_service.matches(service) - && attr_account.matches(account) - { - return true; - } + ) + && attr_service.matches(service) + && attr_account.matches(account) + { + return true; } - } false }) } @@ -459,22 +457,20 @@ impl<'s> KeychainList<'s> { service: &str, ) -> Option<&KeychainEntry<'_>> { self.entries.iter().find(|entry| { - if let Some(EntryClass::GenericPassword) = entry.class { - if let (Some((_, attr_service)), Some((_, attr_type))) = ( + if let Some(EntryClass::GenericPassword) = entry.class + && let (Some((_, attr_service)), Some((_, attr_type))) = ( entry.find_attribute_by_name( AttributeName::SecServiceItemAttr, ), entry.find_attribute_by_name( AttributeName::SecTypeItemAttr, ), - ) { - if attr_service.matches(service) - && attr_type.matches(NOTE_TYPE) - { - return true; - } + ) + && attr_service.matches(service) + && attr_type.matches(NOTE_TYPE) + { + return true; } - } false }) } @@ -529,22 +525,22 @@ impl<'s> KeychainEntry<'s> { /// Attempt to get the entry data as a string /// for the generic password class. pub fn generic_data<'a>(&'a self) -> Result>> { - if let Some(data) = &self.data { - if let Some(EntryClass::GenericPassword) = self.class { - if self.is_note() { - if let Value::BlobString(_, value) = data { - return plist_secure_note(value, true); + if let Some(data) = &self.data + && let Some(EntryClass::GenericPassword) = self.class + { + if self.is_note() { + if let Value::BlobString(_, value) = data { + return plist_secure_note(value, true); + } + } else { + match data { + Value::String(value) => { + return Ok(Some(Cow::Borrowed(value))); } - } else { - match data { - Value::String(value) => { - return Ok(Some(Cow::Borrowed(value))) - } - Value::BlobString(_, value) => { - return Ok(Some(Cow::Borrowed(value))) - } - _ => {} + Value::BlobString(_, value) => { + return Ok(Some(Cow::Borrowed(value))); } + _ => {} } } } diff --git a/crates/login/Cargo.toml b/crates/login/Cargo.toml index df19a17228..6d123e9f52 100644 --- a/crates/login/Cargo.toml +++ b/crates/login/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-login" version = "0.17.3" -edition = "2021" +edition = "2024" description = "Login authentication for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -35,6 +35,3 @@ tokio.workspace = true typeshare.workspace = true ed25519-dalek.workspace = true tracing.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/login/build.rs b/crates/login/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/login/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/login/src/delegated_access.rs b/crates/login/src/delegated_access.rs index 3adbc79d96..b242fb539e 100644 --- a/crates/login/src/delegated_access.rs +++ b/crates/login/src/delegated_access.rs @@ -1,7 +1,7 @@ //! Trait for delegated password access. use async_trait::async_trait; use secrecy::SecretString; -use sos_core::{crypto::AccessKey, VaultId}; +use sos_core::{VaultId, crypto::AccessKey}; use sos_password::diceware::generate_passphrase_words; /// Number of words to use when generating passphrases for vaults. diff --git a/crates/login/src/device.rs b/crates/login/src/device.rs index a8e2871d0e..c46ca952e6 100644 --- a/crates/login/src/device.rs +++ b/crates/login/src/device.rs @@ -2,19 +2,20 @@ use crate::{Error, Result}; use secrecy::ExposeSecret; use sos_backend::{ - database::entity::{AccountEntity, FolderEntity, FolderRow}, AccessPoint, BackendTarget, + database::entity::{AccountEntity, FolderEntity, FolderRow}, }; use sos_core::{ + AccountId, SecretId, VaultFlags, crypto::AccessKey, device::{DeviceMetaData, DevicePublicKey, TrustedDevice}, - encode, AccountId, SecretId, VaultFlags, + encode, }; use sos_filesystem::write_exclusive; use sos_signer::ed25519::{self, BoxedEd25519Signer, SingleParty}; use sos_vault::{ - secret::{Secret, SecretSigner}, BuilderCredentials, SecretAccess, Vault, VaultBuilder, + secret::{Secret, SecretSigner}, }; use urn::Urn; @@ -146,13 +147,11 @@ impl DeviceManager { for id in device_keeper.vault().keys() { if let Some((meta, secret, _)) = device_keeper.read_secret(id).await? + && let Some(urn) = meta.urn() + && urn == &device_key_urn { - if let Some(urn) = meta.urn() { - if urn == &device_key_urn { - device_signer_secret = Some((*id, secret)); - break; - } - } + device_signer_secret = Some((*id, secret)); + break; } } diff --git a/crates/login/src/identity.rs b/crates/login/src/identity.rs index ec3e0ce67e..37b7744598 100644 --- a/crates/login/src/identity.rs +++ b/crates/login/src/identity.rs @@ -7,13 +7,13 @@ //! the delegated passwords used by an account to decrypt //! the folders for the account. use crate::{ - device::DeviceManager, DelegatedAccess, Error, IdentityFolder, - PublicIdentity, Result, + DelegatedAccess, Error, IdentityFolder, PublicIdentity, Result, + device::DeviceManager, }; use async_trait::async_trait; use sos_backend::BackendTarget; use sos_core::{ - crypto::AccessKey, AccountId, AuthenticationError, SecretId, VaultId, + AccountId, AuthenticationError, SecretId, VaultId, crypto::AccessKey, }; use std::collections::HashMap; use urn::Urn; @@ -125,6 +125,18 @@ impl Identity { .ok_or(AuthenticationError::NotAuthenticated)?) } + #[doc(hidden)] + pub fn shared_private_access_key(&self) -> Result { + Ok(AccessKey::Identity( + self.identity()?.private_identity.shared_private.clone(), + )) + } + + /// Public recipient information. + pub fn shared_public_access_key(&self) -> Result { + Ok(self.identity()?.private_identity.shared_public.clone()) + } + /// Verify the access key for this account. pub async fn verify(&self, key: &AccessKey) -> bool { if let Some(identity) = &self.identity { diff --git a/crates/login/src/identity_folder.rs b/crates/login/src/identity_folder.rs index 3b86a17cb5..2a2019d08b 100644 --- a/crates/login/src/identity_folder.rs +++ b/crates/login/src/identity_folder.rs @@ -4,25 +4,25 @@ //! delegated passwords used to decrypt folders managed //! by an account. use crate::{ - device::{DeviceManager, DeviceSigner}, DelegatedAccess, Error, PrivateIdentity, Result, UrnLookup, + device::{DeviceManager, DeviceSigner}, }; use async_trait::async_trait; use secrecy::{ExposeSecret, SecretBox, SecretString}; use sos_backend::{ - database::entity::{AccountEntity, AccountRow, FolderEntity, FolderRow}, AccessPoint, BackendTarget, Folder, FolderEventLog, + database::entity::{AccountEntity, AccountRow, FolderEntity, FolderRow}, }; use sos_core::{ + AccountId, AuthenticationError, VaultFlags, VaultId, constants::LOGIN_AGE_KEY_URN, crypto::AccessKey, encode, - events::EventLogType, AccountId, AuthenticationError, VaultFlags, - VaultId, + events::EventLogType, }; use sos_filesystem::write_exclusive; use sos_vault::Summary; use sos_vault::{ - secret::{Secret, SecretId, SecretMeta, SecretRow, SecretSigner}, BuilderCredentials, SecretAccess, Vault, VaultBuilder, + secret::{Secret, SecretId, SecretMeta, SecretRow, SecretSigner}, }; use std::sync::Arc; use tokio::sync::RwLock; @@ -40,7 +40,7 @@ pub struct IdentityFolder { /// Backend target. target: BackendTarget, /// Private identity. - private_identity: PrivateIdentity, + pub(super) private_identity: PrivateIdentity, /// Device manager. pub(super) devices: Option, } @@ -364,15 +364,14 @@ impl IdentityFolder { for secret_id in keeper.vault().keys() { if let Some((meta, secret, _)) = keeper.read_secret(secret_id).await? + && let Some(urn) = meta.urn() { - if let Some(urn) = meta.urn() { - if urn == &identity_urn { - identity_secret = Some(secret); - } - - // Add to the URN lookup index - index.insert((*keeper.id(), urn.clone()), *secret_id); + if urn == &identity_urn { + identity_secret = Some(secret); } + + // Add to the URN lookup index + index.insert((*keeper.id(), urn.clone()), *secret_id); } } Ok((index, identity_secret)) diff --git a/crates/login/src/lib.rs b/crates/login/src/lib.rs index c34f7a869b..7eafff5348 100644 --- a/crates/login/src/lib.rs +++ b/crates/login/src/lib.rs @@ -1,8 +1,8 @@ -#![deny(missing_docs)] -#![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] //! Identity folder protects delegated passwords and //! is used to authenticate an account. +#![deny(missing_docs)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] mod delegated_access; pub mod device; diff --git a/crates/logs/Cargo.toml b/crates/logs/Cargo.toml index 42cbdddacb..29e88f6e8c 100644 --- a/crates/logs/Cargo.toml +++ b/crates/logs/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-logs" version = "0.17.2" -edition = "2021" +edition = "2024" description = "Log file helpers for the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -19,6 +19,3 @@ tracing-appender.workspace = true tracing-subscriber.workspace = true rev_buf_reader.workspace = true time.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/logs/build.rs b/crates/logs/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/logs/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/logs/src/lib.rs b/crates/logs/src/lib.rs index 91895260b3..edbb95e0f7 100644 --- a/crates/logs/src/lib.rs +++ b/crates/logs/src/lib.rs @@ -1,11 +1,12 @@ +//! Log tracing output to disc. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -//! Log tracing output to disc. +#![cfg_attr(docsrs, feature(doc_cfg))] + mod error; mod logger; pub use error::Error; -pub use logger::{LogFileStatus, Logger, LOG_FILE_NAME}; +pub use logger::{LOG_FILE_NAME, LogFileStatus, Logger}; pub(crate) type Result = std::result::Result; diff --git a/crates/logs/src/logger.rs b/crates/logs/src/logger.rs index 9f87942631..480fb2e665 100644 --- a/crates/logs/src/logger.rs +++ b/crates/logs/src/logger.rs @@ -16,8 +16,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; #[doc(hidden)] pub const LOG_FILE_NAME: &str = "saveoursecrets.log"; -const DEFAULT_LOG_LEVEL: &str = - "sos=info,sos_net=debug,sos_bindings=debug,sos_backend=debug,sos_database=debug,sos_protocol=debug,sos_app=debug,sos_database_upgrader=debug"; +const DEFAULT_LOG_LEVEL: &str = "sos=info,sos_net=debug,sos_bindings=debug,sos_backend=debug,sos_database=debug,sos_protocol=debug,sos_app=debug,sos_database_upgrader=debug"; /// State of the log files on disc. pub struct LogFileStatus { @@ -213,10 +212,10 @@ impl Logger { for entry in std::fs::read_dir(&self.logs_dir)? { let entry = entry?; let path = entry.path(); - if let Some(name) = path.file_name() { - if name.to_string_lossy().starts_with(&self.name) { - files.push(path); - } + if let Some(name) = path.file_name() + && name.to_string_lossy().starts_with(&self.name) + { + files.push(path); } } Ok(files) diff --git a/crates/migrate/Cargo.toml b/crates/migrate/Cargo.toml index a2e7dc11c6..5381e930a0 100644 --- a/crates/migrate/Cargo.toml +++ b/crates/migrate/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-migrate" version = "0.17.1" -edition = "2021" +edition = "2024" description = "Import and export for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -52,5 +52,3 @@ vcard4.workspace = true security-framework = { version = "3.1", optional = true } keychain_parser = { workspace = true, optional = true } -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/migrate/build.rs b/crates/migrate/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/migrate/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/migrate/src/authenticator/export.rs b/crates/migrate/src/authenticator/export.rs index 1ae3de7438..74a05b7aac 100644 --- a/crates/migrate/src/authenticator/export.rs +++ b/crates/migrate/src/authenticator/export.rs @@ -1,11 +1,11 @@ use super::{AuthenticatorUrls, OTP_AUTH_URLS}; use crate::{Error, Result}; use async_zip::{ - tokio::write::ZipFileWriter, Compression, ZipDateTimeBuilder, - ZipEntryBuilder, + Compression, ZipDateTimeBuilder, ZipEntryBuilder, + tokio::write::ZipFileWriter, }; use sos_backend::AccessPoint; -use sos_vault::{secret::Secret, SecretAccess}; +use sos_vault::{SecretAccess, secret::Secret}; use sos_vfs as vfs; use std::{collections::HashMap, path::Path}; use time::OffsetDateTime; diff --git a/crates/migrate/src/authenticator/import.rs b/crates/migrate/src/authenticator/import.rs index 895776ff54..1d96f541c2 100644 --- a/crates/migrate/src/authenticator/import.rs +++ b/crates/migrate/src/authenticator/import.rs @@ -3,8 +3,8 @@ use crate::{Error, Result}; use async_zip::tokio::read::seek::ZipFileReader; use sos_backend::AccessPoint; use sos_vault::{ - secret::{Secret, SecretMeta, SecretRow}, SecretAccess, + secret::{Secret, SecretMeta, SecretRow}, }; use sos_vfs as vfs; use std::path::Path; diff --git a/crates/migrate/src/export.rs b/crates/migrate/src/export.rs index 7b954fdde4..da3a866e9b 100644 --- a/crates/migrate/src/export.rs +++ b/crates/migrate/src/export.rs @@ -1,14 +1,14 @@ //! Export an archive of unencrypted secrets that //! can be used to migrate data to another app. use crate::Result; -use async_zip::{tokio::write::ZipFileWriter, Compression, ZipEntryBuilder}; +use async_zip::{Compression, ZipEntryBuilder, tokio::write::ZipFileWriter}; use secrecy::{ExposeSecret, SecretBox}; use serde::{Deserialize, Serialize}; use sos_backend::AccessPoint; use sos_core::{SecretId, VaultId}; use sos_vault::{ - secret::{FileContent, Secret, SecretMeta}, SecretAccess, Summary, VaultMeta, + secret::{FileContent, Secret, SecretMeta}, }; use std::collections::HashMap; use tokio::io::AsyncWrite; diff --git a/crates/migrate/src/import/csv/bitwarden.rs b/crates/migrate/src/import/csv/bitwarden.rs index be1f4dc8bc..a2d06d2ef2 100644 --- a/crates/migrate/src/import/csv/bitwarden.rs +++ b/crates/migrate/src/import/csv/bitwarden.rs @@ -16,7 +16,7 @@ use super::{ GenericCsvConvert, GenericCsvEntry, GenericNoteRecord, GenericPasswordRecord, UNTITLED, }; -use crate::{import::read_csv_records, Convert, Result}; +use crate::{Convert, Result, import::read_csv_records}; const TYPE_LOGIN: &str = "login"; const TYPE_NOTE: &str = "note"; diff --git a/crates/migrate/src/import/csv/chrome.rs b/crates/migrate/src/import/csv/chrome.rs index 795f39cfae..642c34d57e 100644 --- a/crates/migrate/src/import/csv/chrome.rs +++ b/crates/migrate/src/import/csv/chrome.rs @@ -12,7 +12,7 @@ use url::Url; use super::{ GenericCsvConvert, GenericCsvEntry, GenericPasswordRecord, UNTITLED, }; -use crate::{import::read_csv_records, Convert, Result}; +use crate::{Convert, Result, import::read_csv_records}; /// Record for an entry in a Chrome passwords CSV export. #[derive(Deserialize)] diff --git a/crates/migrate/src/import/csv/dashlane.rs b/crates/migrate/src/import/csv/dashlane.rs index 1a50917a69..a612b66d9f 100644 --- a/crates/migrate/src/import/csv/dashlane.rs +++ b/crates/migrate/src/import/csv/dashlane.rs @@ -2,8 +2,8 @@ use async_trait::async_trait; use serde::Deserialize; -use sos_core::{crypto::AccessKey, UtcDateTime}; -use sos_vault::{secret::IdentityKind, Vault}; +use sos_core::{UtcDateTime, crypto::AccessKey}; +use sos_vault::{Vault, secret::IdentityKind}; use sos_vfs as vfs; use std::{ collections::HashSet, @@ -12,7 +12,7 @@ use std::{ }; use time::{Date, Month}; use url::Url; -use vcard4::{property::DeliveryAddress, Uri, VcardBuilder}; +use vcard4::{Uri, VcardBuilder, property::DeliveryAddress}; use async_zip::tokio::read::seek::ZipFileReader; use tokio::io::{AsyncBufRead, AsyncSeek, BufReader}; @@ -22,7 +22,7 @@ use super::{ GenericIdRecord, GenericNoteRecord, GenericPasswordRecord, GenericPaymentRecord, UNTITLED, }; -use crate::{import::read_csv_records, Convert, Result}; +use crate::{Convert, Result, import::read_csv_records}; /// Record used to deserialize dashlane CSV files. #[derive(Debug)] diff --git a/crates/migrate/src/import/csv/firefox.rs b/crates/migrate/src/import/csv/firefox.rs index a56aa43cbf..3dd54d05c5 100644 --- a/crates/migrate/src/import/csv/firefox.rs +++ b/crates/migrate/src/import/csv/firefox.rs @@ -10,7 +10,7 @@ use tokio::io::AsyncRead; use url::Url; use super::{GenericCsvConvert, GenericCsvEntry, GenericPasswordRecord}; -use crate::{import::read_csv_records, Convert, Result}; +use crate::{Convert, Result, import::read_csv_records}; /// Record for an entry in a Firefox passwords CSV export. #[derive(Deserialize)] diff --git a/crates/migrate/src/import/csv/macos.rs b/crates/migrate/src/import/csv/macos.rs index 6000b1c68c..275f53e66b 100644 --- a/crates/migrate/src/import/csv/macos.rs +++ b/crates/migrate/src/import/csv/macos.rs @@ -12,7 +12,7 @@ use url::Url; use super::{ GenericCsvConvert, GenericCsvEntry, GenericPasswordRecord, UNTITLED, }; -use crate::{import::read_csv_records, Convert, Result}; +use crate::{Convert, Result, import::read_csv_records}; /// Record for an entry in a MacOS passwords CSV export. #[derive(Deserialize)] diff --git a/crates/migrate/src/import/csv/mod.rs b/crates/migrate/src/import/csv/mod.rs index 1471a0c79c..afcc2722f0 100644 --- a/crates/migrate/src/import/csv/mod.rs +++ b/crates/migrate/src/import/csv/mod.rs @@ -10,13 +10,13 @@ pub mod one_password; use crate::Convert; use async_trait::async_trait; use sos_backend::AccessPoint; -use sos_core::{crypto::AccessKey, UtcDateTime}; +use sos_core::{UtcDateTime, crypto::AccessKey}; use sos_search::SearchIndex; use sos_vault::{ + SecretAccess, Vault, secret::{ IdentityKind, Secret, SecretId, SecretMeta, SecretRow, UserData, }, - SecretAccess, Vault, }; use std::collections::{HashMap, HashSet}; use url::Url; diff --git a/crates/migrate/src/import/csv/one_password.rs b/crates/migrate/src/import/csv/one_password.rs index b9be830c17..bd5a838da4 100644 --- a/crates/migrate/src/import/csv/one_password.rs +++ b/crates/migrate/src/import/csv/one_password.rs @@ -2,8 +2,8 @@ use async_trait::async_trait; use serde::{ - de::{self, Deserializer, Unexpected, Visitor}, Deserialize, + de::{self, Deserializer, Unexpected, Visitor}, }; use sos_core::crypto::AccessKey; use sos_vault::Vault; @@ -19,7 +19,7 @@ use url::Url; use super::{ GenericCsvConvert, GenericCsvEntry, GenericPasswordRecord, UNTITLED, }; -use crate::{import::read_csv_records, Convert, Result}; +use crate::{Convert, Result, import::read_csv_records}; /// Record for an entry in a MacOS passwords CSV export. #[derive(Deserialize)] diff --git a/crates/migrate/src/import/keychain/mod.rs b/crates/migrate/src/import/keychain/mod.rs index 69b0d31ee1..92aeeec07a 100644 --- a/crates/migrate/src/import/keychain/mod.rs +++ b/crates/migrate/src/import/keychain/mod.rs @@ -18,15 +18,15 @@ use sos_backend::AccessPoint; use sos_core::crypto::AccessKey; use sos_search::SearchIndex; use sos_vault::{ - secret::{Secret, SecretId, SecretMeta, SecretRow}, SecretAccess, Vault, + secret::{Secret, SecretId, SecretMeta, SecretRow}, }; use std::{ collections::HashMap, io::{BufRead, BufReader, BufWriter, Write}, path::{Path, PathBuf}, process::{Command, Stdio}, - sync::mpsc::{channel, Receiver}, + sync::mpsc::{Receiver, channel}, }; /// Import a MacOS keychain access dump into a vault. diff --git a/crates/migrate/src/lib.rs b/crates/migrate/src/lib.rs index e615728325..466ff30e0c 100644 --- a/crates/migrate/src/lib.rs +++ b/crates/migrate/src/lib.rs @@ -1,9 +1,9 @@ -#![deny(missing_docs)] -#![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] //! Export and import unencrypted data for the [Save Our Secrets](https://saveoursecrets.com) SDK. //! //! Used to move between different apps. +#![deny(missing_docs)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] use async_trait::async_trait; use sos_core::crypto::AccessKey; diff --git a/crates/net/Cargo.toml b/crates/net/Cargo.toml index b588b64c51..e2e84ecc9a 100644 --- a/crates/net/Cargo.toml +++ b/crates/net/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-net" version = "0.17.7" -edition = "2021" +edition = "2024" description = "Networking library for the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -100,9 +100,7 @@ binary-stream.workspace = true rs_merkle.workspace = true prost.workspace = true tokio.workspace = true +age.workspace = true # pairing snow = { workspace = true, optional = true } - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/net/build.rs b/crates/net/build.rs deleted file mode 100644 index 5976a1c6d5..0000000000 --- a/crates/net/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel); -} diff --git a/crates/net/src/account/file_transfers/inflight.rs b/crates/net/src/account/file_transfers/inflight.rs index cd908a5921..1ccc60f7a9 100644 --- a/crates/net/src/account/file_transfers/inflight.rs +++ b/crates/net/src/account/file_transfers/inflight.rs @@ -4,11 +4,11 @@ use sos_protocol::transfer::{CancelReason, TransferOperation}; use std::{ collections::HashMap, sync::{ - atomic::{AtomicU64, Ordering}, Arc, + atomic::{AtomicU64, Ordering}, }, }; -use tokio::sync::{broadcast, RwLock}; +use tokio::sync::{RwLock, broadcast}; use super::{CancelChannel, TransferError}; diff --git a/crates/net/src/account/file_transfers/mod.rs b/crates/net/src/account/file_transfers/mod.rs index 3e7d5548b2..3125af0c60 100644 --- a/crates/net/src/account/file_transfers/mod.rs +++ b/crates/net/src/account/file_transfers/mod.rs @@ -18,12 +18,12 @@ use crate::{Error, Result}; use futures::FutureExt; use sos_core::{ExternalFile, Origin, Paths}; use sos_protocol::{ + SyncClient, network_client::NetworkRetry, transfer::{ CancelReason, FileOperation, FileSyncClient, FileTransferQueueRequest, ProgressChannel, TransferOperation, }, - SyncClient, }; use sos_vfs as vfs; use std::{ @@ -33,7 +33,7 @@ use std::{ time::{Duration, SystemTime}, }; use tokio::{ - sync::{broadcast, mpsc, oneshot, watch, Mutex, RwLock, Semaphore}, + sync::{Mutex, RwLock, Semaphore, broadcast, mpsc, oneshot, watch}, time, }; @@ -335,11 +335,10 @@ where } } - if !items.is_empty() { - if let Err(error) = queue_tx.send(items).await { + if !items.is_empty() + && let Err(error) = queue_tx.send(items).await { tracing::error!(error = ?error, "file_transfers::reinsert"); } - } } Some(events) = queue_rx.recv() => { // println!("queue events: {}", events.len()); @@ -738,11 +737,7 @@ where Ok({ let queue = queue.read().await; - if queue.is_empty() { - None - } else { - Some(()) - } + if queue.is_empty() { None } else { Some(()) } }) } diff --git a/crates/net/src/account/file_transfers/operations.rs b/crates/net/src/account/file_transfers/operations.rs index 14cb9d0e07..753a9b41e1 100644 --- a/crates/net/src/account/file_transfers/operations.rs +++ b/crates/net/src/account/file_transfers/operations.rs @@ -8,17 +8,17 @@ use http::StatusCode; use sos_core::{ExternalFile, Paths}; use sos_protocol::NetworkError; use sos_protocol::{ + Error, SyncClient, network_client::NetworkRetry, transfer::{CancelReason, FileSyncClient}, - Error, SyncClient, }; use sos_vfs as vfs; use std::{io::ErrorKind, sync::Arc}; use tokio::sync::watch; use super::{ - notify_listeners, InflightNotification, InflightTransfers, - ProgressChannel, TransferError, TransferResult, + InflightNotification, InflightTransfers, ProgressChannel, TransferError, + TransferResult, notify_listeners, }; pub struct UploadOperation diff --git a/crates/net/src/account/listen.rs b/crates/net/src/account/listen.rs index d72573c078..3d6875b9cd 100644 --- a/crates/net/src/account/listen.rs +++ b/crates/net/src/account/listen.rs @@ -3,8 +3,8 @@ use crate::{Error, NetworkAccount, Result}; use sos_core::Origin; use sos_protocol::{ - network_client::ListenOptions, NetworkChangeEvent, RemoteResult, - RemoteSync, + NetworkChangeEvent, RemoteResult, RemoteSync, + network_client::ListenOptions, }; use sos_sync::SyncStorage; use std::sync::Arc; diff --git a/crates/net/src/account/network_account.rs b/crates/net/src/account/network_account.rs index 02e24bc9d5..0cf8695bea 100644 --- a/crates/net/src/account/network_account.rs +++ b/crates/net/src/account/network_account.rs @@ -11,32 +11,34 @@ use sos_account::{ use sos_backend::{BackendTarget, Folder, ServerOrigins}; use sos_client_storage::{AccessOptions, NewFolderOptions}; use sos_core::{ + AccountId, AccountRef, AuthenticationError, FolderInvite, FolderRef, + InviteStatus, Origin, Paths, PublicIdentity, Recipient, RemoteOrigins, + SecretId, StorageError, UtcDateTime, VaultCommit, VaultFlags, VaultId, commit::{CommitHash, CommitState}, crypto::{AccessKey, Cipher, KeyDerivation}, device::{DevicePublicKey, TrustedDevice}, + encode, events::{ AccountEvent, DeviceEvent, EventLog, EventLogType, EventRecord, ReadEvent, WriteEvent, }, - AccountId, AccountRef, AuthenticationError, FolderRef, Origin, Paths, - PublicIdentity, RemoteOrigins, SecretId, StorageError, UtcDateTime, - VaultCommit, VaultFlags, VaultId, }; use sos_login::{ - device::{DeviceManager, DeviceSigner}, DelegatedAccess, + device::{DeviceManager, DeviceSigner}, }; use sos_protocol::{ - is_offline, + AccountSync, CreateSharedFolderRequest, DeleteSharedFolderRequest, + DiffRequest, GetFolderInvitesRequest, GetRecipientRequest, RemoteResult, + RemoteSync, SearchRecipientsRequest, SetRecipientRequest, SyncClient, + SyncOptions, SyncResult, UpdateFolderInviteRequest, is_offline, network_client::{HttpClientOptions, NetworkConfig}, - AccountSync, DiffRequest, RemoteResult, RemoteSync, SyncClient, - SyncOptions, SyncResult, }; use sos_remote_sync::RemoteSyncHandler; use sos_sync::{CreateSet, StorageEventLogs, UpdateSet}; use sos_vault::{ + SharedAccess, Summary, Vault, secret::{Secret, SecretMeta, SecretRow, SecretType}, - Summary, Vault, }; use std::{ collections::{HashMap, HashSet}, @@ -47,7 +49,7 @@ use tokio::sync::{Mutex, RwLock}; #[cfg(feature = "clipboard")] use { - sos_account::{xclipboard::Clipboard, ClipboardCopyRequest}, + sos_account::{ClipboardCopyRequest, xclipboard::Clipboard}, sos_core::SecretPath, }; @@ -518,6 +520,214 @@ impl NetworkAccount { handle.shutdown().await; } } + + /// Set recipient information on a server. + pub async fn set_recipient( + &self, + server: &Origin, + name: String, + email: Option, + ) -> Result { + let recipient = Recipient { + name, + email, + public_key: self.shared_access_public_key().await?, + }; + let bridge = self.remote_bridge(server).await?; + let request = SetRecipientRequest { + recipient: recipient.clone(), + }; + bridge.client.set_recipient(request).await?; + Ok(recipient) + } + + /// Fetch recipient information from a server for this account. + pub async fn find_recipient( + &self, + server: &Origin, + ) -> Result> { + let bridge = self.remote_bridge(server).await?; + let request = GetRecipientRequest {}; + let response = bridge.client.get_recipient(request).await?; + Ok(response.recipient) + } + + /// Create a shared folder. + pub async fn create_shared_folder( + &mut self, + options: NewFolderOptions, + server: &Origin, + recipients: &[Recipient], + shared_access: Option, + ) -> Result::NetworkResult>> { + let _ = self.sync_lock.lock().await; + + // Create the vault to send to the server + let (vault, access_key) = { + let account = self.account.lock().await; + account + .prepare_shared_folder(options, recipients, shared_access) + .await? + }; + + let buffer = encode(&vault).await?; + + let bridge = self.remote_bridge(server).await?; + let request = CreateSharedFolderRequest { + vault: buffer.clone(), + recipients: recipients.to_vec(), + }; + + // Try to create the shared folder on the server + bridge.client.create_shared_folder(request).await?; + + let result = self + .import_folder_buffer(&buffer, access_key, false) + .await?; + + let result = FolderCreate { + folder: result.folder, + event: result.event, + commit_state: result.commit_state, + sync_result: self.sync().await, + }; + + Ok(result) + } + + /// Delete a shared folder. + pub async fn delete_shared_folder( + &mut self, + server: &Origin, + folder_id: &VaultId, + ) -> Result::NetworkResult>> { + let bridge = self.remote_bridge(server).await?; + let request = DeleteSharedFolderRequest { + folder_id: *folder_id, + }; + + // Try to delete the shared folder on the server + bridge.client.delete_shared_folder(request).await?; + + // Delete from local storage and sync changes + let _ = self.sync_lock.lock().await; + let result = { + let mut account = self.account.lock().await; + account.delete_folder(folder_id).await? + }; + + let result = FolderDelete { + events: result.events, + commit_state: result.commit_state, + sync_result: self.sync().await, + }; + + Ok(result) + } + + /// List sent folder invites on a server for this account. + pub async fn sent_folder_invites( + &self, + server: &Origin, + invite_status: Option, + limit: Option, + ) -> Result> { + let bridge = self.remote_bridge(server).await?; + let request = GetFolderInvitesRequest { + invite_status, + limit, + }; + let response = bridge.client.sent_folder_invites(request).await?; + Ok(response.folder_invites) + } + + /// List received folder invites on a server for this account. + pub async fn received_folder_invites( + &self, + server: &Origin, + invite_status: Option, + limit: Option, + ) -> Result> { + let bridge = self.remote_bridge(server).await?; + let request = GetFolderInvitesRequest { + invite_status, + limit, + }; + let response = bridge.client.received_folder_invites(request).await?; + Ok(response.folder_invites) + } + + /// Update a folder invite. + async fn update_folder_invite( + &self, + server: &Origin, + invite_status: InviteStatus, + from_public_key: age::x25519::Recipient, + folder_id: VaultId, + ) -> Result<()> { + let bridge = self.remote_bridge(server).await?; + let request = UpdateFolderInviteRequest { + invite_status, + from_public_key, + folder_id, + }; + bridge.client.update_folder_invite(request).await?; + Ok(()) + } + + /// Accept a folder invite. + pub async fn accept_folder_invite( + &mut self, + server: &Origin, + from_public_key: age::x25519::Recipient, + folder_id: VaultId, + ) -> Result<()> { + self.update_folder_invite( + server, + InviteStatus::Accepted, + from_public_key, + folder_id, + ) + .await?; + + let folder_key = { + let account = self.account.lock().await; + account.shared_private_access_key().await? + }; + self.save_folder_password(&folder_id, folder_key).await?; + + self.recover_remote_folder(server, &folder_id).await?; + Ok(()) + } + + /// Decline a folder invite. + pub async fn decline_folder_invite( + &self, + server: &Origin, + from_public_key: age::x25519::Recipient, + folder_id: VaultId, + ) -> Result<()> { + self.update_folder_invite( + server, + InviteStatus::Declined, + from_public_key, + folder_id, + ) + .await + } + + /// Search for recipients. + pub async fn search_recipients( + &self, + server: &Origin, + query: String, + limit: Option, + ) -> Result> { + let bridge = self.remote_bridge(server).await?; + let request = SearchRecipientsRequest { query, limit }; + let response = bridge.client.search_recipients(request).await?; + Ok(response.recipients) + } } impl From<&NetworkAccount> for AccountRef { @@ -654,6 +864,19 @@ impl NetworkAccount { Ok(()) } + + /// Prepare the vault for a new shared folder. + pub async fn prepare_shared_folder( + &self, + options: NewFolderOptions, + recipients: &[Recipient], + shared_access: Option, + ) -> Result<(Vault, AccessKey)> { + let account = self.account.lock().await; + Ok(account + .prepare_shared_folder(options, recipients, shared_access) + .await?) + } } #[async_trait] @@ -684,6 +907,13 @@ impl Account for NetworkAccount { account.is_authenticated().await } + async fn shared_access_public_key( + &self, + ) -> Result { + let account = self.account.lock().await; + Ok(account.shared_access_public_key().await?) + } + async fn import_account_events( &mut self, events: CreateSet, @@ -1531,9 +1761,8 @@ impl Account for NetworkAccount { let result = { let mut account = self.account.lock().await; - let result = - account.update_file(secret_id, meta, path, options).await?; - result + + account.update_file(secret_id, meta, path, options).await? }; let result = SecretChange { @@ -1695,6 +1924,13 @@ impl Account for NetworkAccount { &mut self, folder_id: &VaultId, ) -> Result> { + if let Some(folder) = + self.find_folder(&FolderRef::Id(*folder_id)).await + && folder.flags().is_shared() + { + return Err(Error::SharedFolderOperationNotPermitted(*folder_id)); + } + let _ = self.sync_lock.lock().await; let result = { let mut account = self.account.lock().await; diff --git a/crates/net/src/account/remote.rs b/crates/net/src/account/remote.rs index 6ddbba1aa2..b34fb84087 100644 --- a/crates/net/src/account/remote.rs +++ b/crates/net/src/account/remote.rs @@ -4,8 +4,8 @@ use async_trait::async_trait; use sos_account::LocalAccount; use sos_core::{AccountId, Origin}; use sos_protocol::{ - network_client::{HttpClient, HttpClientOptions}, RemoteResult, RemoteSync, SyncClient, SyncOptions, + network_client::{HttpClient, HttpClientOptions}, }; use sos_remote_sync::{AutoMerge, RemoteSyncHandler}; use sos_sync::{SyncDirection, UpdateSet}; @@ -200,8 +200,8 @@ mod listen { use crate::RemoteBridge; #[cfg(not(target_arch = "wasm32"))] use sos_protocol::{ - network_client::{ListenOptions, WebSocketHandle}, NetworkChangeEvent, + network_client::{ListenOptions, WebSocketHandle}, }; use tokio::sync::mpsc; @@ -222,15 +222,15 @@ mod listen { options: ListenOptions, channel: mpsc::Sender, ) -> WebSocketHandle { - let handle = self.client.listen(options, move |notification| { + + + self.client.listen(options, move |notification| { let tx = channel.clone(); async move { tracing::debug!(notification = ?notification); let _ = tx.send(notification).await; } - }); - - handle + }) } } } diff --git a/crates/net/src/account/sync.rs b/crates/net/src/account/sync.rs index 0b9ef51b86..f3e4e804d5 100644 --- a/crates/net/src/account/sync.rs +++ b/crates/net/src/account/sync.rs @@ -8,9 +8,9 @@ use indexmap::IndexSet; use sos_backend::{AccountEventLog, DeviceEventLog, FolderEventLog}; use sos_core::events::WriteEvent; use sos_core::{ + Origin, VaultId, commit::{CommitState, Comparison}, events::patch::{AccountDiff, CheckedPatch, DeviceDiff, FolderDiff}, - Origin, VaultId, }; use sos_protocol::{ AccountSync, RemoteSync, SyncClient, SyncOptions, SyncResult, diff --git a/crates/net/src/error.rs b/crates/net/src/error.rs index bd801046e4..33f946bcc4 100644 --- a/crates/net/src/error.rs +++ b/crates/net/src/error.rs @@ -1,7 +1,7 @@ //! Error type for network accounts. use sos_core::{AuthenticationError, Origin}; use sos_core::{ErrorExt, VaultId}; -use sos_protocol::{transfer::CancelReason, AsConflict, ConflictError}; +use sos_protocol::{AsConflict, ConflictError, transfer::CancelReason}; use std::error::Error as StdError; use std::path::PathBuf; use thiserror::Error; @@ -46,6 +46,11 @@ pub enum Error { #[error("failed to force update, {0}")] ForceUpdate(Box), + /// Error generated when an operation cannot be permitted due to + /// the folder being shared. + #[error("operation not permitted on shared folder {0}")] + SharedFolderOperationNotPermitted(VaultId), + /// Error generated trying to parse a device enrollment sharing URL. #[deprecated] #[error("invalid share url for device enrollment")] diff --git a/crates/net/src/lib.rs b/crates/net/src/lib.rs index 98948aeaf7..1a1c18adb5 100644 --- a/crates/net/src/lib.rs +++ b/crates/net/src/lib.rs @@ -1,15 +1,14 @@ -#![allow(clippy::result_large_err)] -#![allow(clippy::module_inception)] -#![deny(missing_docs)] -#![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -#![allow(clippy::large_enum_variant)] - //! Networking support for the [Save Our Secrets](https://saveoursecrets.com) SDK. //! //! If the `listen` feature is enabled the client is compiled //! with support for sending and listening for change notification over //! a websocket connection. +#![allow(clippy::result_large_err)] +#![allow(clippy::module_inception)] +#![deny(missing_docs)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![allow(clippy::large_enum_variant)] mod account; mod error; diff --git a/crates/net/src/pairing/enrollment.rs b/crates/net/src/pairing/enrollment.rs index e2c11b38b0..2f54c7b24e 100644 --- a/crates/net/src/pairing/enrollment.rs +++ b/crates/net/src/pairing/enrollment.rs @@ -1,7 +1,7 @@ //! Enroll a device to an account on a remote server. use crate::{ - pairing::{Error, Result}, NetworkAccount, + pairing::{Error, Result}, }; use sos_account::Account; use sos_backend::{BackendTarget, ServerOrigins}; @@ -9,12 +9,12 @@ use sos_client_storage::{ ClientAccountStorage, ClientBaseStorage, ClientStorage, }; use sos_core::{ - crypto::AccessKey, AccountId, Origin, PublicIdentity, RemoteOrigins, + AccountId, Origin, PublicIdentity, RemoteOrigins, crypto::AccessKey, }; use sos_login::device::DeviceSigner; use sos_protocol::{ - network_client::{HttpClient, HttpClientOptions, NetworkConfig}, SyncClient, + network_client::{HttpClient, HttpClientOptions, NetworkConfig}, }; use sos_signer::ed25519::BoxedEd25519Signer; use std::collections::HashSet; diff --git a/crates/net/src/pairing/share_url.rs b/crates/net/src/pairing/share_url.rs index 426aa51e34..63a3118569 100644 --- a/crates/net/src/pairing/share_url.rs +++ b/crates/net/src/pairing/share_url.rs @@ -1,7 +1,7 @@ use super::{Error, Result}; use hex; use rand::Rng; -use sos_core::{csprng, AccountId}; +use sos_core::{AccountId, csprng}; use std::str::FromStr; use url::Url; @@ -37,7 +37,7 @@ impl ServerPairUrl { server: Url, public_key: Vec, ) -> Self { - let pre_shared_key: [u8; 32] = csprng().gen(); + let pre_shared_key: [u8; 32] = csprng().r#gen(); Self { account_id, server, @@ -97,43 +97,23 @@ impl FromStr for ServerPairUrl { let mut pairs = url.query_pairs(); - let account_id = pairs.find_map(|q| { - if q.0.as_ref() == AID { - Some(q.1) - } else { - None - } - }); + let account_id = pairs + .find_map(|q| if q.0.as_ref() == AID { Some(q.1) } else { None }); let account_id = account_id.ok_or(Error::InvalidShareUrl)?; let account_id: AccountId = account_id.as_ref().parse()?; - let server = pairs.find_map(|q| { - if q.0.as_ref() == URL { - Some(q.1) - } else { - None - } - }); + let server = pairs + .find_map(|q| if q.0.as_ref() == URL { Some(q.1) } else { None }); let server = server.ok_or(Error::InvalidShareUrl)?; let server: Url = server.as_ref().parse()?; - let key = pairs.find_map(|q| { - if q.0.as_ref() == KEY { - Some(q.1) - } else { - None - } - }); + let key = pairs + .find_map(|q| if q.0.as_ref() == KEY { Some(q.1) } else { None }); let key = key.ok_or(Error::InvalidShareUrl)?; let key = hex::decode(key.as_ref())?; - let psk = pairs.find_map(|q| { - if q.0.as_ref() == PSK { - Some(q.1) - } else { - None - } - }); + let psk = pairs + .find_map(|q| if q.0.as_ref() == PSK { Some(q.1) } else { None }); let psk = psk.ok_or(Error::InvalidShareUrl)?; let psk = hex::decode(psk.as_ref())?; let psk: [u8; 32] = psk.as_slice().try_into()?; diff --git a/crates/net/src/pairing/websocket.rs b/crates/net/src/pairing/websocket.rs index 35b5234ba2..f2e5a38972 100644 --- a/crates/net/src/pairing/websocket.rs +++ b/crates/net/src/pairing/websocket.rs @@ -2,31 +2,30 @@ use super::{DeviceEnrollment, Error, Result, ServerPairUrl}; use crate::NetworkAccount; use futures::{ - stream::{SplitSink, SplitStream}, SinkExt, StreamExt, + stream::{SplitSink, SplitStream}, }; use snow::{Builder, HandshakeState, Keypair, TransportState}; use sos_account::Account; use sos_backend::BackendTarget; use sos_core::{ + AccountId, Origin, device::{DeviceMetaData, DevicePublicKey, TrustedDevice}, events::DeviceEvent, - AccountId, Origin, }; use sos_protocol::{ + AccountSync, PairingConfirm, PairingMessage, PairingReady, + PairingRequest, ProtoMessage, RelayHeader, RelayPacket, RelayPayload, + SyncOptions, network_client::{NetworkConfig, WebSocketRequest}, pairing_message, tokio_tungstenite::{ - connect_async, + MaybeTlsStream, WebSocketStream, connect_async, tungstenite::{ - protocol::{frame::coding::CloseCode, CloseFrame, Message}, Utf8Bytes, + protocol::{CloseFrame, Message, frame::coding::CloseCode}, }, - MaybeTlsStream, WebSocketStream, }, - AccountSync, PairingConfirm, PairingMessage, PairingReady, - PairingRequest, ProtoMessage, RelayHeader, RelayPacket, RelayPayload, - SyncOptions, }; use std::collections::HashSet; use tokio::{net::TcpStream, sync::mpsc}; diff --git a/crates/password/Cargo.toml b/crates/password/Cargo.toml index 8dafaf23ba..5bd359b5dc 100644 --- a/crates/password/Cargo.toml +++ b/crates/password/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-password" version = "0.17.1" -edition = "2021" +edition = "2024" description = "Password generation and helpers for the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -18,6 +18,3 @@ rand.workspace = true chbs.workspace = true zxcvbn.workspace = true secrecy.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/password/build.rs b/crates/password/build.rs deleted file mode 100644 index 5976a1c6d5..0000000000 --- a/crates/password/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel); -} diff --git a/crates/password/src/diceware.rs b/crates/password/src/diceware.rs index 071db32239..c95f74c333 100644 --- a/crates/password/src/diceware.rs +++ b/crates/password/src/diceware.rs @@ -41,13 +41,13 @@ pub fn generate_passphrase() -> Result<(SecretString, f64)> { /// Get the default config for diceware passphrase generation. pub fn default_config(words: usize) -> BasicConfig { - let config = BasicConfigBuilder::default() + + BasicConfigBuilder::default() .word_provider(WORD_LIST.sampler()) .words(words) .separator(' ') .capitalize_first(Probability::Never) .capitalize_words(Probability::Never) .build() - .unwrap(); - config + .unwrap() } diff --git a/crates/password/src/error.rs b/crates/password/src/error.rs index f7e4c18d59..4defe4b97b 100644 --- a/crates/password/src/error.rs +++ b/crates/password/src/error.rs @@ -5,6 +5,8 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum Error { /// Error generated when secret meta data does not exist. - #[error("too few words for diceware passphrase generation, got {0} but minimum is {1}")] + #[error( + "too few words for diceware passphrase generation, got {0} but minimum is {1}" + )] DicewareWordsTooFew(usize, u8), } diff --git a/crates/password/src/generator.rs b/crates/password/src/generator.rs index 9f1821cb94..a681b15ca3 100644 --- a/crates/password/src/generator.rs +++ b/crates/password/src/generator.rs @@ -4,7 +4,7 @@ use chbs::{config::BasicConfig, word::WordSampler}; use rand::Rng; use secrecy::{ExposeSecret, SecretString}; use sos_core::csprng; -use zxcvbn::{zxcvbn, Entropy}; +use zxcvbn::{Entropy, zxcvbn}; const ROMAN_LOWER: &str = "abcdefghijklmnopqrstuvwxyz"; const ROMAN_UPPER: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; diff --git a/crates/password/src/lib.rs b/crates/password/src/lib.rs index c71aa2f176..4803cc0d2e 100644 --- a/crates/password/src/lib.rs +++ b/crates/password/src/lib.rs @@ -1,5 +1,8 @@ //! Password generation and helpers for the //! [Save Our Secrets](https://saveoursecrets.com) SDK. +#![deny(missing_docs)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] #![allow(clippy::len_without_is_empty)] pub mod diceware; diff --git a/crates/password/src/memorable.rs b/crates/password/src/memorable.rs index 6bf6d3dbd1..a7ca7b62f6 100644 --- a/crates/password/src/memorable.rs +++ b/crates/password/src/memorable.rs @@ -1,6 +1,6 @@ use crate::{CONSONANTS, DIGITS, VOWELS}; -use rand::rngs::OsRng; use rand::Rng; +use rand::rngs::OsRng; /// Memorable password generator. pub fn memorable_password(num_words: usize) -> String { diff --git a/crates/platform_authenticator/Cargo.toml b/crates/platform_authenticator/Cargo.toml index 6e142b8ca0..f02819ac92 100644 --- a/crates/platform_authenticator/Cargo.toml +++ b/crates/platform_authenticator/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-platform-authenticator" version = "0.17.1" -edition = "2021" +edition = "2024" description = "Platform authenticator and keyring suppport for the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -26,6 +26,3 @@ robius-authentication = "0.1" [target.'cfg(target_os = "macos")'.dependencies.security-framework] version = "3.1" # path = "../../../../forks/rust-security-framework/security-framework" - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/platform_authenticator/build.rs b/crates/platform_authenticator/build.rs deleted file mode 100644 index 5976a1c6d5..0000000000 --- a/crates/platform_authenticator/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel); -} diff --git a/crates/platform_authenticator/src/lib.rs b/crates/platform_authenticator/src/lib.rs index bc0afa2042..0c21b74107 100644 --- a/crates/platform_authenticator/src/lib.rs +++ b/crates/platform_authenticator/src/lib.rs @@ -2,7 +2,7 @@ //! [Save Our Secrets](https://saveoursecrets.com) SDK. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] use secrecy::SecretString; mod error; diff --git a/crates/preferences/Cargo.toml b/crates/preferences/Cargo.toml index ca2046bb64..766928eb40 100644 --- a/crates/preferences/Cargo.toml +++ b/crates/preferences/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-preferences" version = "0.17.5" -edition = "2021" +edition = "2024" description = "Preferences for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -13,12 +13,8 @@ rustdoc-args = ["--cfg", "docsrs"] [dependencies] sos-core.workspace = true - thiserror.workspace = true async-trait.workspace = true tokio.workspace = true serde.workspace = true serde_json.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/preferences/build.rs b/crates/preferences/build.rs deleted file mode 100644 index 5976a1c6d5..0000000000 --- a/crates/preferences/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel); -} diff --git a/crates/preferences/src/lib.rs b/crates/preferences/src/lib.rs index 025d1a1042..a9cc81ea3c 100644 --- a/crates/preferences/src/lib.rs +++ b/crates/preferences/src/lib.rs @@ -1,3 +1,7 @@ +//! Preferences management for the [Save Our Secrets](https://saveoursecrets.com) SDK. +#![deny(missing_docs)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] mod error; mod preferences; diff --git a/crates/protocol/Cargo.toml b/crates/protocol/Cargo.toml index 7438d76c93..5da24ab8a9 100644 --- a/crates/protocol/Cargo.toml +++ b/crates/protocol/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-protocol" version = "0.17.5" -edition = "2021" +edition = "2024" description = "Networking and sync protocol types for the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -60,6 +60,7 @@ binary-stream.workspace = true typeshare.workspace = true sha2.workspace = true tokio = { version = "1", features = ["rt", "macros"] } +age.workspace = true # network-client tokio-tungstenite = { workspace = true, optional = true } @@ -82,6 +83,5 @@ features = ["json", "stream"] optional = true [build-dependencies] -rustc_version.workspace = true prost-build.workspace = true protoc-bin-vendored.workspace = true diff --git a/crates/protocol/build.rs b/crates/protocol/build.rs index e9e6eb3693..7c87af17c5 100644 --- a/crates/protocol/build.rs +++ b/crates/protocol/build.rs @@ -1,22 +1,12 @@ extern crate prost_build; -use rustc_version::{version_meta, Channel}; fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel); - - std::env::set_var( - "PROTOC", - protoc_bin_vendored::protoc_bin_path().unwrap(), - ); + unsafe { + std::env::set_var( + "PROTOC", + protoc_bin_vendored::protoc_bin_path().unwrap(), + ); + } prost_build::compile_protos( &[ @@ -27,6 +17,7 @@ fn main() { "src/protobuf/patch.proto", "src/protobuf/relay.proto", "src/protobuf/scan.proto", + "src/protobuf/shared_folder.proto", "src/protobuf/sync.proto", ], &["src"], diff --git a/crates/protocol/src/bindings/common.rs b/crates/protocol/src/bindings/common.rs index 9f38ad1922..57d9fb6b26 100644 --- a/crates/protocol/src/bindings/common.rs +++ b/crates/protocol/src/bindings/common.rs @@ -1,11 +1,11 @@ include!(concat!(env!("OUT_DIR"), "/common.rs")); -use crate::{decode_uuid, encode_uuid, Error, ProtoBinding, Result}; -use rs_merkle::{algorithms::Sha256, MerkleProof}; +use crate::{Error, ProtoBinding, Result, decode_uuid, encode_uuid}; +use rs_merkle::{MerkleProof, algorithms::Sha256}; use sos_core::{ + FolderInvite, InviteStatus, Recipient, SecretPath, UtcDateTime, commit::{CommitHash, CommitProof, CommitState}, - events::{patch::CheckedPatch, EventLogType, EventRecord}, - SecretPath, UtcDateTime, + events::{EventLogType, EventRecord, patch::CheckedPatch}, }; use time::{Duration, OffsetDateTime}; @@ -277,3 +277,94 @@ impl From for WireSecretPath { } } } + +impl ProtoBinding for Recipient { + type Inner = WireRecipient; +} + +impl TryFrom for Recipient { + type Error = Error; + + fn try_from(value: WireRecipient) -> Result { + Ok(Recipient { + name: value.name, + email: value.email, + public_key: value + .public_key + .parse::() + .map_err(Error::AgeX25519Parse)?, + }) + } +} + +impl From for WireRecipient { + fn from(value: Recipient) -> Self { + WireRecipient { + name: value.name, + email: value.email, + public_key: value.public_key.to_string(), + } + } +} + +impl TryFrom for InviteStatus { + type Error = Error; + fn try_from(value: WireInviteStatus) -> Result { + Ok((value as i32).try_into()?) + } +} + +impl From for WireInviteStatus { + fn from(value: InviteStatus) -> WireInviteStatus { + match value { + InviteStatus::Pending => { + WireInviteStatus::from_str_name("Pending").unwrap() + } + InviteStatus::Accepted => { + WireInviteStatus::from_str_name("Accepted").unwrap() + } + InviteStatus::Declined => { + WireInviteStatus::from_str_name("Declined").unwrap() + } + } + } +} + +impl ProtoBinding for FolderInvite { + type Inner = WireFolderInvite; +} + +impl TryFrom for FolderInvite { + type Error = Error; + + fn try_from(value: WireFolderInvite) -> Result { + Ok(FolderInvite { + created_at: value.created_at.unwrap().try_into()?, + modified_at: value.modified_at.unwrap().try_into()?, + invite_status: value.invite_status.try_into()?, + folder_id: value.folder_id.parse()?, + folder_name: value.folder_name, + recipient_name: value.recipient_name, + recipient_email: value.recipient_email, + recipient_public_key: value + .recipient_public_key + .parse() + .map_err(Error::AgeX25519Parse)?, + }) + } +} + +impl From for WireFolderInvite { + fn from(value: FolderInvite) -> Self { + WireFolderInvite { + created_at: Some(value.created_at.into()), + modified_at: Some(value.modified_at.into()), + invite_status: WireInviteStatus::from(value.invite_status) as i32, + folder_id: value.folder_id.to_string(), + folder_name: value.folder_name, + recipient_name: value.recipient_name, + recipient_email: value.recipient_email, + recipient_public_key: value.recipient_public_key.to_string(), + } + } +} diff --git a/crates/protocol/src/bindings/files.rs b/crates/protocol/src/bindings/files.rs index fb9e24163b..08c82450f0 100644 --- a/crates/protocol/src/bindings/files.rs +++ b/crates/protocol/src/bindings/files.rs @@ -5,9 +5,8 @@ include!(concat!(env!("OUT_DIR"), "/files.rs")); mod files { use super::*; use crate::{ - decode_uuid, encode_uuid, + Error, ProtoBinding, Result, decode_uuid, encode_uuid, transfer::{FileSet, FileTransfersSet}, - Error, ProtoBinding, Result, }; use indexmap::IndexSet; use sos_core::{ExternalFile, ExternalFileName, SecretPath}; diff --git a/crates/protocol/src/bindings/mod.rs b/crates/protocol/src/bindings/mod.rs index 0297988653..25450e2b49 100644 --- a/crates/protocol/src/bindings/mod.rs +++ b/crates/protocol/src/bindings/mod.rs @@ -7,6 +7,7 @@ mod patch; #[cfg(feature = "pairing")] mod relay; mod scan; +mod shared_folder; mod sync; pub use diff::{DiffRequest, DiffResponse}; @@ -16,7 +17,15 @@ pub use patch::{PatchRequest, PatchResponse}; #[cfg(feature = "pairing")] #[doc(hidden)] pub use relay::{ - pairing_message, PairingConfirm, PairingMessage, PairingReady, - PairingRequest, RelayHeader, RelayPacket, RelayPayload, + PairingConfirm, PairingMessage, PairingReady, PairingRequest, + RelayHeader, RelayPacket, RelayPayload, pairing_message, }; pub use scan::{ScanRequest, ScanResponse}; +pub use shared_folder::{ + CreateSharedFolderRequest, CreateSharedFolderResponse, + DeleteSharedFolderRequest, DeleteSharedFolderResponse, + GetFolderInvitesRequest, GetFolderInvitesResponse, GetRecipientRequest, + GetRecipientResponse, SearchRecipientsRequest, SearchRecipientsResponse, + SetRecipientRequest, SetRecipientResponse, UpdateFolderInviteRequest, + UpdateFolderInviteResponse, +}; diff --git a/crates/protocol/src/bindings/notifications.rs b/crates/protocol/src/bindings/notifications.rs index c517904cef..86c77d8b29 100644 --- a/crates/protocol/src/bindings/notifications.rs +++ b/crates/protocol/src/bindings/notifications.rs @@ -1,7 +1,7 @@ include!(concat!(env!("OUT_DIR"), "/notifications.rs")); use crate::{Error, ProtoBinding, Result}; -use sos_core::{commit::CommitHash, AccountId}; +use sos_core::{AccountId, commit::CommitHash}; use sos_sync::MergeOutcome; /// Notification sent by the server when changes were made. diff --git a/crates/protocol/src/bindings/patch.rs b/crates/protocol/src/bindings/patch.rs index 646e060720..c3dd9b3997 100644 --- a/crates/protocol/src/bindings/patch.rs +++ b/crates/protocol/src/bindings/patch.rs @@ -3,7 +3,7 @@ include!(concat!(env!("OUT_DIR"), "/patch.rs")); use crate::{Error, ProtoBinding, Result}; use sos_core::{ commit::{CommitHash, CommitProof}, - events::{patch::CheckedPatch, EventLogType, EventRecord}, + events::{EventLogType, EventRecord, patch::CheckedPatch}, }; /// Request to patch an event log from a specific commit. diff --git a/crates/protocol/src/bindings/shared_folder.rs b/crates/protocol/src/bindings/shared_folder.rs new file mode 100644 index 0000000000..d719ff5c47 --- /dev/null +++ b/crates/protocol/src/bindings/shared_folder.rs @@ -0,0 +1,455 @@ +include!(concat!(env!("OUT_DIR"), "/shared_folder.rs")); + +use crate::{ + Error, ProtoBinding, Result, bindings::common::WireInviteStatus, +}; +use serde::{Deserialize, Serialize}; +use sos_core::{FolderInvite, InviteStatus, Recipient, VaultId}; + +/// Request to create or update recipient information. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SetRecipientRequest { + /// Recipient information. + pub recipient: Recipient, +} + +impl ProtoBinding for SetRecipientRequest { + type Inner = WireSetRecipientRequest; +} + +impl TryFrom for SetRecipientRequest { + type Error = Error; + + fn try_from(value: WireSetRecipientRequest) -> Result { + Ok(Self { + recipient: value.recipient.unwrap().try_into()?, + }) + } +} + +impl From for WireSetRecipientRequest { + fn from(value: SetRecipientRequest) -> WireSetRecipientRequest { + Self { + recipient: Some(value.recipient.into()), + } + } +} + +/// Response from a request to set recipient information. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SetRecipientResponse {} + +impl ProtoBinding for SetRecipientResponse { + type Inner = WireSetRecipientResponse; +} + +impl TryFrom for SetRecipientResponse { + type Error = Error; + + fn try_from(_value: WireSetRecipientResponse) -> Result { + Ok(Self {}) + } +} + +impl From for WireSetRecipientResponse { + fn from(_value: SetRecipientResponse) -> WireSetRecipientResponse { + Self {} + } +} + +/// Response with recipient information. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GetRecipientResponse { + /// Recipient information. + pub recipient: Option, +} + +impl ProtoBinding for GetRecipientResponse { + type Inner = WireGetRecipientResponse; +} + +impl TryFrom for GetRecipientResponse { + type Error = Error; + + fn try_from(value: WireGetRecipientResponse) -> Result { + let recipient = if let Some(recipient) = value.recipient { + Some(recipient.try_into()?) + } else { + None + }; + Ok(Self { recipient }) + } +} + +impl From for WireGetRecipientResponse { + fn from(value: GetRecipientResponse) -> WireGetRecipientResponse { + Self { + recipient: value.recipient.map(|r| r.into()), + } + } +} + +/// Request to get recipient information. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GetRecipientRequest {} + +impl ProtoBinding for GetRecipientRequest { + type Inner = WireGetRecipientRequest; +} + +impl TryFrom for GetRecipientRequest { + type Error = Error; + + fn try_from(_value: WireGetRecipientRequest) -> Result { + Ok(Self {}) + } +} + +impl From for WireGetRecipientRequest { + fn from(_value: GetRecipientRequest) -> WireGetRecipientRequest { + Self {} + } +} + +/// Request to create a shared folder on a remote. +/// +/// Used during auto merge to force push a combined collection +/// of events. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CreateSharedFolderRequest { + /// Encoded vault. + pub vault: Vec, + /// List of recipients. + pub recipients: Vec, +} + +impl ProtoBinding for CreateSharedFolderRequest { + type Inner = WireCreateSharedFolderRequest; +} + +impl TryFrom for CreateSharedFolderRequest { + type Error = Error; + + fn try_from(value: WireCreateSharedFolderRequest) -> Result { + let mut recipients = Vec::with_capacity(value.recipients.len()); + for recipient in value.recipients { + recipients.push(recipient.try_into()?); + } + Ok(Self { + vault: value.vault, + recipients, + }) + } +} + +impl From for WireCreateSharedFolderRequest { + fn from( + value: CreateSharedFolderRequest, + ) -> WireCreateSharedFolderRequest { + Self { + vault: value.vault, + recipients: value + .recipients + .into_iter() + .map(|r| r.into()) + .collect(), + } + } +} + +/// Response from a create shared folder request. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CreateSharedFolderResponse {} + +impl ProtoBinding for CreateSharedFolderResponse { + type Inner = WireCreateSharedFolderResponse; +} + +impl TryFrom for CreateSharedFolderResponse { + type Error = Error; + + fn try_from(_value: WireCreateSharedFolderResponse) -> Result { + Ok(Self {}) + } +} + +impl From for WireCreateSharedFolderResponse { + fn from( + _value: CreateSharedFolderResponse, + ) -> WireCreateSharedFolderResponse { + Self {} + } +} + +/// Request to get folder invites. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct GetFolderInvitesRequest { + /// Invite status. + pub invite_status: Option, + /// Limit the number of rows. + pub limit: Option, +} + +impl ProtoBinding for GetFolderInvitesRequest { + type Inner = WireGetFolderInvitesRequest; +} + +impl TryFrom for GetFolderInvitesRequest { + type Error = Error; + + fn try_from(value: WireGetFolderInvitesRequest) -> Result { + let invite_status = if let Some(invite_status) = value.invite_status { + Some(invite_status.try_into()?) + } else { + None + }; + Ok(Self { + invite_status, + limit: value.limit.map(|l| l as usize), + }) + } +} + +impl From for WireGetFolderInvitesRequest { + fn from(value: GetFolderInvitesRequest) -> WireGetFolderInvitesRequest { + Self { + invite_status: value + .invite_status + .map(|s| WireInviteStatus::from(s) as i32), + limit: value.limit.map(|l| l as u32), + } + } +} + +/// Response from a request to get folder invites. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GetFolderInvitesResponse { + /// Folder invites. + pub folder_invites: Vec, +} + +impl ProtoBinding for GetFolderInvitesResponse { + type Inner = WireGetFolderInvitesResponse; +} + +impl TryFrom for GetFolderInvitesResponse { + type Error = Error; + + fn try_from(value: WireGetFolderInvitesResponse) -> Result { + let mut folder_invites = + Vec::with_capacity(value.folder_invites.len()); + for invite in value.folder_invites { + folder_invites.push(invite.try_into()?); + } + Ok(Self { folder_invites }) + } +} + +impl From for WireGetFolderInvitesResponse { + fn from(value: GetFolderInvitesResponse) -> WireGetFolderInvitesResponse { + Self { + folder_invites: value + .folder_invites + .into_iter() + .map(|f| f.into()) + .collect(), + } + } +} + +/// Request to update a folder invite with a new status. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UpdateFolderInviteRequest { + /// New update status. + pub invite_status: InviteStatus, + /// Public key of the recipient that sent the invite. + pub from_public_key: age::x25519::Recipient, + /// Folder identifier. + pub folder_id: VaultId, +} + +impl ProtoBinding for UpdateFolderInviteRequest { + type Inner = WireUpdateFolderInviteRequest; +} + +impl TryFrom for UpdateFolderInviteRequest { + type Error = Error; + + fn try_from(value: WireUpdateFolderInviteRequest) -> Result { + Ok(Self { + invite_status: value.invite_status.try_into()?, + from_public_key: value + .from_public_key + .parse() + .map_err(Error::AgeX25519Parse)?, + folder_id: value.folder_id.parse()?, + }) + } +} + +impl From for WireUpdateFolderInviteRequest { + fn from( + value: UpdateFolderInviteRequest, + ) -> WireUpdateFolderInviteRequest { + Self { + invite_status: WireInviteStatus::from(value.invite_status) as i32, + from_public_key: value.from_public_key.to_string(), + folder_id: value.folder_id.to_string(), + } + } +} + +/// Response from a request to get folder invites. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct UpdateFolderInviteResponse {} + +impl ProtoBinding for UpdateFolderInviteResponse { + type Inner = WireUpdateFolderInviteResponse; +} + +impl TryFrom for UpdateFolderInviteResponse { + type Error = Error; + + fn try_from(_value: WireUpdateFolderInviteResponse) -> Result { + Ok(Self {}) + } +} + +impl From for WireUpdateFolderInviteResponse { + fn from( + _value: UpdateFolderInviteResponse, + ) -> WireUpdateFolderInviteResponse { + Self {} + } +} + +/// Request to search for recipients. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SearchRecipientsRequest { + /// Search query. + pub query: String, + /// Limit the number of rows. + pub limit: Option, +} + +impl ProtoBinding for SearchRecipientsRequest { + type Inner = WireSearchRecipientsRequest; +} + +impl TryFrom for SearchRecipientsRequest { + type Error = Error; + + fn try_from(value: WireSearchRecipientsRequest) -> Result { + Ok(Self { + query: value.query, + limit: value.limit.map(|l| l as usize), + }) + } +} + +impl From for WireSearchRecipientsRequest { + fn from(value: SearchRecipientsRequest) -> WireSearchRecipientsRequest { + Self { + query: value.query, + limit: value.limit.map(|l| l as u32), + } + } +} + +/// Response from a request to search for recipients. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SearchRecipientsResponse { + /// Matched recipients. + pub recipients: Vec, +} + +impl ProtoBinding for SearchRecipientsResponse { + type Inner = WireSearchRecipientsResponse; +} + +impl TryFrom for SearchRecipientsResponse { + type Error = Error; + + fn try_from(value: WireSearchRecipientsResponse) -> Result { + let mut recipients = Vec::with_capacity(value.recipients.len()); + for recipient in value.recipients { + recipients.push(recipient.try_into()?); + } + Ok(Self { recipients }) + } +} + +impl From for WireSearchRecipientsResponse { + fn from(value: SearchRecipientsResponse) -> WireSearchRecipientsResponse { + Self { + recipients: value + .recipients + .into_iter() + .map(|f| f.into()) + .collect(), + } + } +} + +/// Request to delete a folder invite. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeleteSharedFolderRequest { + /// Folder identifier. + pub folder_id: VaultId, +} + +impl ProtoBinding for DeleteSharedFolderRequest { + type Inner = WireDeleteSharedFolderRequest; +} + +impl TryFrom for DeleteSharedFolderRequest { + type Error = Error; + + fn try_from(value: WireDeleteSharedFolderRequest) -> Result { + Ok(Self { + folder_id: value.folder_id.parse()?, + }) + } +} + +impl From for WireDeleteSharedFolderRequest { + fn from( + value: DeleteSharedFolderRequest, + ) -> WireDeleteSharedFolderRequest { + Self { + folder_id: value.folder_id.to_string(), + } + } +} + +/// Response from a request to delete a folder. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DeleteSharedFolderResponse { + /// Whether the account deleting the folder + /// also created the folder. + pub is_creator: bool, +} + +impl ProtoBinding for DeleteSharedFolderResponse { + type Inner = WireDeleteSharedFolderResponse; +} + +impl TryFrom for DeleteSharedFolderResponse { + type Error = Error; + + fn try_from(value: WireDeleteSharedFolderResponse) -> Result { + Ok(Self { + is_creator: value.is_creator, + }) + } +} + +impl From for WireDeleteSharedFolderResponse { + fn from( + value: DeleteSharedFolderResponse, + ) -> WireDeleteSharedFolderResponse { + Self { + is_creator: value.is_creator, + } + } +} diff --git a/crates/protocol/src/bindings/sync.rs b/crates/protocol/src/bindings/sync.rs index 80563ae81c..ecd56f8b2a 100644 --- a/crates/protocol/src/bindings/sync.rs +++ b/crates/protocol/src/bindings/sync.rs @@ -1,14 +1,14 @@ include!(concat!(env!("OUT_DIR"), "/sync.rs")); -use crate::{decode_uuid, encode_uuid, Error, ProtoBinding, Result}; +use crate::{Error, ProtoBinding, Result, decode_uuid, encode_uuid}; use indexmap::{IndexMap, IndexSet}; use sos_core::{ + Origin, commit::Comparison, events::{ - patch::{Diff, Patch}, EventRecord, + patch::{Diff, Patch}, }, - Origin, }; use sos_sync::{ CreateSet, MaybeDiff, MergeOutcome, SyncCompare, SyncDiff, SyncPacket, @@ -764,11 +764,11 @@ impl From for WireTrackedDeviceChange { #[cfg(feature = "files")] mod files { use super::{ - wire_tracked_file_change, WireTrackedFileChange, - WireTrackedFileDeleted, WireTrackedFileMoved, + WireTrackedFileChange, WireTrackedFileDeleted, WireTrackedFileMoved, + wire_tracked_file_change, }; use crate::{ - bindings::sync::WireTrackedFileCreated, Error, ProtoBinding, Result, + Error, ProtoBinding, Result, bindings::sync::WireTrackedFileCreated, }; use sos_sync::TrackedFileChange; diff --git a/crates/protocol/src/constants.rs b/crates/protocol/src/constants.rs index cbd0559ab1..8dfbb3a17c 100644 --- a/crates/protocol/src/constants.rs +++ b/crates/protocol/src/constants.rs @@ -34,6 +34,28 @@ pub mod routes { /// Route for syncing account events. pub const SYNC_ACCOUNT_EVENTS: &str = "/api/v1/sync/account/events"; + + /// Route for set and get recipient information. + pub const SHARING_RECIPIENT: &str = "/api/v1/sharing/recipient"; + + /// Route for creating and deleting a shared folder. + pub const SHARING_FOLDER: &str = "/api/v1/sharing/folder"; + + /// Route for listing sent invites. + pub const SHARING_SENT_INVITES: &str = + "/api/v1/sharing/folder/invites/sent"; + + /// Route for listing received invites. + pub const SHARING_RECEIVED_INVITES: &str = + "/api/v1/sharing/folder/invites/inbox"; + + /// Route for updating folder invite. + pub const SHARING_UPDATE_INVITE: &str = + "/api/v1/sharing/folder/invites"; + + /// Route for searching recipients. + pub const SHARING_SEARCH_RECIPIENTS: &str = + "/api/v1/sharing/recipient/search"; } } diff --git a/crates/protocol/src/diff.rs b/crates/protocol/src/diff.rs index 1beb67920c..1a5d293e37 100644 --- a/crates/protocol/src/diff.rs +++ b/crates/protocol/src/diff.rs @@ -1,9 +1,9 @@ //! Types and functions to compute diffs. use indexmap::IndexMap; use sos_core::{ - commit::Comparison, - events::{patch::FolderDiff, EventLog}, VaultId, + commit::Comparison, + events::{EventLog, patch::FolderDiff}, }; use sos_sync::{ MaybeDiff, StorageEventLogs, SyncDiff, SyncStatus, SyncStorage, diff --git a/crates/protocol/src/error.rs b/crates/protocol/src/error.rs index e22689e8b7..f7d5d1fe85 100644 --- a/crates/protocol/src/error.rs +++ b/crates/protocol/src/error.rs @@ -26,6 +26,10 @@ pub enum Error { #[error("relay packet end of file")] EndOfFile, + /// Failed to parse AGE X25519 public key. + #[error("unable to parse AGE X25519 public key: {0}")] + AgeX25519Parse(&'static str), + /// Error generated when a conflict is detected. #[error(transparent)] Conflict(#[from] ConflictError), @@ -98,6 +102,10 @@ pub enum Error { #[error(transparent)] Network(#[from] NetworkError), + /// Error parsing a UUID. + #[error(transparent)] + Uuid(#[from] uuid::Error), + /// Error generated joining a task. #[error(transparent)] Join(#[from] tokio::task::JoinError), diff --git a/crates/protocol/src/hashcheck.rs b/crates/protocol/src/hashcheck.rs index 1ecaff41aa..48c484e9b6 100644 --- a/crates/protocol/src/hashcheck.rs +++ b/crates/protocol/src/hashcheck.rs @@ -1,5 +1,5 @@ //! Check password hashes using the hashcheck service. -use super::{is_offline, Result}; +use super::{Result, is_offline}; use tracing::instrument; /// Default endpoint for HIBP database checks. diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 62350f5435..c07e4061db 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -1,11 +1,3 @@ -#![deny(missing_docs)] -#![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -#![allow(clippy::result_large_err)] -#![allow(clippy::large_enum_variant)] -// For the prost generated types -#![allow(clippy::enum_variant_names)] - //! Networking and sync protocol types for the //! [Save Our Secrets](https://saveoursecrets.com) SDK. @@ -26,12 +18,21 @@ // A 64-bit machine is assumed as we cast between `u64` and `usize` // for convenience, the code may panic on 32-bit machines. +#![deny(missing_docs)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![allow(clippy::result_large_err)] +#![allow(clippy::large_enum_variant)] +// For the prost generated types +#![allow(clippy::enum_variant_names)] + mod bindings; pub mod constants; mod diff; mod error; #[cfg(feature = "network-client")] pub mod network_client; +pub mod query; mod traits; #[cfg(any( @@ -49,7 +50,7 @@ pub use diff::*; pub use error::{AsConflict, ConflictError, Error, ErrorReply, NetworkError}; pub use traits::*; -use prost::{bytes::Buf, Message}; +use prost::{Message, bytes::Buf}; #[cfg(feature = "network-client")] pub use reqwest; diff --git a/crates/protocol/src/network_client/http.rs b/crates/protocol/src/network_client/http.rs index 7ae686ad9c..7df33f7764 100644 --- a/crates/protocol/src/network_client/http.rs +++ b/crates/protocol/src/network_client/http.rs @@ -1,20 +1,29 @@ //! HTTP client implementation. use crate::{ + CreateSharedFolderRequest, CreateSharedFolderResponse, + DeleteSharedFolderRequest, DeleteSharedFolderResponse, DiffRequest, + DiffResponse, Error, GetFolderInvitesRequest, GetFolderInvitesResponse, + GetRecipientRequest, GetRecipientResponse, NetworkError, PatchRequest, + PatchResponse, Result, ScanRequest, ScanResponse, + SearchRecipientsRequest, SearchRecipientsResponse, SetRecipientRequest, + SetRecipientResponse, SyncClient, UpdateFolderInviteRequest, + UpdateFolderInviteResponse, WireEncodeDecode, constants::{ + MIME_TYPE_JSON, MIME_TYPE_PROTOBUF, X_SOS_ACCOUNT_ID, routes::v1::{ - SYNC_ACCOUNT, SYNC_ACCOUNT_EVENTS, SYNC_ACCOUNT_STATUS, + SHARING_FOLDER, SHARING_RECEIVED_INVITES, SHARING_RECIPIENT, + SHARING_SEARCH_RECIPIENTS, SHARING_SENT_INVITES, + SHARING_UPDATE_INVITE, SYNC_ACCOUNT, SYNC_ACCOUNT_EVENTS, + SYNC_ACCOUNT_STATUS, }, - MIME_TYPE_JSON, MIME_TYPE_PROTOBUF, X_SOS_ACCOUNT_ID, }, - DiffRequest, DiffResponse, Error, NetworkError, PatchRequest, - PatchResponse, Result, ScanRequest, ScanResponse, SyncClient, - WireEncodeDecode, + query::MoveFileQuery, }; use async_trait::async_trait; use http::StatusCode; use reqwest::{ - header::{AUTHORIZATION, CONTENT_TYPE, USER_AGENT}, Certificate, RequestBuilder, + header::{AUTHORIZATION, CONTENT_TYPE, USER_AGENT}, }; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -34,10 +43,10 @@ use super::{bearer_prefix, encode_device_signature}; #[cfg(feature = "listen")] use crate::{ + NetworkChangeEvent, network_client::websocket::{ ListenOptions, WebSocketChangeListener, WebSocketHandle, }, - NetworkChangeEvent, }; #[cfg(feature = "files")] @@ -462,6 +471,174 @@ impl SyncClient for HttpClient { let buffer = response.bytes().await?; Ok(PatchResponse::decode(buffer).await?) } + + #[cfg_attr(not(target_arch = "wasm32"), instrument(skip_all))] + async fn set_recipient( + &self, + request: SetRecipientRequest, + ) -> Result { + let body = request.encode().await?; + let url = self.build_url(SHARING_RECIPIENT)?; + tracing::debug!(url = %url, "http::set_recipient"); + let request = self + .client + .put(url) + .header(CONTENT_TYPE, MIME_TYPE_PROTOBUF); + let request = self.request_headers(request, &body).await?; + let response = request.body(body).send().await?; + let status = response.status(); + tracing::debug!(status = %status, "http::set_recipient"); + let response = self.check_response(response).await?; + let buffer = response.bytes().await?; + Ok(SetRecipientResponse::decode(buffer).await?) + } + + #[cfg_attr(not(target_arch = "wasm32"), instrument(skip_all))] + async fn get_recipient( + &self, + _request: GetRecipientRequest, + ) -> Result { + let url = self.build_url(SHARING_RECIPIENT)?; + tracing::debug!(url = %url, "http::get_recipient"); + + let sign_url = url.path().to_owned(); + let request = self.client.get(url); + let request = + self.request_headers(request, sign_url.as_bytes()).await?; + + let response = request.send().await?; + let status = response.status(); + tracing::debug!(status = %status, "http::get_recipient"); + let response = self.check_response(response).await?; + let buffer = response.bytes().await?; + Ok(GetRecipientResponse::decode(buffer).await?) + } + + #[cfg_attr(not(target_arch = "wasm32"), instrument(skip_all))] + async fn create_shared_folder( + &self, + request: CreateSharedFolderRequest, + ) -> Result { + let body = request.encode().await?; + let url = self.build_url(SHARING_FOLDER)?; + tracing::debug!(url = %url, "http::create_shared_folder"); + let request = self + .client + .post(url) + .header(CONTENT_TYPE, MIME_TYPE_PROTOBUF); + let request = self.request_headers(request, &body).await?; + let response = request.body(body).send().await?; + let status = response.status(); + tracing::debug!(status = %status, "http::create_shared_folder"); + let response = self.check_response(response).await?; + let buffer = response.bytes().await?; + Ok(CreateSharedFolderResponse::decode(buffer).await?) + } + + #[cfg_attr(not(target_arch = "wasm32"), instrument(skip_all))] + async fn sent_folder_invites( + &self, + request: GetFolderInvitesRequest, + ) -> Result { + let url = self.build_url(SHARING_SENT_INVITES)?; + tracing::debug!(url = %url, "http::sent_folder_invites"); + + let sign_url = url.path().to_owned(); + let request = self.client.get(url).query(&request); + let request = + self.request_headers(request, sign_url.as_bytes()).await?; + + let response = request.send().await?; + let status = response.status(); + tracing::debug!(status = %status, "http::sent_folder_invites"); + let response = self.check_response(response).await?; + let buffer = response.bytes().await?; + Ok(GetFolderInvitesResponse::decode(buffer).await?) + } + + #[cfg_attr(not(target_arch = "wasm32"), instrument(skip_all))] + async fn received_folder_invites( + &self, + request: GetFolderInvitesRequest, + ) -> Result { + let url = self.build_url(SHARING_RECEIVED_INVITES)?; + tracing::debug!(url = %url, "http::received_folder_invites"); + + let sign_url = url.path().to_owned(); + let request = self.client.get(url).query(&request); + let request = + self.request_headers(request, sign_url.as_bytes()).await?; + + let response = request.send().await?; + let status = response.status(); + tracing::debug!(status = %status, "http::received_folder_invites"); + let response = self.check_response(response).await?; + let buffer = response.bytes().await?; + Ok(GetFolderInvitesResponse::decode(buffer).await?) + } + + #[cfg_attr(not(target_arch = "wasm32"), instrument(skip_all))] + async fn update_folder_invite( + &self, + request: UpdateFolderInviteRequest, + ) -> Result { + let body = request.encode().await?; + let url = self.build_url(SHARING_UPDATE_INVITE)?; + tracing::debug!(url = %url, "http::update_folder_invite"); + let request = self + .client + .put(url) + .header(CONTENT_TYPE, MIME_TYPE_PROTOBUF); + let request = self.request_headers(request, &body).await?; + let response = request.body(body).send().await?; + let status = response.status(); + tracing::debug!(status = %status, "http::update_folder_invite"); + let response = self.check_response(response).await?; + let buffer = response.bytes().await?; + Ok(UpdateFolderInviteResponse::decode(buffer).await?) + } + + #[cfg_attr(not(target_arch = "wasm32"), instrument(skip_all))] + async fn search_recipients( + &self, + request: SearchRecipientsRequest, + ) -> Result { + let url = self.build_url(SHARING_SEARCH_RECIPIENTS)?; + tracing::debug!(url = %url, "http::search_recipients"); + + let sign_url = url.path().to_owned(); + let request = self.client.get(url).query(&request); + let request = + self.request_headers(request, sign_url.as_bytes()).await?; + + let response = request.send().await?; + let status = response.status(); + tracing::debug!(status = %status, "http::search_recipients"); + let response = self.check_response(response).await?; + let buffer = response.bytes().await?; + Ok(SearchRecipientsResponse::decode(buffer).await?) + } + + #[cfg_attr(not(target_arch = "wasm32"), instrument(skip_all))] + async fn delete_shared_folder( + &self, + request: DeleteSharedFolderRequest, + ) -> Result { + let body = request.encode().await?; + let url = self.build_url(SHARING_FOLDER)?; + tracing::debug!(url = %url, "http::delete_shared_folder"); + let request = self + .client + .delete(url) + .header(CONTENT_TYPE, MIME_TYPE_PROTOBUF); + let request = self.request_headers(request, &body).await?; + let response = request.body(body).send().await?; + let status = response.status(); + tracing::debug!(status = %status, "http::delete_shared_folder"); + let response = self.check_response(response).await?; + let buffer = response.bytes().await?; + Ok(DeleteSharedFolderResponse::decode(buffer).await?) + } } #[cfg(feature = "files")] @@ -485,8 +662,8 @@ impl FileSyncClient for HttpClient { ) -> Result { use futures::StreamExt; use reqwest::{ - header::{CONTENT_LENGTH, CONTENT_TYPE}, Body, + header::{CONTENT_LENGTH, CONTENT_TYPE}, }; use sos_vfs as vfs; use tokio::sync::mpsc; @@ -708,17 +885,18 @@ impl FileSyncClient for HttpClient { to: &ExternalFile, ) -> Result { let url_path = format!("api/v1/sync/file/{}", from); - let mut url = self.build_url(&url_path)?; + let url = self.build_url(&url_path)?; - url.query_pairs_mut() - .append_pair("vault_id", &to.vault_id().to_string()) - .append_pair("secret_id", &to.secret_id().to_string()) - .append_pair("name", &to.file_name().to_string()); + let query = MoveFileQuery { + vault_id: *to.vault_id(), + secret_id: *to.secret_id(), + name: *to.file_name(), + }; tracing::debug!(from = %from, to = %to, url = %url, "http::move_file"); let sign_url = url.path().to_owned(); - let request = self.client.post(url); + let request = self.client.post(url).query(&query); let request = self.request_headers(request, sign_url.as_bytes()).await?; let response = request.send().await?; diff --git a/crates/protocol/src/network_client/mod.rs b/crates/protocol/src/network_client/mod.rs index 2230d96050..7ab0076a68 100644 --- a/crates/protocol/src/network_client/mod.rs +++ b/crates/protocol/src/network_client/mod.rs @@ -8,8 +8,8 @@ use sos_signer::ed25519::{ use std::{ future::Future, sync::{ - atomic::{AtomicU32, Ordering}, Arc, + atomic::{AtomicU32, Ordering}, }, time::Duration, }; @@ -20,11 +20,11 @@ mod http; mod websocket; pub use self::http::{ - set_user_agent, HttpClient, HttpClientOptions, NetworkConfig, + HttpClient, HttpClientOptions, NetworkConfig, set_user_agent, }; #[cfg(feature = "listen")] -pub use websocket::{changes, connect, ListenOptions, WebSocketHandle}; +pub use websocket::{ListenOptions, WebSocketHandle, changes, connect}; /// Network retry state and logic for exponential backoff. #[cfg(not(target_arch = "wasm32"))] diff --git a/crates/protocol/src/network_client/websocket.rs b/crates/protocol/src/network_client/websocket.rs index 19eddfe668..c1a1d53fbb 100644 --- a/crates/protocol/src/network_client/websocket.rs +++ b/crates/protocol/src/network_client/websocket.rs @@ -1,12 +1,12 @@ //! Listen for change notifications on a websocket connection. use crate::{ + Error, NetworkChangeEvent, Result, WireEncodeDecode, network_client::{NetworkConfig, NetworkRetry, WebSocketRequest}, transfer::CancelReason, - Error, NetworkChangeEvent, Result, WireEncodeDecode, }; use futures::{ - stream::{Map, SplitStream}, Future, FutureExt, StreamExt, + stream::{Map, SplitStream}, }; use prost::bytes::Bytes; use rustls::{ClientConfig, RootCertStore}; @@ -16,16 +16,16 @@ use std::pin::Pin; use std::sync::Arc; use tokio::{net::TcpStream, sync::watch, time::Duration}; use tokio_tungstenite::{ - client_async_tls_with_config, connect_async, + Connector, MaybeTlsStream, WebSocketStream, client_async_tls_with_config, + connect_async, tungstenite::{ self, client::IntoClientRequest, protocol::{ - frame::{coding::CloseCode, Utf8Bytes}, CloseFrame, Message, + frame::{Utf8Bytes, coding::CloseCode}, }, }, - Connector, MaybeTlsStream, WebSocketStream, }; use super::{bearer_prefix, encode_device_signature}; diff --git a/crates/protocol/src/protobuf/common.proto b/crates/protocol/src/protobuf/common.proto index 359fde194b..3e301e59c1 100644 --- a/crates/protocol/src/protobuf/common.proto +++ b/crates/protocol/src/protobuf/common.proto @@ -80,3 +80,38 @@ message WireCheckedPatch { } } +message WireRecipient { + // Name of the recipient. + string name = 1; + // Email of the recipient. + optional string email = 2; + // Recipient public key. + string public_key = 3; +} + +/// Status of a folder invite. +enum WireInviteStatus { + Pending = 0; + Accepted = 1; + Declined = 2; +} + +/// Folder invite. +message WireFolderInvite { + /// Created at date and time. + common.WireUtcDateTime created_at = 1; + /// Modified at date and time. + common.WireUtcDateTime modified_at = 2; + /// Invite status. + common.WireInviteStatus invite_status = 3; + /// Folder identifier. + string folder_id = 4; + /// Folder name. + string folder_name = 5; + /// Recipient name. + string recipient_name = 6; + /// Recipient email. + optional string recipient_email = 7; + /// Recipient public key. + string recipient_public_key = 8; +} diff --git a/crates/protocol/src/protobuf/shared_folder.proto b/crates/protocol/src/protobuf/shared_folder.proto new file mode 100644 index 0000000000..cccefb8769 --- /dev/null +++ b/crates/protocol/src/protobuf/shared_folder.proto @@ -0,0 +1,67 @@ +syntax = "proto3"; + +package shared_folder; + +import "protobuf/common.proto"; + +message WireSetRecipientRequest { + /// Recipient information. + common.WireRecipient recipient = 1; +} +message WireSetRecipientResponse {} + +message WireGetRecipientRequest {} +message WireGetRecipientResponse { + /// Recipient information. + common.WireRecipient recipient = 1; +} + +message WireCreateSharedFolderRequest { + /// Encoded vault. + bytes vault = 1; + // Collection of recipients. + repeated common.WireRecipient recipients = 2; +} +message WireCreateSharedFolderResponse {} + +message WireGetFolderInvitesRequest { + /// Status of the invite. + optional common.WireInviteStatus invite_status = 1; + /// Limit the number of rows. + optional uint32 limit = 2; +} +message WireGetFolderInvitesResponse { + /// Folder invites. + repeated common.WireFolderInvite folder_invites = 1; +} + +message WireUpdateFolderInviteRequest { + /// Status of the invite. + common.WireInviteStatus invite_status = 1; + /// Public key of the recipient that sent the invite. + string from_public_key = 2; + /// Folder identifier. + string folder_id = 3; +} +message WireUpdateFolderInviteResponse {} + +message WireSearchRecipientsRequest { + /// Query for the search request. + string query = 1; + /// Limit the number of rows. + optional uint32 limit = 2; +} +message WireSearchRecipientsResponse { + // Collection of recipients. + repeated common.WireRecipient recipients = 1; +} + +message WireDeleteSharedFolderRequest { + /// Folder identifier. + string folder_id = 1; +} +message WireDeleteSharedFolderResponse { + /// Whether the account that deleted the folder + /// also created the folder. + bool is_creator = 1; +} diff --git a/crates/protocol/src/query.rs b/crates/protocol/src/query.rs new file mode 100644 index 0000000000..9be4c605df --- /dev/null +++ b/crates/protocol/src/query.rs @@ -0,0 +1,14 @@ +//! Types encoded in query strings. +use serde::{Deserialize, Serialize}; +use sos_core::{ExternalFileName, SecretId, VaultId}; + +/// Query string for moving a file. +#[derive(Debug, Serialize, Deserialize)] +pub struct MoveFileQuery { + /// Folder identifier. + pub vault_id: VaultId, + /// Secret identifier. + pub secret_id: SecretId, + /// External file name. + pub name: ExternalFileName, +} diff --git a/crates/protocol/src/traits.rs b/crates/protocol/src/traits.rs index 2dd8817d17..99017268cc 100644 --- a/crates/protocol/src/traits.rs +++ b/crates/protocol/src/traits.rs @@ -1,6 +1,11 @@ use crate::{ - DiffRequest, DiffResponse, PatchRequest, PatchResponse, ScanRequest, - ScanResponse, SyncOptions, + CreateSharedFolderRequest, CreateSharedFolderResponse, + DeleteSharedFolderRequest, DeleteSharedFolderResponse, DiffRequest, + DiffResponse, GetFolderInvitesRequest, GetFolderInvitesResponse, + GetRecipientRequest, GetRecipientResponse, PatchRequest, PatchResponse, + ScanRequest, ScanResponse, SearchRecipientsRequest, + SearchRecipientsResponse, SetRecipientRequest, SetRecipientResponse, + SyncOptions, UpdateFolderInviteRequest, UpdateFolderInviteResponse, }; use async_trait::async_trait; use sos_core::Origin; @@ -208,4 +213,52 @@ pub trait SyncClient { &self, request: PatchRequest, ) -> Result; + + /// Set recipient information for the account. + async fn set_recipient( + &self, + request: SetRecipientRequest, + ) -> Result; + + /// Get recipient information for the account. + async fn get_recipient( + &self, + request: GetRecipientRequest, + ) -> Result; + + /// Create a shared folder. + async fn create_shared_folder( + &self, + request: CreateSharedFolderRequest, + ) -> Result; + + /// List sent folder invites. + async fn sent_folder_invites( + &self, + request: GetFolderInvitesRequest, + ) -> Result; + + /// List received folder invites. + async fn received_folder_invites( + &self, + request: GetFolderInvitesRequest, + ) -> Result; + + /// Update a received folder invite. + async fn update_folder_invite( + &self, + request: UpdateFolderInviteRequest, + ) -> Result; + + /// Search for recipients. + async fn search_recipients( + &self, + request: SearchRecipientsRequest, + ) -> Result; + + /// Delete a shared folder. + async fn delete_shared_folder( + &self, + request: DeleteSharedFolderRequest, + ) -> Result; } diff --git a/crates/recovery/Cargo.toml b/crates/recovery/Cargo.toml index 08d3f139eb..23e57595d7 100644 --- a/crates/recovery/Cargo.toml +++ b/crates/recovery/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-recovery" version = "0.16.1" -edition = "2021" +edition = "2024" description = "Types and traits for social recovery." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" diff --git a/crates/reducers/Cargo.toml b/crates/reducers/Cargo.toml index 8d8eb62e74..78fc76351e 100644 --- a/crates/reducers/Cargo.toml +++ b/crates/reducers/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-reducers" version = "0.17.1" -edition = "2021" +edition = "2024" description = "Event log reducers for the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -13,15 +13,11 @@ rustdoc-args = ["--cfg", "docsrs"] [features] full = ["files"] -files = [] +files = ["sos-core/files"] [dependencies] sos-core.workspace = true sos-vault.workspace = true - futures.workspace = true indexmap.workspace = true tracing.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/reducers/build.rs b/crates/reducers/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/reducers/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/reducers/src/files.rs b/crates/reducers/src/files.rs index 350857a8f0..c7c7327c3d 100644 --- a/crates/reducers/src/files.rs +++ b/crates/reducers/src/files.rs @@ -1,9 +1,9 @@ use futures::{pin_mut, stream::StreamExt}; use indexmap::IndexSet; use sos_core::{ + ExternalFile, commit::CommitHash, events::{EventLog, FileEvent}, - ExternalFile, }; /// Reduce file events to a collection of external files. diff --git a/crates/reducers/src/folder.rs b/crates/reducers/src/folder.rs index ea5d74c27c..19d5381a46 100644 --- a/crates/reducers/src/folder.rs +++ b/crates/reducers/src/folder.rs @@ -1,8 +1,8 @@ -use futures::{pin_mut, StreamExt}; +use futures::{StreamExt, pin_mut}; use indexmap::IndexMap; use sos_core::{ - commit::CommitHash, crypto::AeadPack, decode, events::EventLog, - events::WriteEvent, SecretId, VaultCommit, VaultFlags, + SecretId, VaultCommit, VaultFlags, commit::CommitHash, crypto::AeadPack, + decode, events::EventLog, events::WriteEvent, }; use sos_vault::{Error, Vault}; @@ -81,19 +81,18 @@ impl FolderReducer { // If we are only reading until the first commit // hash return early. - if let Some(until) = &self.until_commit { - if &until.0 == record.commit().as_ref() { + if let Some(until) = &self.until_commit + && &until.0 == record.commit().as_ref() { return Ok(self); } - } while let Some(result) = stream.next().await { let (record, event) = result?; match event { WriteEvent::CreateVault(_) => { return Err( - sos_core::Error::CreateEventOnlyFirst.into() - ) + sos_core::Error::CreateEventOnlyFirst.into(), + ); } WriteEvent::SetVaultName(name) => { self.vault_name = Some(name); @@ -118,11 +117,10 @@ impl FolderReducer { // If we are reading to a particular commit hash // we are done. - if let Some(until) = &self.until_commit { - if &until.0 == record.commit().as_ref() { + if let Some(until) = &self.until_commit + && &until.0 == record.commit().as_ref() { break; } - } } } else { return Err(sos_core::Error::CreateEventMustBeFirst.into()); diff --git a/crates/reducers/src/lib.rs b/crates/reducers/src/lib.rs index 3142fa8ce9..e30d9f7de7 100644 --- a/crates/reducers/src/lib.rs +++ b/crates/reducers/src/lib.rs @@ -1,7 +1,7 @@ +//! Reduce event logs into compact representations. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -//! Reduce event logs into compact representations. +#![cfg_attr(docsrs, feature(doc_cfg))] mod device; #[cfg(feature = "files")] diff --git a/crates/remote_sync/Cargo.toml b/crates/remote_sync/Cargo.toml index 292f82fc1b..3765cabdb0 100644 --- a/crates/remote_sync/Cargo.toml +++ b/crates/remote_sync/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-remote-sync" version = "0.17.1" -edition = "2021" +edition = "2024" description = "Sync protocol implementation for the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -27,12 +27,8 @@ sos-core.workspace = true sos-protocol.workspace = true sos-sync.workspace = true sos-vfs.workspace = true - async-trait.workspace = true thiserror.workspace = true indexmap.workspace = true tokio.workspace = true tracing.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/remote_sync/build.rs b/crates/remote_sync/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/remote_sync/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/remote_sync/src/auto_merge.rs b/crates/remote_sync/src/auto_merge.rs index 649a05ffa8..544292f021 100644 --- a/crates/remote_sync/src/auto_merge.rs +++ b/crates/remote_sync/src/auto_merge.rs @@ -2,15 +2,15 @@ use async_trait::async_trait; use sos_account::Account; use sos_core::{ + VaultId, commit::{CommitHash, CommitProof, CommitTree}, events::{ + AccountEvent, DeviceEvent, EventLog, EventLogType, EventRecord, + WriteEvent, patch::{ AccountDiff, CheckedPatch, DeviceDiff, Diff, FolderDiff, Patch, }, - AccountEvent, DeviceEvent, EventLog, EventLogType, EventRecord, - WriteEvent, }, - VaultId, }; use sos_protocol::{ AsConflict, ConflictError, DiffRequest, HardConflictResolver, @@ -26,7 +26,7 @@ use tracing::instrument; const PROOF_SCAN_LIMIT: u16 = 32; #[cfg(feature = "files")] -use sos_core::events::{patch::FileDiff, FileEvent}; +use sos_core::events::{FileEvent, patch::FileDiff}; use super::RemoteSyncHandler; diff --git a/crates/remote_sync/src/lib.rs b/crates/remote_sync/src/lib.rs index 4fe126f059..2a1f2d1d3e 100644 --- a/crates/remote_sync/src/lib.rs +++ b/crates/remote_sync/src/lib.rs @@ -1,7 +1,8 @@ +//! Sync protocol implementation types and traits. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -//! Sync protocol implementation types and traits. +#![cfg_attr(docsrs, feature(doc_cfg))] + mod auto_merge; mod error; mod remote; diff --git a/crates/remote_sync/src/remote.rs b/crates/remote_sync/src/remote.rs index bf5940318d..14028dcc88 100644 --- a/crates/remote_sync/src/remote.rs +++ b/crates/remote_sync/src/remote.rs @@ -188,35 +188,31 @@ pub trait RemoteSyncHandler { } else { // Some parts of the remote patch may not // be in conflict and must still be merged - if !maybe_conflict.identity { - if let Some(MaybeDiff::Diff(diff)) = + if !maybe_conflict.identity + && let Some(MaybeDiff::Diff(diff)) = remote_changes.diff.identity { account.merge_identity(diff, &mut outcome).await?; } - } - if !maybe_conflict.account { - if let Some(MaybeDiff::Diff(diff)) = + if !maybe_conflict.account + && let Some(MaybeDiff::Diff(diff)) = remote_changes.diff.account { account.merge_account(diff, &mut outcome).await?; } - } - if !maybe_conflict.device { - if let Some(MaybeDiff::Diff(diff)) = + if !maybe_conflict.device + && let Some(MaybeDiff::Diff(diff)) = remote_changes.diff.device { account.merge_device(diff, &mut outcome).await?; } - } #[cfg(feature = "files")] - if !maybe_conflict.files { - if let Some(MaybeDiff::Diff(diff)) = + if !maybe_conflict.files + && let Some(MaybeDiff::Diff(diff)) = remote_changes.diff.files { account.merge_files(diff, &mut outcome).await?; } - } let merge_folders = remote_changes .diff diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 378d0e83d2..1a5b7861f4 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-sdk" version = "0.17.2" -edition = "2021" +edition = "2024" description = "Distributed, encrypted database for private secrets." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -23,6 +23,3 @@ sos-password.workspace = true sos-signer.workspace = true sos-vault.workspace = true sos-vfs.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/sdk/build.rs b/crates/sdk/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/sdk/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index dfbb9c2e9a..6ecc2185f0 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -1,5 +1,5 @@ #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #![doc = include_str!("../README.md")] pub mod prelude; diff --git a/crates/sdk/src/prelude.rs b/crates/sdk/src/prelude.rs index 8c516ecfb8..be2d13f986 100644 --- a/crates/sdk/src/prelude.rs +++ b/crates/sdk/src/prelude.rs @@ -1,16 +1,16 @@ //! Prelude re-exports common types. pub use sos_core::{ - constants::*, crypto::*, decode, device::*, encode, events::*, AccountRef, ErrorExt, FolderRef, Paths, PublicIdentity, SecretId, SecretPath, UtcDateTime, VaultCommit, VaultEntry, VaultFlags, VaultId, + constants::*, crypto::*, decode, device::*, encode, events::*, }; -pub use sos_login::{device::*, Identity, IdentityFolder}; +pub use sos_login::{Identity, IdentityFolder, device::*}; pub use sos_password::diceware::generate_passphrase; pub use sos_vault::{ + AccessPoint, BuilderCredentials, ChangePassword, Contents, + EncryptedEntry, Header, Summary, Vault, VaultBuilder, secret::{ FileContent, IdentityKind, Secret, SecretFlags, SecretMeta, SecretRef, SecretRow, SecretSigner, SecretType, UserData, }, - AccessPoint, BuilderCredentials, ChangePassword, Contents, - EncryptedEntry, Header, Summary, Vault, VaultBuilder, }; diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 25e315c212..af1ce8cf16 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-search" version = "0.17.2" -edition = "2021" +edition = "2024" description = "In-memory meta data search index the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -10,11 +10,13 @@ license = "MIT OR Apache-2.0" all-features = true rustdoc-args = ["--cfg", "docsrs"] +[features] +files = ["sos-backend/files"] + [dependencies] sos-core.workspace = true sos-backend.workspace = true sos-vault.workspace = true - thiserror.workspace = true serde.workspace = true unicode-segmentation.workspace = true @@ -24,6 +26,3 @@ vcard4.workspace = true tokio.workspace = true typeshare.workspace = true tracing.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/search/build.rs b/crates/search/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/search/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/search/src/lib.rs b/crates/search/src/lib.rs index 9ce52b7d69..3087395c45 100644 --- a/crates/search/src/lib.rs +++ b/crates/search/src/lib.rs @@ -1,7 +1,8 @@ +//! Search provides an in-memory index for secret meta data. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -//! Search provides an in-memory index for secret meta data. +#![cfg_attr(docsrs, feature(doc_cfg))] + mod error; mod search; diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index cf87d8a9c7..dd9ce3a440 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,16 +1,16 @@ //! Search provides an in-memory index for secret meta data. use crate::{Error, Result}; -use probly_search::{score::bm25, Index, QueryResult}; +use probly_search::{Index, QueryResult, score::bm25}; use serde::{Deserialize, Serialize}; use sos_backend::AccessPoint; -use sos_core::{crypto::AccessKey, VaultId}; +use sos_core::{VaultId, crypto::AccessKey}; use sos_vault::{ - secret::{Secret, SecretId, SecretMeta, SecretRef, SecretType}, SecretAccess, Summary, Vault, + secret::{Secret, SecretId, SecretMeta, SecretRef, SecretType}, }; use std::{ borrow::Cow, - collections::{btree_map::Values, BTreeMap, HashMap, HashSet}, + collections::{BTreeMap, HashMap, HashSet, btree_map::Values}, sync::Arc, }; use tokio::sync::RwLock; @@ -928,11 +928,10 @@ impl DocumentView { doc: &Document, archive: Option<&ArchiveFilter>, ) -> bool { - if let Some(filter) = archive { - if !filter.include_documents && doc.folder_id() == &filter.id { + if let Some(filter) = archive + && !filter.include_documents && doc.folder_id() == &filter.id { return false; } - } match self { DocumentView::All { ignored_types } => { if let Some(ignored_types) = ignored_types { diff --git a/crates/security_report/Cargo.toml b/crates/security_report/Cargo.toml index 8397d3c75d..5c519cb063 100644 --- a/crates/security_report/Cargo.toml +++ b/crates/security_report/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-security-report" version = "0.17.2" -edition = "2021" +edition = "2024" description = "Generate a security report for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -14,7 +14,6 @@ sos-client-storage.workspace = true sos-core.workspace = true sos-password.workspace = true sos-vault.workspace = true - sha1.workspace = true serde.workspace = true hex.workspace = true diff --git a/crates/security_report/src/lib.rs b/crates/security_report/src/lib.rs index 2ff2ffc221..562320ecd0 100644 --- a/crates/security_report/src/lib.rs +++ b/crates/security_report/src/lib.rs @@ -1,7 +1,8 @@ +//! Helpers for security report generation. #![forbid(unsafe_code)] #![allow(clippy::type_complexity)] +#![cfg_attr(docsrs, feature(doc_cfg))] -//! Helpers for security report generation. use secrecy::ExposeSecret; use serde::{Deserialize, Serialize}; use sos_account::Account; @@ -9,8 +10,8 @@ use sos_backend::AccessPoint; use sos_core::VaultId; use sos_password::generator::measure_entropy; use sos_vault::{ - secret::{Secret, SecretId, SecretType}, SecretAccess, Summary, + secret::{Secret, SecretId, SecretType}, }; use zxcvbn::{Entropy, Score}; diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 75b83654e7..ba8eb647af 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-server" version = "0.17.2" -edition = "2021" +edition = "2024" description = "Server for the Save Our Secrets sync protocol." homepage = "https://saveoursecrets.com" license = "AGPL-3.0-or-later" @@ -68,7 +68,4 @@ tokio-stream = { version = "0.1" } utoipa = { version = "5", features = ["uuid"] } utoipa-rapidoc = { version = "6", features = ["axum"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync", "macros"] } -tokio-rustls-acme = { version = "0.6", features = ["axum"], optional = true } - -[build-dependencies] -rustc_version = "0.4.1" +tokio-rustls-acme = { version = "0.8", features = ["axum"], optional = true } diff --git a/crates/server/build.rs b/crates/server/build.rs deleted file mode 100644 index 5976a1c6d5..0000000000 --- a/crates/server/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel); -} diff --git a/crates/server/src/api_docs.rs b/crates/server/src/api_docs.rs index 711bdc7c9d..ac6dc1d40b 100644 --- a/crates/server/src/api_docs.rs +++ b/crates/server/src/api_docs.rs @@ -1,5 +1,5 @@ -use crate::handlers::{account, files}; -use utoipa::{openapi::security::*, Modify, OpenApi}; +use crate::handlers::{account, files, sharing}; +use utoipa::{Modify, OpenApi, openapi::security::*}; #[derive(OpenApi)] #[openapi( @@ -33,6 +33,8 @@ use utoipa::{openapi::security::*, Modify, OpenApi}; files::send_file, files::move_file, files::delete_file, + sharing::set_recipient, + sharing::create_folder, ), components( schemas(), diff --git a/crates/server/src/authenticate.rs b/crates/server/src/authenticate.rs index e9c9867456..388a86dc7d 100644 --- a/crates/server/src/authenticate.rs +++ b/crates/server/src/authenticate.rs @@ -1,8 +1,8 @@ //! Authentication helper functions for extracting an address //! from a signature given in bearer authorization data. use super::{Error, Result}; -use axum_extra::headers::{authorization::Bearer, Authorization}; -use sos_core::{decode, AccountId}; +use axum_extra::headers::{Authorization, authorization::Bearer}; +use sos_core::{AccountId, decode}; use sos_signer::ed25519::{self, BinaryEd25519Signature}; #[derive(Debug)] diff --git a/crates/server/src/backend.rs b/crates/server/src/backend.rs index 68493f9299..a0575f644a 100644 --- a/crates/server/src/backend.rs +++ b/crates/server/src/backend.rs @@ -1,13 +1,15 @@ use super::{Error, Result}; use sos_backend::BackendTarget; -use sos_core::{device::DevicePublicKey, AccountId, Paths}; +use sos_core::{AccountId, Paths, device::DevicePublicKey}; use sos_database::{async_sqlite::Client, entity::AccountEntity}; -use sos_server_storage::{ServerAccountStorage, ServerStorage}; +use sos_server_storage::{ + ServerAccountStorage, ServerStorage, SharedFolderEvents, +}; use sos_signer::ed25519::{self, Verifier, VerifyingKey}; use sos_sync::{CreateSet, ForceMerge, MergeOutcome, SyncStorage, UpdateSet}; use sos_vfs as vfs; use std::{collections::HashMap, sync::Arc}; -use tokio::sync::RwLock; +use tokio::sync::{Mutex, RwLock}; /// Individual account. pub type ServerAccount = Arc>; @@ -27,6 +29,7 @@ pub struct Backend { paths: Arc, accounts: Accounts, target: BackendTarget, + shared_folder_events: SharedFolderEvents, } impl Backend { @@ -36,6 +39,7 @@ impl Backend { paths, accounts: Arc::new(RwLock::new(Default::default())), target, + shared_folder_events: Arc::new(Mutex::new(HashMap::new())), } } @@ -80,29 +84,25 @@ impl Backend { let mut dir = vfs::read_dir(self.paths.local_dir()).await?; while let Some(entry) = dir.next_entry().await? { let path = entry.path(); - if vfs::metadata(&path).await?.is_dir() { - if let Some(name) = path.file_stem() { - if let Ok(account_id) = - name.to_string_lossy().parse::() - { - tracing::debug!( - account_id = %account_id, - "server_backend::load_fs_accounts", - ); - - let account = ServerStorage::new( - self.target.clone(), - &account_id, - ) - .await?; - - let mut accounts = self.accounts.write().await; - accounts.insert( - account_id, - Arc::new(RwLock::new(account)), - ); - } - } + if vfs::metadata(&path).await?.is_dir() + && let Some(name) = path.file_stem() + && let Ok(account_id) = + name.to_string_lossy().parse::() + { + tracing::debug!( + account_id = %account_id, + "server_backend::load_fs_accounts", + ); + + let account = ServerStorage::new( + self.target.clone(), + &account_id, + self.shared_folder_events.clone(), + ) + .await?; + + let mut accounts = self.accounts.write().await; + accounts.insert(account_id, Arc::new(RwLock::new(account))); } } @@ -121,8 +121,12 @@ impl Backend { for account in accounts { let account_id = *account.identity.account_id(); - let account = - ServerStorage::new(self.target.clone(), &account_id).await?; + let account = ServerStorage::new( + self.target.clone(), + &account_id, + self.shared_folder_events.clone(), + ) + .await?; let mut accounts = self.accounts.write().await; accounts.insert(account_id, Arc::new(RwLock::new(account))); @@ -152,9 +156,13 @@ impl Backend { let target = self.target.clone().with_account_id(account_id); - let account = - ServerStorage::create_account(target, account_id, &account_data) - .await?; + let account = ServerStorage::create_account( + target, + account_id, + &account_data, + self.shared_folder_events.clone(), + ) + .await?; let mut accounts = self.accounts.write().await; accounts diff --git a/crates/server/src/error.rs b/crates/server/src/error.rs index 4a85acd205..1e67ffeb4c 100644 --- a/crates/server/src/error.rs +++ b/crates/server/src/error.rs @@ -4,7 +4,7 @@ use axum::{ http::StatusCode, response::{IntoResponse, Json, Response}, }; -use serde_json::{json, Value}; +use serde_json::{Value, json}; use sos_core::AccountId; use std::path::PathBuf; use thiserror::Error; diff --git a/crates/server/src/handlers/account.rs b/crates/server/src/handlers/account.rs index 502ce31927..075a84ce31 100644 --- a/crates/server/src/handlers/account.rs +++ b/crates/server/src/handlers/account.rs @@ -1,14 +1,14 @@ use super::BODY_LIMIT; -use super::{authenticate_endpoint, parse_account_id, Caller}; -use crate::{handlers::ConnectionQuery, ServerBackend, ServerState}; +use super::{Caller, authenticate_endpoint, parse_account_id}; +use crate::{ServerBackend, ServerState, handlers::ConnectionQuery}; use axum::{ - body::{to_bytes, Body}, + body::{Body, to_bytes}, extract::{Extension, OriginalUri, Query}, http::{HeaderMap, StatusCode}, response::IntoResponse, }; use axum_extra::{ - headers::{authorization::Bearer, Authorization}, + headers::{Authorization, authorization::Bearer}, typed_header::TypedHeader, }; use std::sync::Arc; @@ -612,12 +612,12 @@ mod handlers { use crate::{Error, Result, ServerBackend, ServerState}; use axum::body::Bytes; use http::{ - header::{self, HeaderMap, HeaderValue}, StatusCode, + header::{self, HeaderMap, HeaderValue}, }; use sos_protocol::{ - constants::MIME_TYPE_PROTOBUF, DiffRequest, PatchRequest, - ScanRequest, WireEncodeDecode, + DiffRequest, PatchRequest, ScanRequest, WireEncodeDecode, + constants::MIME_TYPE_PROTOBUF, }; use sos_server_storage::server_helpers; use sos_sync::{CreateSet, SyncPacket, SyncStorage, UpdateSet}; @@ -814,19 +814,19 @@ mod handlers { }; #[cfg(feature = "listen")] - if outcome.changes > 0 { - if let Some(conn_id) = caller.connection_id() { - let reader = account.read().await; - let local_status = reader.sync_status().await?; - let notification = NetworkChangeEvent::new( - caller.account_id(), - conn_id.to_string(), - local_status.root, - outcome, - ); - let reader = state.read().await; - send_notification(&reader, &caller, notification).await; - } + if outcome.changes > 0 + && let Some(conn_id) = caller.connection_id() + { + let reader = account.read().await; + let local_status = reader.sync_status().await?; + let notification = NetworkChangeEvent::new( + caller.account_id(), + conn_id.to_string(), + local_status.root, + outcome, + ); + let reader = state.read().await; + send_notification(&reader, &caller, notification).await; } let mut headers = HeaderMap::new(); @@ -866,17 +866,17 @@ mod handlers { }; #[cfg(feature = "listen")] - if outcome.changes > 0 { - if let Some(conn_id) = caller.connection_id() { - let notification = NetworkChangeEvent::new( - caller.account_id(), - conn_id.to_string(), - packet.status.root, - outcome, - ); - let reader = state.read().await; - send_notification(&reader, &caller, notification).await; - } + if outcome.changes > 0 + && let Some(conn_id) = caller.connection_id() + { + let notification = NetworkChangeEvent::new( + caller.account_id(), + conn_id.to_string(), + packet.status.root, + outcome, + ); + let reader = state.read().await; + send_notification(&reader, &caller, notification).await; } let mut headers = HeaderMap::new(); diff --git a/crates/server/src/handlers/files.rs b/crates/server/src/handlers/files.rs index 99a827d548..e4bba5f6cf 100644 --- a/crates/server/src/handlers/files.rs +++ b/crates/server/src/handlers/files.rs @@ -1,35 +1,27 @@ -use super::{parse_account_id, BODY_LIMIT}; +use super::{BODY_LIMIT, parse_account_id}; use crate::{ - handlers::{authenticate_endpoint, ConnectionQuery}, ServerBackend, ServerState, ServerTransfer, + handlers::{ConnectionQuery, authenticate_endpoint}, }; use axum::{ - body::{to_bytes, Body}, + body::{Body, to_bytes}, extract::{Extension, OriginalUri, Path, Query, Request}, http::{HeaderMap, StatusCode}, middleware::Next, response::{IntoResponse, Response}, }; use axum_extra::{ - headers::{authorization::Bearer, Authorization}, + headers::{Authorization, authorization::Bearer}, typed_header::TypedHeader, }; -use serde::Deserialize; use sos_core::{ ExternalFile, ExternalFileName, SecretId, SecretPath, VaultId, }; +use sos_protocol::query::MoveFileQuery; use std::sync::Arc; //use axum_macros::debug_handler; -/// Query string for moving a file. -#[derive(Debug, Deserialize)] -pub struct MoveFileQuery { - pub vault_id: VaultId, - pub secret_id: SecretId, - pub name: ExternalFileName, -} - /// Upload a file. #[utoipa::path( put, @@ -366,7 +358,7 @@ pub(crate) async fn compare_files( mod handlers { use super::MoveFileQuery; use crate::{ - handlers::Caller, Error, Result, ServerBackend, ServerState, + Error, Result, ServerBackend, ServerState, handlers::Caller, }; use axum::{ body::{Body, Bytes}, @@ -380,9 +372,9 @@ mod handlers { use sos_core::{ExternalFileName, SecretId, VaultId}; use sos_external_files::list_external_files; use sos_protocol::{ + WireEncodeDecode, constants::MIME_TYPE_PROTOBUF, transfer::{FileSet, FileTransfersSet}, - WireEncodeDecode, }; use sos_server_storage::ServerAccountStorage; use std::{path::PathBuf, sync::Arc}; diff --git a/crates/server/src/handlers/mod.rs b/crates/server/src/handlers/mod.rs index 86be9d83d1..0273cd5d83 100644 --- a/crates/server/src/handlers/mod.rs +++ b/crates/server/src/handlers/mod.rs @@ -1,16 +1,16 @@ use axum::{ + Json, extract::Extension, response::{IntoResponse, Redirect}, - Json, }; //use axum_macros::debug_handler; use crate::{ - authenticate::{self, BearerToken}, Error, Result, ServerBackend, + authenticate::{self, BearerToken}, }; -use axum_extra::headers::{authorization::Bearer, Authorization}; +use axum_extra::headers::{Authorization, authorization::Bearer}; use http::HeaderMap; use serde::Deserialize; use serde_json::json; @@ -19,6 +19,7 @@ use sos_protocol::constants::X_SOS_ACCOUNT_ID; pub mod account; pub mod files; +pub mod sharing; #[cfg(feature = "pairing")] pub(crate) mod relay; @@ -104,10 +105,10 @@ async fn authenticate_endpoint( // Deny unauthorized account ids { let reader = state.read().await; - if let Some(access) = &reader.config.access { - if !access.is_allowed_access(&token.account_id) { - return Err(Error::Forbidden); - } + if let Some(access) = &reader.config.access + && !access.is_allowed_access(&token.account_id) + { + return Err(Error::Forbidden); } } @@ -138,10 +139,10 @@ pub(crate) async fn send_notification( // Send notification on the websockets channel match notification.encode().await { Ok(buffer) => { - if let Some(account) = reader.sockets.get(caller.account_id()) { - if let Err(error) = account.broadcast(caller, buffer).await { - tracing::warn!(error = ?error); - } + if let Some(account) = reader.sockets.get(caller.account_id()) + && let Err(error) = account.broadcast(caller, buffer).await + { + tracing::warn!(error = ?error); } } Err(e) => { diff --git a/crates/server/src/handlers/relay.rs b/crates/server/src/handlers/relay.rs index f7896092af..62d123bfad 100644 --- a/crates/server/src/handlers/relay.rs +++ b/crates/server/src/handlers/relay.rs @@ -1,13 +1,13 @@ //! Relay forwards packets between peers over a websocket connection. use axum::{ extract::{ - ws::{Message, WebSocket, WebSocketUpgrade}, Extension, Query, + ws::{Message, WebSocket, WebSocketUpgrade}, }, http::StatusCode, response::Response, }; -use futures::{stream::SplitSink, SinkExt, StreamExt}; +use futures::{SinkExt, StreamExt, stream::SplitSink}; use serde::Deserialize; use sos_protocol::RelayPacket; use std::{collections::HashMap, sync::Arc}; diff --git a/crates/server/src/handlers/sharing.rs b/crates/server/src/handlers/sharing.rs new file mode 100644 index 0000000000..0704773b08 --- /dev/null +++ b/crates/server/src/handlers/sharing.rs @@ -0,0 +1,830 @@ +use super::{BODY_LIMIT, Caller, parse_account_id}; +use crate::{ServerBackend, ServerState, handlers::authenticate_endpoint}; +use axum::{ + body::{Body, to_bytes}, + extract::{Extension, OriginalUri, Query}, + http::{HeaderMap, StatusCode}, + response::IntoResponse, +}; +use axum_extra::{ + headers::{Authorization, authorization::Bearer}, + typed_header::TypedHeader, +}; +use sos_protocol::{GetFolderInvitesRequest, SearchRecipientsRequest}; +use std::sync::Arc; + +/// Upsert account recipient information. +#[utoipa::path( + put, + path = "/sharing/recipient", + security( + ("bearer_token" = []) + ), + request_body( + content_type = "application/octet-stream", + content = Vec, + ), + responses( + ( + status = StatusCode::UNAUTHORIZED, + description = "Authorization failed.", + ), + ( + status = StatusCode::FORBIDDEN, + description = "Account identifier is not allowed on this server.", + ), + ( + status = StatusCode::OK, + description = "Recipient information was inserted or updated.", + ), + ), +)] +#[allow(clippy::too_many_arguments)] +pub(crate) async fn set_recipient( + Extension(state): Extension, + Extension(backend): Extension, + TypedHeader(bearer): TypedHeader>, + headers: HeaderMap, + body: Body, +) -> impl IntoResponse { + let account_id = parse_account_id(&headers); + match to_bytes(body, BODY_LIMIT).await { + Ok(bytes) => match authenticate_endpoint( + account_id, + bearer, + &bytes, + None, + Arc::clone(&state), + Arc::clone(&backend), + ) + .await + { + Ok(caller) => { + match handlers::set_recipient(state, backend, caller, bytes) + .await + { + Ok(result) => result.into_response(), + Err(error) => error.into_response(), + } + } + Err(error) => error.into_response(), + }, + Err(_) => StatusCode::BAD_REQUEST.into_response(), + } +} + +/// Get account recipient information. +#[utoipa::path( + get, + path = "/sharing/recipient", + security( + ("bearer_token" = []) + ), + request_body( + content_type = "application/octet-stream", + content = Vec, + ), + responses( + ( + status = StatusCode::UNAUTHORIZED, + description = "Authorization failed.", + ), + ( + status = StatusCode::FORBIDDEN, + description = "Account identifier is not allowed on this server.", + ), + ( + status = StatusCode::OK, + description = "Recipient information was fetched.", + ), + ), +)] +#[allow(clippy::too_many_arguments)] +pub(crate) async fn get_recipient( + Extension(state): Extension, + Extension(backend): Extension, + TypedHeader(bearer): TypedHeader>, + OriginalUri(uri): OriginalUri, + headers: HeaderMap, +) -> impl IntoResponse { + let uri = uri.path().to_string(); + let account_id = parse_account_id(&headers); + match authenticate_endpoint( + account_id, + bearer, + uri.as_bytes(), + None, + Arc::clone(&state), + Arc::clone(&backend), + ) + .await + { + Ok(caller) => { + match handlers::get_recipient(state, backend, caller).await { + Ok(response) => response.into_response(), + Err(error) => error.into_response(), + } + } + Err(error) => error.into_response(), + } +} + +/// Create a shared folder. +#[utoipa::path( + post, + path = "/sharing/folder", + security( + ("bearer_token" = []) + ), + request_body( + content_type = "application/octet-stream", + content = Vec, + ), + responses( + ( + status = StatusCode::UNAUTHORIZED, + description = "Authorization failed.", + ), + ( + status = StatusCode::FORBIDDEN, + description = "Account identifier is not allowed on this server.", + ), + ( + status = StatusCode::OK, + description = "Folder was created.", + ), + ), +)] +#[allow(clippy::too_many_arguments)] +pub(crate) async fn create_folder( + Extension(state): Extension, + Extension(backend): Extension, + TypedHeader(bearer): TypedHeader>, + headers: HeaderMap, + body: Body, +) -> impl IntoResponse { + let account_id = parse_account_id(&headers); + match to_bytes(body, BODY_LIMIT).await { + Ok(bytes) => match authenticate_endpoint( + account_id, + bearer, + &bytes, + None, + Arc::clone(&state), + Arc::clone(&backend), + ) + .await + { + Ok(caller) => { + match handlers::create_folder(state, backend, caller, bytes) + .await + { + Ok(result) => result.into_response(), + Err(error) => error.into_response(), + } + } + Err(error) => error.into_response(), + }, + Err(_) => StatusCode::BAD_REQUEST.into_response(), + } +} + +/// Get sent folder invites. +#[utoipa::path( + get, + path = "/sharing/folder/invites/sent", + security( + ("bearer_token" = []) + ), + request_body( + content_type = "application/octet-stream", + content = Vec, + ), + responses( + ( + status = StatusCode::UNAUTHORIZED, + description = "Authorization failed.", + ), + ( + status = StatusCode::FORBIDDEN, + description = "Account identifier is not allowed on this server.", + ), + ( + status = StatusCode::OK, + description = "List of folder invites.", + ), + ), +)] +#[allow(clippy::too_many_arguments)] +pub(crate) async fn sent_folder_invites( + Extension(state): Extension, + Extension(backend): Extension, + TypedHeader(bearer): TypedHeader>, + Query(params): Query, + OriginalUri(uri): OriginalUri, + headers: HeaderMap, +) -> impl IntoResponse { + let uri = uri.path().to_string(); + let account_id = parse_account_id(&headers); + match authenticate_endpoint( + account_id, + bearer, + uri.as_bytes(), + None, + Arc::clone(&state), + Arc::clone(&backend), + ) + .await + { + Ok(caller) => { + match handlers::sent_folder_invites( + state, backend, caller, params, + ) + .await + { + Ok(response) => response.into_response(), + Err(error) => error.into_response(), + } + } + Err(error) => error.into_response(), + } +} + +/// Get received folder invites. +#[utoipa::path( + get, + path = "/sharing/folder/invites/inbox", + security( + ("bearer_token" = []) + ), + request_body( + content_type = "application/octet-stream", + content = Vec, + ), + responses( + ( + status = StatusCode::UNAUTHORIZED, + description = "Authorization failed.", + ), + ( + status = StatusCode::FORBIDDEN, + description = "Account identifier is not allowed on this server.", + ), + ( + status = StatusCode::OK, + description = "List of folder invites.", + ), + ), +)] +#[allow(clippy::too_many_arguments)] +pub(crate) async fn received_folder_invites( + Extension(state): Extension, + Extension(backend): Extension, + TypedHeader(bearer): TypedHeader>, + Query(params): Query, + OriginalUri(uri): OriginalUri, + headers: HeaderMap, +) -> impl IntoResponse { + let uri = uri.path().to_string(); + let account_id = parse_account_id(&headers); + match authenticate_endpoint( + account_id, + bearer, + uri.as_bytes(), + None, + Arc::clone(&state), + Arc::clone(&backend), + ) + .await + { + Ok(caller) => { + match handlers::received_folder_invites( + state, backend, caller, params, + ) + .await + { + Ok(response) => response.into_response(), + Err(error) => error.into_response(), + } + } + Err(error) => error.into_response(), + } +} + +/// Update a folder invite. +#[utoipa::path( + put, + path = "/sharing/folder/invites", + security( + ("bearer_token" = []) + ), + request_body( + content_type = "application/octet-stream", + content = Vec, + ), + responses( + ( + status = StatusCode::UNAUTHORIZED, + description = "Authorization failed.", + ), + ( + status = StatusCode::FORBIDDEN, + description = "Account identifier is not allowed on this server.", + ), + ( + status = StatusCode::OK, + description = "Folder invite was updated.", + ), + ), +)] +#[allow(clippy::too_many_arguments)] +pub(crate) async fn update_folder_invite( + Extension(state): Extension, + Extension(backend): Extension, + TypedHeader(bearer): TypedHeader>, + headers: HeaderMap, + body: Body, +) -> impl IntoResponse { + let account_id = parse_account_id(&headers); + match to_bytes(body, BODY_LIMIT).await { + Ok(bytes) => match authenticate_endpoint( + account_id, + bearer, + &bytes, + None, + Arc::clone(&state), + Arc::clone(&backend), + ) + .await + { + Ok(caller) => { + match handlers::update_folder_invite( + state, backend, caller, bytes, + ) + .await + { + Ok(result) => result.into_response(), + Err(error) => error.into_response(), + } + } + Err(error) => error.into_response(), + }, + Err(_) => StatusCode::BAD_REQUEST.into_response(), + } +} + +/// Search for recipients. +#[utoipa::path( + get, + path = "/sharing/recipient/search", + security( + ("bearer_token" = []) + ), + request_body( + content_type = "application/octet-stream", + content = Vec, + ), + responses( + ( + status = StatusCode::UNAUTHORIZED, + description = "Authorization failed.", + ), + ( + status = StatusCode::FORBIDDEN, + description = "Account identifier is not allowed on this server.", + ), + ( + status = StatusCode::OK, + description = "List of recipients.", + ), + ), +)] +#[allow(clippy::too_many_arguments)] +pub(crate) async fn search_recipients( + Extension(state): Extension, + Extension(backend): Extension, + TypedHeader(bearer): TypedHeader>, + Query(params): Query, + OriginalUri(uri): OriginalUri, + headers: HeaderMap, +) -> impl IntoResponse { + let uri = uri.path().to_string(); + let account_id = parse_account_id(&headers); + match authenticate_endpoint( + account_id, + bearer, + uri.as_bytes(), + None, + Arc::clone(&state), + Arc::clone(&backend), + ) + .await + { + Ok(caller) => { + match handlers::search_recipients(state, backend, caller, params) + .await + { + Ok(response) => response.into_response(), + Err(error) => error.into_response(), + } + } + Err(error) => error.into_response(), + } +} + +/// Delete a shared folder. +#[utoipa::path( + delete, + path = "/sharing/folder", + security( + ("bearer_token" = []) + ), + request_body( + content_type = "application/octet-stream", + content = Vec, + ), + responses( + ( + status = StatusCode::UNAUTHORIZED, + description = "Authorization failed.", + ), + ( + status = StatusCode::FORBIDDEN, + description = "Account identifier is not allowed on this server.", + ), + ( + status = StatusCode::OK, + description = "Folder was deleted.", + ), + ), +)] +#[allow(clippy::too_many_arguments)] +pub(crate) async fn delete_folder( + Extension(state): Extension, + Extension(backend): Extension, + TypedHeader(bearer): TypedHeader>, + headers: HeaderMap, + body: Body, +) -> impl IntoResponse { + let account_id = parse_account_id(&headers); + match to_bytes(body, BODY_LIMIT).await { + Ok(bytes) => match authenticate_endpoint( + account_id, + bearer, + &bytes, + None, + Arc::clone(&state), + Arc::clone(&backend), + ) + .await + { + Ok(caller) => { + match handlers::delete_folder(state, backend, caller, bytes) + .await + { + Ok(result) => result.into_response(), + Err(error) => error.into_response(), + } + } + Err(error) => error.into_response(), + }, + Err(_) => StatusCode::BAD_REQUEST.into_response(), + } +} + +mod handlers { + use super::Caller; + use crate::{Error, Result, ServerBackend, ServerState}; + use axum::body::Bytes; + use http::header::{self, HeaderMap, HeaderValue}; + use sos_core::events::{AccountEvent, EventLog}; + use sos_protocol::{ + CreateSharedFolderRequest, CreateSharedFolderResponse, + DeleteSharedFolderRequest, DeleteSharedFolderResponse, + GetFolderInvitesRequest, GetFolderInvitesResponse, + GetRecipientResponse, SearchRecipientsRequest, + SearchRecipientsResponse, SetRecipientRequest, SetRecipientResponse, + UpdateFolderInviteRequest, UpdateFolderInviteResponse, + WireEncodeDecode, constants::MIME_TYPE_PROTOBUF, + }; + use sos_server_storage::ServerAccountStorage; + use sos_sync::StorageEventLogs; + use std::sync::Arc; + + pub(super) async fn set_recipient( + _state: ServerState, + backend: ServerBackend, + caller: Caller, + bytes: Bytes, + ) -> Result<(HeaderMap, Vec)> { + let account = { + let reader = backend.read().await; + let accounts = reader.accounts(); + let reader = accounts.read().await; + let account = reader + .get(caller.account_id()) + .ok_or_else(|| Error::NoAccount(*caller.account_id()))?; + Arc::clone(account) + }; + + let packet = SetRecipientRequest::decode(bytes).await?; + + { + let mut account = account.write().await; + account.set_recipient(packet.recipient).await?; + } + + // Empty response packet for now + let packet = SetRecipientResponse {}; + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_static(MIME_TYPE_PROTOBUF), + ); + + Ok((headers, packet.encode().await?)) + } + + pub(super) async fn get_recipient( + _state: ServerState, + backend: ServerBackend, + caller: Caller, + ) -> Result<(HeaderMap, Vec)> { + let account = { + let reader = backend.read().await; + let accounts = reader.accounts(); + let reader = accounts.read().await; + let account = reader + .get(caller.account_id()) + .ok_or_else(|| Error::NoAccount(*caller.account_id()))?; + Arc::clone(account) + }; + + let recipient = { + let mut account = account.write().await; + account.get_recipient().await? + }; + + // Empty response packet for now + let packet = GetRecipientResponse { recipient }; + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_static(MIME_TYPE_PROTOBUF), + ); + + Ok((headers, packet.encode().await?)) + } + + pub(super) async fn create_folder( + _state: ServerState, + backend: ServerBackend, + caller: Caller, + bytes: Bytes, + ) -> Result<(HeaderMap, Vec)> { + let account = { + let reader = backend.read().await; + let accounts = reader.accounts(); + let reader = accounts.read().await; + let account = reader + .get(caller.account_id()) + .ok_or_else(|| Error::NoAccount(*caller.account_id()))?; + Arc::clone(account) + }; + + let packet = CreateSharedFolderRequest::decode(bytes).await?; + + { + let mut account = account.write().await; + account + .create_shared_folder( + &packet.vault, + packet.recipients.as_slice(), + ) + .await?; + } + + // Empty response packet for now + let packet = CreateSharedFolderResponse {}; + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_static(MIME_TYPE_PROTOBUF), + ); + + Ok((headers, packet.encode().await?)) + } + + pub(super) async fn sent_folder_invites( + _state: ServerState, + backend: ServerBackend, + caller: Caller, + params: GetFolderInvitesRequest, + ) -> Result<(HeaderMap, Vec)> { + let account = { + let reader = backend.read().await; + let accounts = reader.accounts(); + let reader = accounts.read().await; + let account = reader + .get(caller.account_id()) + .ok_or_else(|| Error::NoAccount(*caller.account_id()))?; + Arc::clone(account) + }; + + let folder_invites = { + let mut account = account.write().await; + account + .sent_folder_invites(params.invite_status, params.limit) + .await? + }; + let packet = GetFolderInvitesResponse { folder_invites }; + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_static(MIME_TYPE_PROTOBUF), + ); + + Ok((headers, packet.encode().await?)) + } + + pub(super) async fn received_folder_invites( + _state: ServerState, + backend: ServerBackend, + caller: Caller, + params: GetFolderInvitesRequest, + ) -> Result<(HeaderMap, Vec)> { + let account = { + let reader = backend.read().await; + let accounts = reader.accounts(); + let reader = accounts.read().await; + let account = reader + .get(caller.account_id()) + .ok_or_else(|| Error::NoAccount(*caller.account_id()))?; + Arc::clone(account) + }; + + let folder_invites = { + let mut account = account.write().await; + account + .received_folder_invites(params.invite_status, params.limit) + .await? + }; + let packet = GetFolderInvitesResponse { folder_invites }; + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_static(MIME_TYPE_PROTOBUF), + ); + + Ok((headers, packet.encode().await?)) + } + + pub(super) async fn update_folder_invite( + _state: ServerState, + backend: ServerBackend, + caller: Caller, + bytes: Bytes, + ) -> Result<(HeaderMap, Vec)> { + let account = { + let reader = backend.read().await; + let accounts = reader.accounts(); + let reader = accounts.read().await; + let account = reader + .get(caller.account_id()) + .ok_or_else(|| Error::NoAccount(*caller.account_id()))?; + Arc::clone(account) + }; + + let params = UpdateFolderInviteRequest::decode(bytes).await?; + let mut account = account.write().await; + account + .update_folder_invite( + params.invite_status, + params.from_public_key.to_string(), + params.folder_id, + ) + .await?; + let packet = UpdateFolderInviteResponse {}; + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_static(MIME_TYPE_PROTOBUF), + ); + + Ok((headers, packet.encode().await?)) + } + + pub(super) async fn search_recipients( + _state: ServerState, + backend: ServerBackend, + caller: Caller, + params: SearchRecipientsRequest, + ) -> Result<(HeaderMap, Vec)> { + let account = { + let reader = backend.read().await; + let accounts = reader.accounts(); + let reader = accounts.read().await; + let account = reader + .get(caller.account_id()) + .ok_or_else(|| Error::NoAccount(*caller.account_id()))?; + Arc::clone(account) + }; + + let recipients = { + let mut account = account.write().await; + account + .search_recipients(params.query, params.limit) + .await? + }; + let packet = SearchRecipientsResponse { recipients }; + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_static(MIME_TYPE_PROTOBUF), + ); + + Ok((headers, packet.encode().await?)) + } + + pub(super) async fn delete_folder( + _state: ServerState, + backend: ServerBackend, + caller: Caller, + bytes: Bytes, + ) -> Result<(HeaderMap, Vec)> { + let account = { + let reader = backend.read().await; + let accounts = reader.accounts(); + let reader = accounts.read().await; + let account = reader + .get(caller.account_id()) + .ok_or_else(|| Error::NoAccount(*caller.account_id()))?; + Arc::clone(account) + }; + + let request = DeleteSharedFolderRequest::decode(bytes).await?; + + let outcome = { + let mut account = account.write().await; + account.delete_shared_folder(&request.folder_id).await? + }; + + // Empty response packet for now + let response = DeleteSharedFolderResponse { + is_creator: outcome.is_creator, + }; + + let other_recipient_account_ids = outcome + .participants + .iter() + .filter(|a| a.1 != outcome.caller_public_key) + .map(|a| a.0) + .collect::>(); + + // Inject a delete folder event into the account event log + // for the other participants which will make them delete + // the folder on the next sync. + // + // The calling client should delete their own local copy + // which will generate the event. + if outcome.is_creator { + let synthetic_account_event = + AccountEvent::DeleteFolder(request.folder_id); + let reader = backend.read().await; + let accounts = reader.accounts(); + let reader = accounts.read().await; + for account_id in &other_recipient_account_ids { + if let Some(account) = reader.get(account_id) { + let mut account = account.write().await; + + // Delete in-memory folder event logs for other participants + // the server database storage will handle the caller's. + account.delete_folder(&request.folder_id).await?; + + // Apply the account event log so participant clients will clean up + // the folder on the next sync + let account_events = account.account_log().await?; + let mut account_events = account_events.write().await; + account_events + .apply(std::slice::from_ref(&synthetic_account_event)) + .await?; + } + } + } + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + HeaderValue::from_static(MIME_TYPE_PROTOBUF), + ); + + Ok((headers, response.encode().await?)) + } +} diff --git a/crates/server/src/handlers/websocket.rs b/crates/server/src/handlers/websocket.rs index 4828a3b0d6..f8c0f53b13 100644 --- a/crates/server/src/handlers/websocket.rs +++ b/crates/server/src/handlers/websocket.rs @@ -1,22 +1,22 @@ use super::{ - authenticate_endpoint, parse_account_id, Caller, ConnectionQuery, + Caller, ConnectionQuery, authenticate_endpoint, parse_account_id, }; use crate::{Result, ServerBackend, ServerState}; use axum::{ extract::{ - ws::{Message, WebSocket, WebSocketUpgrade}, Extension, OriginalUri, Query, + ws::{Message, WebSocket, WebSocketUpgrade}, }, http::{HeaderMap, StatusCode}, response::Response, }; use axum_extra::{ - headers::{authorization::Bearer, Authorization}, + headers::{Authorization, authorization::Bearer}, typed_header::TypedHeader, }; use futures::{ - stream::{SplitSink, SplitStream}, SinkExt, StreamExt, + stream::{SplitSink, SplitStream}, }; use sos_core::AccountId; use std::{collections::HashMap, sync::Arc}; @@ -246,8 +246,8 @@ async fn write( break; } event = outgoing.recv() => { - if let Ok(msg) = event { - if sink.send(Message::Binary(msg.into())).await.is_err() { + if let Ok(msg) = event + && sink.send(Message::Binary(msg.into())).await.is_err() { tracing::trace!( account_id = %account_id, "ws_server::disconnect::send_error", @@ -259,7 +259,6 @@ async fn write( ).await; break; } - } }, } } diff --git a/crates/server/src/lib.rs b/crates/server/src/lib.rs index 7d0dfa0406..80022143fd 100644 --- a/crates/server/src/lib.rs +++ b/crates/server/src/lib.rs @@ -1,14 +1,13 @@ -#![deny(missing_docs)] -#![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -#![allow(clippy::result_large_err)] - //! Server for the [Save Our Secrets](https://saveoursecrets.com) //! sync protocol. //! //! If the `listen` feature is enabled the server is compiled //! with support for sending change notifications over //! a websocket connection. +#![deny(missing_docs)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![allow(clippy::result_large_err)] mod api_docs; mod authenticate; diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 1eeb2ca496..c3c515dbcb 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -36,7 +36,7 @@ fn init_server_logs(config: &LogConfig) -> Logger { } mod cli { - use crate::{init_default_subscriber, init_server_logs, Result}; + use crate::{Result, init_default_subscriber, init_server_logs}; use clap::{CommandFactory, Parser, Subcommand}; use sos_cli_helpers::CommandTree; use sos_server::ServerConfig; diff --git a/crates/server/src/server.rs b/crates/server/src/server.rs index a8227d9bd6..4af0883a10 100644 --- a/crates/server/src/server.rs +++ b/crates/server/src/server.rs @@ -1,20 +1,20 @@ use crate::{ - config::{self, TlsConfig}, - handlers::{account, api, home, websocket::WebSocketAccount}, Backend, Result, ServerConfig, SslConfig, StorageConfig, + config::{self, TlsConfig}, + handlers::{account, api, home, sharing, websocket::WebSocketAccount}, }; use axum::{ + Router, extract::Extension, http::{ - header::{AUTHORIZATION, CONTENT_TYPE}, HeaderValue, Method, + header::{AUTHORIZATION, CONTENT_TYPE}, }, middleware, response::{IntoResponse, Json}, routing::{get, post, put}, - Router, }; -use axum_server::{tls_rustls::RustlsConfig, Handle}; +use axum_server::{Handle, tls_rustls::RustlsConfig}; use colored::Colorize; use futures::StreamExt; use sos_core::{AccountId, UtcDateTime}; @@ -32,7 +32,7 @@ use tower_http::{ use tracing::Level; #[cfg(feature = "acme")] -use tokio_rustls_acme::{caches::DirCache, AcmeConfig}; +use tokio_rustls_acme::{AcmeConfig, caches::DirCache}; #[cfg(feature = "listen")] use super::handlers::websocket::upgrade; @@ -40,7 +40,7 @@ use super::handlers::websocket::upgrade; use sos_core::ExternalFile; #[cfg(feature = "pairing")] -use super::handlers::relay::{upgrade as relay_upgrade, RelayState}; +use super::handlers::relay::{RelayState, upgrade as relay_upgrade}; /// Server state. pub struct State { @@ -324,6 +324,31 @@ impl Server { get(account::event_scan) .post(account::event_diff) .patch(account::event_patch), + ) + .route( + "/sharing/recipient", + get(sharing::get_recipient).put(sharing::set_recipient), + ) + .route( + "/sharing/recipient/search", + get(sharing::search_recipients), + ) + .route( + "/sharing/folder", + post(sharing::create_folder) + .delete(sharing::delete_folder), + ) + .route( + "/sharing/folder/invites/sent", + get(sharing::sent_folder_invites), + ) + .route( + "/sharing/folder/invites/inbox", + get(sharing::received_folder_invites), + ) + .route( + "/sharing/folder/invites", + put(sharing::update_folder_invite), ); { diff --git a/crates/signer/Cargo.toml b/crates/signer/Cargo.toml index d7343f3483..133e3f2168 100644 --- a/crates/signer/Cargo.toml +++ b/crates/signer/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-signer" version = "0.17.2" -edition = "2021" +edition = "2024" description = "Message signing for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -22,6 +22,3 @@ bs58.workspace = true sha2.workspace = true binary-stream.workspace = true tokio.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/signer/build.rs b/crates/signer/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/signer/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/signer/src/lib.rs b/crates/signer/src/lib.rs index 494e1e7416..6890792ed3 100644 --- a/crates/signer/src/lib.rs +++ b/crates/signer/src/lib.rs @@ -1,8 +1,7 @@ +//! Traits and types for signing messages. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -//! Traits and types for signing messages. -use async_trait::async_trait; +#![cfg_attr(docsrs, feature(doc_cfg))] mod encoding; mod error; @@ -21,7 +20,7 @@ type BoxedSigner = /// This trait is declared with an async signature so that /// in the future we can support threshold signatures /// which are inherently asynchronous. -#[async_trait] +#[async_trait::async_trait] pub trait Signer { /// Signature output. type Output; @@ -58,8 +57,8 @@ pub mod ecdsa { use sha3::{Digest, Keccak256}; pub use k256::ecdsa::{ - hazmat::SignPrimitive, RecoveryId, Signature, SigningKey, - VerifyingKey, + RecoveryId, Signature, SigningKey, VerifyingKey, + hazmat::SignPrimitive, }; use super::{BoxedSigner, Signer}; @@ -145,8 +144,8 @@ pub mod ecdsa { pub mod ed25519 { use async_trait::async_trait; pub use ed25519_dalek::{ - Signature, Signer as Ed25519Signer, SigningKey, Verifier, - VerifyingKey, SECRET_KEY_LENGTH, + SECRET_KEY_LENGTH, Signature, Signer as Ed25519Signer, SigningKey, + Verifier, VerifyingKey, }; use rand::rngs::OsRng; diff --git a/crates/sos/Cargo.toml b/crates/sos/Cargo.toml index fb962b52dc..b3c3c666fb 100644 --- a/crates/sos/Cargo.toml +++ b/crates/sos/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos" version = "0.17.4" -edition = "2021" +edition = "2024" description = "Distributed, encrypted database for private secrets." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" diff --git a/crates/sos/src/cli/sos.rs b/crates/sos/src/cli/sos.rs index 53fb57b6d9..21b5d1c16c 100644 --- a/crates/sos/src/cli/sos.rs +++ b/crates/sos/src/cli/sos.rs @@ -1,12 +1,12 @@ use crate::{ + CommandTree, Result, commands::{ - account, device, environment, folder, preferences, secret, server, - shell, sync, tools, AccountCommand, DeviceCommand, - EnvironmentCommand, FolderCommand, PreferenceCommand, SecretCommand, - ServerCommand, SyncCommand, ToolsCommand, + AccountCommand, DeviceCommand, EnvironmentCommand, FolderCommand, + PreferenceCommand, SecretCommand, ServerCommand, SyncCommand, + ToolsCommand, account, device, environment, folder, preferences, + secret, server, shell, sync, tools, }, - helpers::{account::SHELL, PROGRESS_MONITOR}, - CommandTree, Result, + helpers::{PROGRESS_MONITOR, account::SHELL}, }; use clap::{CommandFactory, Parser, Subcommand}; use sos_backend::{BackendTarget, InferOptions}; @@ -145,8 +145,12 @@ pub async fn run() -> Result<()> { let options = if std::env::var("SOS_TEST").ok().is_some() { InferOptions { // When testing don't automatically use database - // backend so we can respect SOS_TEST_CLIENT_DB - use_database_when_accounts_empty: false, + // backend so we can respect SOS_TEST_CLIENT_FS + use_database_when_accounts_empty: std::env::var( + "SOS_TEST_CLIENT_FS", + ) + .ok() + .is_none(), ..Default::default() } } else { @@ -160,7 +164,9 @@ pub async fn run() -> Result<()> { #[cfg(any(test, debug_assertions))] if let Some(password) = args.password.take() { - std::env::set_var("SOS_PASSWORD", password); + unsafe { + std::env::set_var("SOS_PASSWORD", password); + } } match args.cmd { diff --git a/crates/sos/src/commands/account.rs b/crates/sos/src/commands/account.rs index bde5154799..6784a7ec3f 100644 --- a/crates/sos/src/commands/account.rs +++ b/crates/sos/src/commands/account.rs @@ -1,13 +1,13 @@ use crate::{ + Error, Result, helpers::{ account::{ - list_accounts, new_account, resolve_account, resolve_user, - verify, Owner, SHELL, USER, + Owner, SHELL, USER, list_accounts, new_account, resolve_account, + resolve_user, verify, }, messages::success, readline::read_flag, }, - Error, Result, }; use clap::Subcommand; use enum_iterator::all; diff --git a/crates/sos/src/commands/device.rs b/crates/sos/src/commands/device.rs index a09535bb20..8d962996ea 100644 --- a/crates/sos/src/commands/device.rs +++ b/crates/sos/src/commands/device.rs @@ -1,14 +1,14 @@ use crate::{ + Error, Result, helpers::{ - account::{resolve_user, Owner}, + account::{Owner, resolve_user}, messages::success, readline::read_flag, }, - Error, Result, }; use clap::Subcommand; use sos_account::Account; -use sos_core::{device::TrustedDevice, AccountRef}; +use sos_core::{AccountRef, device::TrustedDevice}; use std::sync::Arc; #[derive(Subcommand, Debug)] diff --git a/crates/sos/src/commands/environment.rs b/crates/sos/src/commands/environment.rs index 21187a9077..07c49e5a06 100644 --- a/crates/sos/src/commands/environment.rs +++ b/crates/sos/src/commands/environment.rs @@ -1,9 +1,9 @@ -use crate::{helpers::account::resolve_account_address, Result}; +use crate::{Result, helpers::account::resolve_account_address}; use clap::Subcommand; -use enum_iterator::{all, Sequence}; +use enum_iterator::{Sequence, all}; use sos_core::{ - constants::{SOS_DATA_DIR, SOS_OFFLINE, SOS_PROMPT}, AccountRef, Paths, + constants::{SOS_DATA_DIR, SOS_OFFLINE, SOS_PROMPT}, }; use std::{fmt, str::FromStr}; diff --git a/crates/sos/src/commands/folder.rs b/crates/sos/src/commands/folder.rs index 3ed48e7a7b..87bf9e6674 100644 --- a/crates/sos/src/commands/folder.rs +++ b/crates/sos/src/commands/folder.rs @@ -1,10 +1,10 @@ use crate::{ + Error, Result, helpers::{ - account::{cd_folder, resolve_folder, resolve_user, SHELL}, + account::{SHELL, cd_folder, resolve_folder, resolve_user}, messages::success, readline::read_flag, }, - Error, Result, }; use clap::Subcommand; use hex; @@ -12,8 +12,8 @@ use sos_account::{Account, FolderCreate}; use sos_backend::BackendTarget; use sos_client_storage::NewFolderOptions; use sos_core::{ - events::{EventLog, LogEvent}, AccountId, AccountRef, FolderRef, + events::{EventLog, LogEvent}, }; use sos_sync::StorageEventLogs; diff --git a/crates/sos/src/commands/preferences.rs b/crates/sos/src/commands/preferences.rs index 41d928ef92..bf03a0aabe 100644 --- a/crates/sos/src/commands/preferences.rs +++ b/crates/sos/src/commands/preferences.rs @@ -1,9 +1,9 @@ use crate::{ + Error, Result, helpers::{ account::resolve_user, messages::{fail, success}, }, - Error, Result, }; use clap::Subcommand; use sos_account::Account; diff --git a/crates/sos/src/commands/secret.rs b/crates/sos/src/commands/secret.rs index dd4e92861d..2e76d82eab 100644 --- a/crates/sos/src/commands/secret.rs +++ b/crates/sos/src/commands/secret.rs @@ -1,34 +1,34 @@ use crate::{ + Error, Result, helpers::{ - account::{resolve_folder, resolve_user, verify, Owner, SHELL}, + account::{Owner, SHELL, resolve_folder, resolve_user, verify}, editor, messages::success, readline::{read_flag, read_line}, secret::{ - add_file, add_link, add_list, add_login, add_note, add_password, - copy_secret_text, download_file_secret, normalize_tags, - print_secret, read_file_secret, read_name, resolve_secret, - ResolvedSecret, + ResolvedSecret, add_file, add_link, add_list, add_login, + add_note, add_password, copy_secret_text, download_file_secret, + normalize_tags, print_secret, read_file_secret, read_name, + resolve_secret, }, }, - Error, Result, }; use clap::Subcommand; use crossterm::{ execute, terminal::{Clear, ClearType}, }; -use futures::{future::LocalBoxFuture, select, FutureExt}; +use futures::{FutureExt, future::LocalBoxFuture, select}; use human_bytes::human_bytes; -use kdam::{term, tqdm, BarExt, Column, RichProgress, Spinner}; +use kdam::{BarExt, Column, RichProgress, Spinner, term, tqdm}; use sos_account::Account; use sos_client_storage::AccessOptions; use sos_core::{AccountRef, FolderRef}; use sos_external_files::FileProgress; use sos_search::{ArchiveFilter, Document, DocumentView}; use sos_vault::{ - secret::{Secret, SecretId, SecretMeta, SecretRef, SecretRow}, Summary, + secret::{Secret, SecretId, SecretMeta, SecretRef, SecretRow}, }; use sos_vfs as vfs; use std::{borrow::Cow, collections::HashSet, path::PathBuf, sync::Arc}; diff --git a/crates/sos/src/commands/server.rs b/crates/sos/src/commands/server.rs index 7b09bd75b4..9730c784e0 100644 --- a/crates/sos/src/commands/server.rs +++ b/crates/sos/src/commands/server.rs @@ -1,9 +1,9 @@ use crate::{ + Error, Result, helpers::{ account::resolve_user, messages::{fail, success}, }, - Error, Result, }; use clap::Subcommand; use sos_core::{AccountRef, Origin}; @@ -49,11 +49,10 @@ pub async fn run(cmd: Command) -> Result<()> { let origin: Origin = url.into(); let sync_result = owner.add_server(origin.clone()).await?; - if let Some(res) = sync_result { - if let Err(err) = res.result { + if let Some(res) = sync_result + && let Err(err) = res.result { return Err(Error::InitialSync(err)); } - } success(format!("Added {}", origin.url())); } diff --git a/crates/sos/src/commands/shell/cli.rs b/crates/sos/src/commands/shell/cli.rs index 9d9c03c83c..7c7252bddf 100644 --- a/crates/sos/src/commands/shell/cli.rs +++ b/crates/sos/src/commands/shell/cli.rs @@ -1,11 +1,11 @@ use super::repl::exec; use crate::{ + Error, Result, helpers::{ - account::{cd_folder, choose_account, sign_in, SHELL, USER}, + account::{SHELL, USER, cd_folder, choose_account, sign_in}, messages::fail, readline, }, - Error, Result, }; use sos_account::Account; use sos_core::{AccountRef, FolderRef, Paths}; diff --git a/crates/sos/src/commands/shell/repl.rs b/crates/sos/src/commands/shell/repl.rs index ac422f1177..b7bd16225d 100644 --- a/crates/sos/src/commands/shell/repl.rs +++ b/crates/sos/src/commands/shell/repl.rs @@ -1,10 +1,10 @@ use crate::{ + Error, Result, commands::{ AccountCommand, EnvironmentCommand, FolderCommand, PreferenceCommand, SecretCommand, ServerCommand, SyncCommand, ToolsCommand, }, - helpers::account::{cd_folder, switch, USER}, - Error, Result, + helpers::account::{USER, cd_folder, switch}, }; use clap::{CommandFactory, Parser, Subcommand}; use sos_account::Account; diff --git a/crates/sos/src/commands/sync.rs b/crates/sos/src/commands/sync.rs index 69206d7891..ce1317bce3 100644 --- a/crates/sos/src/commands/sync.rs +++ b/crates/sos/src/commands/sync.rs @@ -1,13 +1,13 @@ use crate::{ - helpers::{account::resolve_user, messages::success}, Error, Result, + helpers::{account::resolve_user, messages::success}, }; use clap::Subcommand; use sos_account::Account; use sos_core::{ + AccountRef, Origin, commit::{CommitState, CommitTree, Comparison}, events::EventLog, - AccountRef, Origin, }; use sos_net::NetworkAccount; use sos_protocol::{AccountSync, SyncOptions}; diff --git a/crates/sos/src/commands/tools/audit.rs b/crates/sos/src/commands/tools/audit.rs index 0134fea71b..78ec7c7f85 100644 --- a/crates/sos/src/commands/tools/audit.rs +++ b/crates/sos/src/commands/tools/audit.rs @@ -1,6 +1,6 @@ -use crate::{helpers::messages::info, Error, Result}; +use crate::{Error, Result, helpers::messages::info}; use clap::Subcommand; -use futures::{pin_mut, StreamExt}; +use futures::{StreamExt, pin_mut}; use sos_audit::{AuditData, AuditEvent}; use sos_core::AccountId; use sos_vfs::{self as vfs}; diff --git a/crates/sos/src/commands/tools/authenticator.rs b/crates/sos/src/commands/tools/authenticator.rs index 2938cd9d62..4691954f23 100644 --- a/crates/sos/src/commands/tools/authenticator.rs +++ b/crates/sos/src/commands/tools/authenticator.rs @@ -1,8 +1,8 @@ use crate::{ + Error, Result, helpers::{ account::resolve_user, messages::success, readline::read_flag, }, - Error, Result, }; use clap::Subcommand; use sos_account::{Account, FolderCreate}; diff --git a/crates/sos/src/commands/tools/backup.rs b/crates/sos/src/commands/tools/backup.rs index a81282b63d..23ee452dd0 100644 --- a/crates/sos/src/commands/tools/backup.rs +++ b/crates/sos/src/commands/tools/backup.rs @@ -1,11 +1,11 @@ use crate::{ - helpers::account::{find_account, resolve_account}, Error, Result, + helpers::account::{find_account, resolve_account}, }; use clap::Subcommand; use sos_backend::{ - archive::{list_backup_archive_accounts, read_backup_archive_manifest}, BackendTarget, + archive::{list_backup_archive_accounts, read_backup_archive_manifest}, }; use sos_cli_helpers::messages::success; use sos_core::{AccountId, AccountRef, Paths, PublicIdentity}; diff --git a/crates/sos/src/commands/tools/check.rs b/crates/sos/src/commands/tools/check.rs index 4a9106a6d6..05a590a5aa 100644 --- a/crates/sos/src/commands/tools/check.rs +++ b/crates/sos/src/commands/tools/check.rs @@ -1,12 +1,12 @@ use crate::{ + Error, Result, helpers::{ account::resolve_account_address, messages::{fail, success}, }, - Error, Result, }; use clap::Subcommand; -use futures::{pin_mut, StreamExt}; +use futures::{StreamExt, pin_mut}; use sos_backend::BackendTarget; use sos_client_storage::{ClientFolderStorage, ClientStorage}; use sos_core::{AccountRef, FolderRef, Paths}; diff --git a/crates/sos/src/commands/tools/db.rs b/crates/sos/src/commands/tools/db.rs index 591287d3ee..c415986ca2 100644 --- a/crates/sos/src/commands/tools/db.rs +++ b/crates/sos/src/commands/tools/db.rs @@ -5,7 +5,7 @@ use sos_cli_helpers::messages::{info, success, warn}; use sos_core::Paths; use sos_database::{migrations::migrate_client, open_file}; use sos_database_upgrader::{ - archive::upgrade_backup_archive, upgrade_accounts, UpgradeOptions, + UpgradeOptions, archive::upgrade_backup_archive, upgrade_accounts, }; use sos_vault::list_accounts; use std::path::PathBuf; diff --git a/crates/sos/src/commands/tools/debug.rs b/crates/sos/src/commands/tools/debug.rs index 4200f48e18..74ee423ca1 100644 --- a/crates/sos/src/commands/tools/debug.rs +++ b/crates/sos/src/commands/tools/debug.rs @@ -1,9 +1,9 @@ -use crate::{helpers::account::resolve_account_address, Result}; +use crate::{Result, helpers::account::resolve_account_address}; use clap::Subcommand; use sos_backend::BackendTarget; use sos_client_storage::ClientStorage; use sos_core::{AccountRef, Paths}; -use sos_debug_snapshot::{export_debug_snapshot, DebugSnapshotOptions}; +use sos_debug_snapshot::{DebugSnapshotOptions, export_debug_snapshot}; use sos_sync::SyncStorage; use std::path::PathBuf; diff --git a/crates/sos/src/commands/tools/events.rs b/crates/sos/src/commands/tools/events.rs index f840b56ee7..0a7e842154 100644 --- a/crates/sos/src/commands/tools/events.rs +++ b/crates/sos/src/commands/tools/events.rs @@ -1,4 +1,4 @@ -use crate::{helpers::account::resolve_account_address, Error, Result}; +use crate::{Error, Result, helpers::account::resolve_account_address}; use binary_stream::futures::{Decodable, Encodable}; use clap::Subcommand; use futures::{pin_mut, stream::StreamExt}; @@ -8,12 +8,12 @@ use sos_backend::{ FileEventLog, FolderEventLog, }; use sos_core::{ + AccountRef, FolderRef, Paths, UtcDateTime, commit::{CommitHash, CommitTree}, events::{ AccountEvent, DeviceEvent, EventKind, EventLog, FileEvent, LogEvent, WriteEvent, }, - AccountRef, FolderRef, Paths, UtcDateTime, }; #[derive(Debug, Serialize)] @@ -217,11 +217,10 @@ async fn print_events< tree.append(&mut vec![record.commit().into()]); tree.commit(); - if let Some(commit) = &until_commit { - if commit == record.commit() { + if let Some(commit) = &until_commit + && commit == record.commit() { break; } - } } if !tree.is_empty() { diff --git a/crates/sos/src/commands/tools/mod.rs b/crates/sos/src/commands/tools/mod.rs index abdb13e832..aa529a51d9 100644 --- a/crates/sos/src/commands/tools/mod.rs +++ b/crates/sos/src/commands/tools/mod.rs @@ -1,10 +1,10 @@ use crate::{ + Error, Result, helpers::{ account::{resolve_account_address, resolve_user_with_password}, messages::{info, success}, readline::read_flag, }, - Error, Result, }; use clap::Subcommand; use sos_account::Account; @@ -12,9 +12,9 @@ use sos_backend::BackendTarget; use sos_client_storage::ClientStorage; use sos_core::FolderRef; use sos_core::{ + AccountRef, Paths, constants::EVENT_LOG_EXT, crypto::{AccessKey, Cipher, KeyDerivation}, - AccountRef, Paths, }; use std::path::PathBuf; use terminal_banner::{Banner, Padding}; @@ -31,7 +31,7 @@ mod security_report; use audit::Command as AuditCommand; use authenticator::Command as AuthenticatorCommand; use backup::Command as BackupCommand; -use check::{verify_events, Command as CheckCommand}; +use check::{Command as CheckCommand, verify_events}; use db::Command as DbCommand; use debug::Command as DebugCommand; use events::Command as EventsCommand; diff --git a/crates/sos/src/commands/tools/security_report.rs b/crates/sos/src/commands/tools/security_report.rs index 0f42f75349..b4a195524c 100644 --- a/crates/sos/src/commands/tools/security_report.rs +++ b/crates/sos/src/commands/tools/security_report.rs @@ -1,11 +1,11 @@ use crate::{ - helpers::{account::resolve_user, messages::success}, Error, Result, + helpers::{account::resolve_user, messages::success}, }; use sos_core::AccountRef; -use sos_net::{hashcheck, NetworkAccount}; +use sos_net::{NetworkAccount, hashcheck}; use sos_security_report::{ - generate_security_report, SecurityReportOptions, SecurityReportRow, + SecurityReportOptions, SecurityReportRow, generate_security_report, }; use std::{fmt, path::PathBuf, str::FromStr}; use zxcvbn::Score; diff --git a/crates/sos/src/error.rs b/crates/sos/src/error.rs index f854010b90..a305841fb5 100644 --- a/crates/sos/src/error.rs +++ b/crates/sos/src/error.rs @@ -54,7 +54,9 @@ pub enum Error { NoMatchServers, /// Failed to copy to the clipboard. - #[error("unable to copy to the clipboard, secret type may not support copy operation")] + #[error( + "unable to copy to the clipboard, secret type may not support copy operation" + )] ClipboardCopy, /// Archive folder not found. diff --git a/crates/sos/src/helpers/account.rs b/crates/sos/src/helpers/account.rs index d065f2dc49..b55886bd3a 100644 --- a/crates/sos/src/helpers/account.rs +++ b/crates/sos/src/helpers/account.rs @@ -2,7 +2,7 @@ use crate::helpers::{ display_passphrase, messages::success, - readline::{choose, choose_password, read_flag, read_password, Choice}, + readline::{Choice, choose, choose_password, read_flag, read_password}, }; use crate::{Error, Result}; use parking_lot::Mutex; @@ -10,8 +10,8 @@ use secrecy::{ExposeSecret, SecretString}; use sos_account::Account; use sos_backend::BackendTarget; use sos_core::{ - constants::DEFAULT_VAULT_NAME, crypto::AccessKey, AccountId, AccountRef, - FolderRef, Paths, PublicIdentity, + AccountId, AccountRef, FolderRef, Paths, PublicIdentity, + constants::DEFAULT_VAULT_NAME, crypto::AccessKey, }; use sos_net::{NetworkAccount, NetworkAccountSwitcher}; use sos_password::diceware::generate_passphrase; @@ -138,20 +138,18 @@ pub async fn resolve_account( if account.is_none() { if is_shell { let owner = USER.read().await; - if let Some(owner) = owner.selected_account() { - if owner.is_authenticated().await { + if let Some(owner) = owner.selected_account() + && owner.is_authenticated().await { return Ok(Some((owner).into())); } - } } let paths = Paths::new_client(Paths::data_dir()?); let target = BackendTarget::from_paths(&paths).await?; - if let Ok(mut accounts) = target.list_accounts().await { - if accounts.len() == 1 { + if let Ok(mut accounts) = target.list_accounts().await + && accounts.len() == 1 { return Ok(Some(accounts.remove(0).into())); } - } } Ok(account.cloned()) } diff --git a/crates/sos/src/helpers/editor.rs b/crates/sos/src/helpers/editor.rs index fb38b04e3e..109b879efe 100644 --- a/crates/sos/src/helpers/editor.rs +++ b/crates/sos/src/helpers/editor.rs @@ -3,7 +3,7 @@ //! VIM users should use `set nofixendofline` in their .vimrc //! to prevent an appended newline changing the file automatically. //! -use crate::{helpers::messages::fail, Error, Result}; +use crate::{Error, Result, helpers::messages::fail}; use async_recursion::async_recursion; use secrecy::ExposeSecret; use sha2::{Digest, Sha256}; diff --git a/crates/sos/src/helpers/readline.rs b/crates/sos/src/helpers/readline.rs index 2c1b9159ad..072d232eec 100644 --- a/crates/sos/src/helpers/readline.rs +++ b/crates/sos/src/helpers/readline.rs @@ -1,7 +1,7 @@ -use crate::{helpers::messages::fail, Error, Result}; +use crate::{Error, Result, helpers::messages::fail}; use rustyline::{ - config::Configurer, error::ReadlineError, highlight::Highlighter, - history::MemHistory, ColorMode, Editor, + ColorMode, Editor, config::Configurer, error::ReadlineError, + highlight::Highlighter, history::MemHistory, }; use rustyline_derive::{Completer, Helper, Hinter, Validator}; use secrecy::{ExposeSecret, SecretString}; diff --git a/crates/sos/src/helpers/secret.rs b/crates/sos/src/helpers/secret.rs index 7118a8d9aa..e35ed3f356 100644 --- a/crates/sos/src/helpers/secret.rs +++ b/crates/sos/src/helpers/secret.rs @@ -1,5 +1,6 @@ use super::{account::Owner, set_clipboard_text}; use crate::{ + Error, Result, helpers::{ messages::{fail, success}, readline::{ @@ -7,7 +8,6 @@ use crate::{ read_option, read_password, }, }, - Error, Result, }; use human_bytes::human_bytes; use secrecy::{ExposeSecret, SecretString}; @@ -15,8 +15,8 @@ use sos_account::Account; use sos_core::ExternalFileName; use sos_search::Document; use sos_vault::{ - secret::{FileContent, Secret, SecretId, SecretMeta, SecretRef}, Summary, + secret::{FileContent, Secret, SecretId, SecretMeta, SecretRef}, }; use sos_vfs as vfs; use std::{ diff --git a/crates/sos/src/lib.rs b/crates/sos/src/lib.rs index cade75880d..f6717af630 100644 --- a/crates/sos/src/lib.rs +++ b/crates/sos/src/lib.rs @@ -5,7 +5,7 @@ //! //! See the [CLI documentation](https://saveoursecrets.com/docs/cli/) for usage information or browse the [online help manual](https://saveoursecrets.com/docs/cli/help/); the libraries are available at [sos-sdk](https://docs.rs/sos-sdk/) and [sos-net](https://docs.rs/sos-net/). #![deny(missing_docs)] -#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] #![allow(clippy::result_large_err)] #[doc(hidden)] diff --git a/crates/storage/client/Cargo.toml b/crates/storage/client/Cargo.toml index 94a1f5a027..1e98113e59 100644 --- a/crates/storage/client/Cargo.toml +++ b/crates/storage/client/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-client-storage" version = "0.17.2" -edition = "2021" +edition = "2024" description = "Client storage for the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -57,6 +57,3 @@ parking_lot.workspace = true binary-stream = { workspace = true, optional = true } tokio-util = { workspace = true, optional = true } age = { workspace = true, optional = true } - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/storage/client/build.rs b/crates/storage/client/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/storage/client/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/storage/client/src/database.rs b/crates/storage/client/src/database.rs index abf144b6e3..e3c8b288d3 100644 --- a/crates/storage/client/src/database.rs +++ b/crates/storage/client/src/database.rs @@ -1,22 +1,21 @@ //! Storage backed by a database. use crate::{ - traits::private::Internal, ClientAccountStorage, ClientBaseStorage, - ClientDeviceStorage, ClientEventLogStorage, ClientFolderStorage, - ClientVaultStorage, Error, Result, + ClientAccountStorage, ClientBaseStorage, ClientDeviceStorage, + ClientEventLogStorage, ClientFolderStorage, ClientVaultStorage, Error, + Result, traits::private::Internal, }; use async_trait::async_trait; use indexmap::IndexSet; use parking_lot::Mutex; use sos_backend::{ - extract_vault, AccountEventLog, BackendTarget, DeviceEventLog, Folder, - FolderEventLog, StorageError, + AccountEventLog, BackendTarget, DeviceEventLog, Folder, FolderEventLog, + StorageError, extract_vault, }; use sos_core::{ - decode, + AccountId, Paths, SecretId, VaultFlags, VaultId, decode, device::TrustedDevice, encode, events::{DeviceEvent, Event, EventLog, ReadEvent}, - AccountId, Paths, SecretId, VaultFlags, VaultId, }; use sos_database::{ async_sqlite::Client, @@ -328,11 +327,12 @@ impl ClientVaultStorage for ClientDatabaseStorage { folder_id: &VaultId, _: Internal, ) -> Result<()> { + let account_row_id = self.account_row_id; let folder_id = *folder_id; self.client .conn(move |conn| { let folder_entity = FolderEntity::new(&conn); - folder_entity.delete_folder(&folder_id) + folder_entity.delete_folder(account_row_id, &folder_id) }) .await .map_err(sos_database::Error::from)?; diff --git a/crates/storage/client/src/files/file_manager.rs b/crates/storage/client/src/files/file_manager.rs index ce77b2505f..744ed5947c 100644 --- a/crates/storage/client/src/files/file_manager.rs +++ b/crates/storage/client/src/files/file_manager.rs @@ -1,23 +1,23 @@ //! File manager to keep external files in sync //! as secrets are created, updated and moved. -use crate::{files::FileStorage, Error, Result}; +use crate::{Error, Result, files::FileStorage}; use hex; use sos_backend::FileEventLog; use sos_core::events::{EventLog, FileEvent}; use sos_core::{ - basename, ExternalFileName, Paths, SecretId, SecretPath, VaultId, + ExternalFileName, Paths, SecretId, SecretPath, VaultId, basename, }; use sos_external_files::{ EncryptedFile, FileMutationEvent, FileProgress, FileSource, FileStorageDiff, FileStorageResult, }; use sos_vault::{ - secret::{FileContent, Secret, SecretRow, UserData}, Summary, + secret::{FileContent, Secret, SecretRow, UserData}, }; use sos_vfs as vfs; use std::{collections::HashMap, path::Path, sync::Arc}; -use tokio::sync::{mpsc, RwLock}; +use tokio::sync::{RwLock, mpsc}; /// Manages external files. pub struct ExternalFileManager { @@ -377,11 +377,10 @@ impl ExternalFileManager { file_name, ); - if let Some(parent) = new_path.parent() { - if !vfs::try_exists(parent).await? { + if let Some(parent) = new_path.parent() + && !vfs::try_exists(parent).await? { vfs::create_dir_all(parent).await?; } - } vfs::rename(old_path, new_path).await?; @@ -570,8 +569,7 @@ fn get_file_sources(secret: &Secret) -> Vec { content: FileContent::External { path, .. }, .. } = secret - { - if path.is_some() { + && path.is_some() { let name = basename(path.as_ref().unwrap()); files.push(FileSource { path: path.clone().unwrap(), @@ -579,7 +577,6 @@ fn get_file_sources(secret: &Secret) -> Vec { field_index, }); } - } } let mut files = Vec::new(); @@ -623,11 +620,9 @@ fn get_file_secret_diff<'a>( content: FileContent::External { path, .. }, .. } = new_secret - { - if path.is_none() { + && path.is_none() { unchanged.push(new_secret); } - } // Check if the top-level secret will be overwritten // so we delete the old files @@ -635,11 +630,9 @@ fn get_file_secret_diff<'a>( content: FileContent::External { path, .. }, .. } = new_secret - { - if path.is_some() { + && path.is_some() { deleted.push(old_secret); } - } // Find attachments that are unchanged for field in new_secret.user_data().fields() { @@ -647,11 +640,9 @@ fn get_file_secret_diff<'a>( content: FileContent::External { path, .. }, .. } = field.secret() - { - if path.is_none() { + && path.is_none() { unchanged.push(field.secret()); } - } } // Find deleted attachments @@ -660,8 +651,7 @@ fn get_file_secret_diff<'a>( content: FileContent::External { path, .. }, .. } = field.secret() - { - if path.is_none() { + && path.is_none() { let existing = new_secret.user_data().fields().iter().find(|other| { // Must compare on secret as the label can @@ -675,7 +665,6 @@ fn get_file_secret_diff<'a>( deleted.push(field.secret()); } } - } } FileStorageDiff { deleted, unchanged } diff --git a/crates/storage/client/src/filesystem.rs b/crates/storage/client/src/filesystem.rs index c4752818fc..9aab1ebc8d 100644 --- a/crates/storage/client/src/filesystem.rs +++ b/crates/storage/client/src/filesystem.rs @@ -1,8 +1,8 @@ //! Storage backed by the filesystem. use crate::{ - traits::private::Internal, ClientAccountStorage, ClientBaseStorage, - ClientDeviceStorage, ClientEventLogStorage, ClientFolderStorage, - ClientVaultStorage, Error, Result, + ClientAccountStorage, ClientBaseStorage, ClientDeviceStorage, + ClientEventLogStorage, ClientFolderStorage, ClientVaultStorage, Error, + Result, traits::private::Internal, }; use async_trait::async_trait; use indexmap::IndexSet; @@ -12,12 +12,12 @@ use sos_backend::{ StorageError, }; use sos_core::{ + AccountId, Paths, SecretId, VaultFlags, VaultId, constants::VAULT_EXT, decode, device::TrustedDevice, encode, events::{DeviceEvent, Event, EventLog, EventLogType, ReadEvent}, - AccountId, Paths, SecretId, VaultFlags, VaultId, }; use sos_filesystem::write_exclusive; use sos_login::Identity; @@ -290,15 +290,14 @@ impl ClientVaultStorage for ClientFileSystemStorage { let mut contents = vfs::read_dir(&storage).await?; while let Some(entry) = contents.next_entry().await? { let path = entry.path(); - if let Some(extension) = path.extension() { - if extension == VAULT_EXT { + if let Some(extension) = path.extension() + && extension == VAULT_EXT { let summary = Header::read_summary_file(path).await?; if summary.flags().is_system() { continue; } summaries.push(summary); } - } } Ok(summaries) } diff --git a/crates/storage/client/src/folder_sync.rs b/crates/storage/client/src/folder_sync.rs index 3192242084..7360fc496c 100644 --- a/crates/storage/client/src/folder_sync.rs +++ b/crates/storage/client/src/folder_sync.rs @@ -3,15 +3,15 @@ use crate::Result; use async_trait::async_trait; use sos_backend::Folder; use sos_core::{ + VaultId, events::{ - patch::{CheckedPatch, FolderDiff}, EventLog, LogEvent, WriteEvent, + patch::{CheckedPatch, FolderDiff}, }, - VaultId, }; use sos_login::IdentityFolder; use sos_reducers::FolderReducer; -use sos_vault::{secret::SecretRow, SecretAccess}; +use sos_vault::{SecretAccess, secret::SecretRow}; /// Options for folder merge. pub(crate) enum FolderMergeOptions<'a> { diff --git a/crates/storage/client/src/lib.rs b/crates/storage/client/src/lib.rs index ab28d84b26..484ac0af6e 100644 --- a/crates/storage/client/src/lib.rs +++ b/crates/storage/client/src/lib.rs @@ -1,11 +1,12 @@ +//! Client storage for a backend target. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -//! Client storage for a backend target. +#![cfg_attr(docsrs, feature(doc_cfg))] + use sos_core::{ + AccountId, VaultFlags, VaultId, crypto::{AccessKey, Cipher, KeyDerivation}, events::WriteEvent, - AccountId, VaultFlags, VaultId, }; use sos_vault::Vault; @@ -54,10 +55,7 @@ impl NewFolderOptions { pub fn new(name: String) -> Self { Self { name, - flags: None, - key: None, - cipher: None, - kdf: None, + ..Default::default() } } } diff --git a/crates/storage/client/src/secret_storage.rs b/crates/storage/client/src/secret_storage.rs index c2b1ed67cd..20d68d4e38 100644 --- a/crates/storage/client/src/secret_storage.rs +++ b/crates/storage/client/src/secret_storage.rs @@ -8,11 +8,11 @@ use crate::{ use async_trait::async_trait; use sos_backend::StorageError; use sos_core::{ - events::{ReadEvent, WriteEvent}, SecretId, VaultCommit, VaultId, + events::{ReadEvent, WriteEvent}, }; -use sos_vault::secret::{Secret, SecretMeta, SecretRow}; use sos_vault::Summary; +use sos_vault::secret::{Secret, SecretMeta, SecretRow}; #[cfg(feature = "files")] use sos_core::AuthenticationError; @@ -30,10 +30,8 @@ where async fn create_secret( &mut self, secret_data: SecretRow, - #[cfg(not(feature = "files"))] - options: AccessOptions, - #[cfg(feature = "files")] - mut options: AccessOptions, + #[cfg(not(feature = "files"))] options: AccessOptions, + #[cfg(feature = "files")] mut options: AccessOptions, ) -> Result { self.guard_authenticated(Internal)?; @@ -235,10 +233,8 @@ where folder: &Summary, id: &SecretId, mut secret_data: SecretRow, - #[cfg(not(feature = "search"))] - _is_update: bool, - #[cfg(feature = "search")] - is_update: bool, + #[cfg(not(feature = "search"))] _is_update: bool, + #[cfg(feature = "search")] is_update: bool, _: Internal, ) -> Result { // let summary = self.current_folder().ok_or(Error::NoOpenVault)?; diff --git a/crates/storage/client/src/storage.rs b/crates/storage/client/src/storage.rs index b4fa1ccaab..c11f05320b 100644 --- a/crates/storage/client/src/storage.rs +++ b/crates/storage/client/src/storage.rs @@ -1,11 +1,11 @@ use crate::{ + ClientEventLogStorage, Error, Result, database::ClientDatabaseStorage, filesystem::ClientFileSystemStorage, traits::{ - private::Internal, ClientAccountStorage, ClientBaseStorage, - ClientDeviceStorage, ClientFolderStorage, ClientVaultStorage, + ClientAccountStorage, ClientBaseStorage, ClientDeviceStorage, + ClientFolderStorage, ClientVaultStorage, private::Internal, }, - ClientEventLogStorage, Error, Result, }; use async_trait::async_trait; use indexmap::IndexSet; @@ -13,12 +13,12 @@ use sos_backend::{ AccountEventLog, BackendTarget, DeviceEventLog, Folder, FolderEventLog, }; use sos_core::{ + AccountId, Paths, SecretId, VaultId, device::TrustedDevice, events::{ - patch::{AccountDiff, CheckedPatch, DeviceDiff, FolderDiff}, Event, ReadEvent, WriteEvent, + patch::{AccountDiff, CheckedPatch, DeviceDiff, FolderDiff}, }, - AccountId, Paths, SecretId, VaultId, }; use sos_login::Identity; use sos_reducers::FolderReducer; diff --git a/crates/storage/client/src/sync.rs b/crates/storage/client/src/sync.rs index e64bf729cf..753bf22d27 100644 --- a/crates/storage/client/src/sync.rs +++ b/crates/storage/client/src/sync.rs @@ -1,8 +1,8 @@ use crate::{ - folder_sync::{FolderMerge, FolderMergeOptions, IdentityFolderMerge}, - traits::private::Internal, ClientAccountStorage, ClientDeviceStorage, ClientFolderStorage, Error, Result, + folder_sync::{FolderMerge, FolderMergeOptions, IdentityFolderMerge}, + traits::private::Internal, }; use async_trait::async_trait; use indexmap::IndexSet; @@ -10,12 +10,11 @@ use sos_backend::{ AccountEventLog, DeviceEventLog, FolderEventLog, StorageError, }; use sos_core::{ - decode, + AuthenticationError, VaultId, decode, events::{ - patch::{AccountDiff, CheckedPatch, DeviceDiff, FolderDiff}, AccountEvent, EventLog, LogEvent, WriteEvent, + patch::{AccountDiff, CheckedPatch, DeviceDiff, FolderDiff}, }, - AuthenticationError, VaultId, }; use sos_login::DelegatedAccess; use sos_reducers::DeviceReducer; diff --git a/crates/storage/client/src/traits.rs b/crates/storage/client/src/traits.rs index d2d02c31d5..9c5a3935f9 100644 --- a/crates/storage/client/src/traits.rs +++ b/crates/storage/client/src/traits.rs @@ -4,33 +4,33 @@ use crate::{ StorageChangeEvent, }; use async_trait::async_trait; -use futures::{pin_mut, StreamExt}; +use futures::{StreamExt, pin_mut}; use indexmap::IndexSet; use sos_backend::{ - compact::compact_folder, extract_vault, BackendTarget, DeviceEventLog, - Folder, FolderEventLog, + BackendTarget, DeviceEventLog, Folder, FolderEventLog, + compact::compact_folder, extract_vault, }; use sos_core::{ + AccountId, AuthenticationError, FolderRef, Paths, SecretId, StorageError, + UtcDateTime, VaultCommit, VaultFlags, VaultId, commit::{CommitHash, CommitState}, crypto::AccessKey, decode, device::{DevicePublicKey, TrustedDevice}, encode, events::{ - patch::FolderPatch, AccountEvent, DeviceEvent, Event, - EventLog, EventRecord, ReadEvent, WriteEvent, + AccountEvent, DeviceEvent, Event, EventLog, EventRecord, ReadEvent, + WriteEvent, patch::FolderPatch, }, - AccountId, AuthenticationError, FolderRef, Paths, SecretId, StorageError, - UtcDateTime, VaultCommit, VaultFlags, VaultId, }; use sos_login::{DelegatedAccess, FolderKeys, Identity}; use sos_password::diceware::generate_passphrase; use sos_reducers::{DeviceReducer, FolderReducer}; use sos_sync::{CreateSet, StorageEventLogs}; use sos_vault::{ - secret::{Secret, SecretMeta, SecretRow}, BuilderCredentials, ChangePassword, SecretAccess, Summary, Vault, VaultBuilder, + secret::{Secret, SecretMeta, SecretRow}, }; use std::{collections::HashMap, sync::Arc}; use tokio::sync::RwLock; @@ -417,10 +417,10 @@ pub trait ClientFolderStorage: // If the deleted vault is the currently selected // vault we must close it - if let Some(id) = ¤t_id { - if id == folder_id { - self.close_folder(); - } + if let Some(id) = ¤t_id + && id == folder_id + { + self.close_folder(); } // Remove from our cache of managed vaults @@ -1135,7 +1135,7 @@ pub trait ClientAccountStorage: /// Prepare a new folder. #[doc(hidden)] - async fn prepare_folder( + async fn prepare_new_folder( &mut self, mut options: NewFolderOptions, _: Internal, @@ -1162,14 +1162,37 @@ pub trait ClientAccountStorage: )) .await? } - AccessKey::Identity(id) => { + AccessKey::Identity(_id) => { + panic!( + "prepare_new_folder does not support asymmetric folder encryption" + ); + + /* + let (recipients, read_only) = if let Some(shared_access) = + options.shared_access + { + let recipients = match &shared_access { + SharedAccess::WriteAccess(list) => { + SharedAccess::parse_recipients(list)? + } + SharedAccess::ReadOnly(_) => vec![], + }; + ( + recipients, + matches!(shared_access, SharedAccess::ReadOnly(_)), + ) + } else { + (vec![], true) + }; + builder .build(BuilderCredentials::Shared { owner: id, - recipients: vec![], - read_only: true, + recipients, + read_only, }) .await? + */ } }; @@ -1196,7 +1219,7 @@ pub trait ClientAccountStorage: self.guard_authenticated(Internal)?; let (buf, key, summary) = - self.prepare_folder(options, Internal).await?; + self.prepare_new_folder(options, Internal).await?; let account_event = AccountEvent::CreateFolder(*summary.id(), buf.clone()); @@ -1328,11 +1351,9 @@ pub trait ClientAccountStorage: let summary = vault.summary().clone(); #[cfg(feature = "search")] - if exists { - if let Some(index) = self.search_index_mut() { - // Clean entries from the search index - index.remove_folder(summary.id()).await; - } + if exists && let Some(index) = self.search_index_mut() { + // Clean entries from the search index + index.remove_folder(summary.id()).await; } self.write_vault(&vault, Internal).await?; @@ -1352,11 +1373,11 @@ pub trait ClientAccountStorage: } #[cfg(feature = "search")] - if let Some(key) = key { - if let Some(index) = self.search_index_mut() { - // Ensure the imported secrets are in the search index - index.add_vault(vault.clone(), key).await?; - } + if let Some(key) = key + && let Some(index) = self.search_index_mut() + { + // Ensure the imported secrets are in the search index + index.add_vault(vault.clone(), key).await?; } let event = vault.into_event().await?; diff --git a/crates/storage/server/Cargo.toml b/crates/storage/server/Cargo.toml index abae252603..79c97e5854 100644 --- a/crates/storage/server/Cargo.toml +++ b/crates/storage/server/Cargo.toml @@ -45,6 +45,3 @@ sha2.workspace = true serde.workspace = true futures.workspace = true binary-stream.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/storage/server/build.rs b/crates/storage/server/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/storage/server/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/storage/server/src/database.rs b/crates/storage/server/src/database.rs index 3cc2a27bce..04535b885e 100644 --- a/crates/storage/server/src/database.rs +++ b/crates/storage/server/src/database.rs @@ -14,12 +14,14 @@ use sos_core::{ patch::{FolderDiff, FolderPatch}, AccountEvent, EventLog, }, - AccountId, Paths, VaultFlags, VaultId, + AccountId, FolderInvite, InviteStatus, Paths, Recipient, VaultFlags, + VaultId, }; -use sos_database::async_sqlite::Client; use sos_database::entity::{ - AccountEntity, AccountRow, FolderEntity, FolderRecord, FolderRow, + AccountEntity, AccountRow, DeleteSharedFolderOutcome, FolderEntity, + FolderRecord, FolderRow, RecipientEntity, }; +use sos_database::{async_sqlite::Client, entity::SharedFolderEntity}; use sos_reducers::{DeviceReducer, FolderReducer}; use sos_sync::{CreateSet, StorageEventLogs}; use sos_vault::{EncryptedEntry, Summary, Vault}; @@ -28,7 +30,7 @@ use std::{ collections::{HashMap, HashSet}, sync::Arc, }; -use tokio::sync::RwLock; +use tokio::sync::{Mutex, RwLock}; #[cfg(feature = "files")] use sos_backend::FileEventLog; @@ -36,6 +38,10 @@ use sos_backend::FileEventLog; #[cfg(feature = "audit")] use {sos_audit::AuditEvent, sos_backend::audit::append_audit_events}; +/// Storage for shared folder event logs. +pub type SharedFolderEvents = + Arc>>>>; + /// Server folders loaded into memory and mirrored to the database. pub struct ServerDatabaseStorage { /// Account identifier. @@ -71,6 +77,9 @@ pub struct ServerDatabaseStorage { /// Reduced collection of devices. pub(super) devices: IndexSet, + + /// Shared folder events. + pub(super) shared_folder_events: SharedFolderEvents, } impl ServerDatabaseStorage { @@ -81,6 +90,7 @@ impl ServerDatabaseStorage { mut target: BackendTarget, account_id: &AccountId, identity_log: Arc>, + shared_folder_events: SharedFolderEvents, ) -> Result { let (paths, client, account_row) = { let BackendTarget::Database(paths, client) = &mut target else { @@ -130,6 +140,7 @@ impl ServerDatabaseStorage { file_log: Arc::new(RwLock::new(file_log)), folders: Default::default(), devices, + shared_folder_events, }; storage.load_folders().await?; @@ -152,25 +163,58 @@ impl ServerDatabaseStorage { } /// Create new event log cache entries. - async fn create_folder_entry(&mut self, id: &VaultId) -> Result<()> { - let mut event_log = FolderEventLog::new_folder( - self.target.clone(), - &self.account_id, - id, - ) - .await?; - event_log.load_tree().await?; - self.folders.insert(*id, Arc::new(RwLock::new(event_log))); + async fn create_folder_entry(&mut self, folder: &Summary) -> Result<()> { + #[inline(always)] + async fn initialize_folder_event_log( + target: BackendTarget, + account_id: &AccountId, + folder_id: &VaultId, + ) -> Result { + let mut event_log = + FolderEventLog::new_folder(target, account_id, folder_id) + .await?; + event_log.load_tree().await?; + Ok(event_log) + } + + if folder.flags().is_shared() { + let mut shared_events = self.shared_folder_events.lock().await; + if let Some(shared_folder) = shared_events.get(folder.id()) { + self.folders.insert(*folder.id(), shared_folder.clone()); + } else { + let event_log = initialize_folder_event_log( + self.target.clone(), + &self.account_id, + folder.id(), + ) + .await?; + let folder_event_log = Arc::new(RwLock::new(event_log)); + let folder_event_ref = folder_event_log.clone(); + shared_events.insert(*folder.id(), folder_event_log); + self.folders.insert(*folder.id(), folder_event_ref); + } + } else { + let event_log = initialize_folder_event_log( + self.target.clone(), + &self.account_id, + folder.id(), + ) + .await?; + self.folders + .insert(*folder.id(), Arc::new(RwLock::new(event_log))); + } + Ok(()) } /// Remove a folder. async fn remove_vault_file(&self, folder_id: &VaultId) -> Result<()> { + let account_row_id = self.account_row_id; let folder_id = *folder_id; self.client .conn(move |conn| { let folder = FolderEntity::new(&conn); - folder.delete_folder(&folder_id) + folder.delete_folder(account_row_id, &folder_id) }) .await .map_err(sos_database::Error::from)?; @@ -240,6 +284,39 @@ impl ServerDatabaseStorage { .await .map_err(sos_database::Error::from)?) } + + async fn list_folder_invites( + &mut self, + sent: bool, + invite_status: Option, + limit: Option, + ) -> Result> { + let account_id = self.account_id; + let records = self + .client + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + if sent { + entity.sent_folder_invites( + &account_id, + invite_status, + limit, + ) + } else { + entity.received_folder_invites( + &account_id, + invite_status, + limit, + ) + } + }) + .await?; + let mut invites = Vec::with_capacity(records.len()); + for record in records { + invites.push(record.try_into()?); + } + Ok(invites) + } } #[async_trait] @@ -459,7 +536,7 @@ impl ServerAccountStorage for ServerDatabaseStorage { for summary in &folders { // Ensure we don't overwrite existing data if !self.folders.contains_key(summary.id()) { - self.create_folder_entry(summary.id()).await?; + self.create_folder_entry(summary).await?; } } @@ -487,7 +564,7 @@ impl ServerAccountStorage for ServerDatabaseStorage { ) .await?; - self.create_folder_entry(id).await?; + self.create_folder_entry(vault.summary()).await?; { let event_log = self.folders.get_mut(id).unwrap(); @@ -518,8 +595,28 @@ impl ServerAccountStorage for ServerDatabaseStorage { } async fn delete_folder(&mut self, id: &VaultId) -> Result<()> { - // Remove from the database - self.remove_vault_file(id).await?; + use sos_database::{ + async_sqlite::{self, rusqlite}, + Error as DbError, + }; + + // Remove from the database. + // + // With the introduction of shared folders it is now possible + // that the folder does not exist for an account but we still + // need to clean up in-memory data for shared folders so we + // allow QueryReturnedNoRows to allow this. + match self.remove_vault_file(id).await { + Err(Error::Database(DbError::AsyncSqlite( + async_sqlite::Error::Rusqlite( + rusqlite::Error::QueryReturnedNoRows, + ), + ))) => {} + Err(e) => { + return Err(e); + } + Ok(_) => {} + } // Remove local state self.folders.remove(id); @@ -582,6 +679,141 @@ impl ServerAccountStorage for ServerDatabaseStorage { Ok(()) } + + async fn set_recipient(&mut self, recipient: Recipient) -> Result<()> { + let account_id = self.account_id; + self.client + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + entity.upsert_recipient(account_id, recipient) + }) + .await?; + Ok(()) + } + + async fn get_recipient(&mut self) -> Result> { + let account_id = self.account_id; + let record = self + .client + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + entity.find_recipient(account_id) + }) + .await?; + Ok(match record { + Some(record) => Some(record.try_into()?), + None => None, + }) + } + + async fn create_shared_folder( + &mut self, + buffer: &[u8], + recipients: &[Recipient], + ) -> Result<()> { + let account_id = self.account_id; + let vault: Vault = decode(buffer).await?; + SharedFolderEntity::create_shared_folder( + &self.client, + &account_id, + &vault, + recipients, + ) + .await?; + + Ok(()) + } + + async fn sent_folder_invites( + &mut self, + invite_status: Option, + limit: Option, + ) -> Result> { + self.list_folder_invites(true, invite_status, limit).await + } + + async fn received_folder_invites( + &mut self, + invite_status: Option, + limit: Option, + ) -> Result> { + self.list_folder_invites(false, invite_status, limit).await + } + + async fn update_folder_invite( + &mut self, + invite_status: InviteStatus, + from_public_key: String, + folder_id: VaultId, + ) -> Result<()> { + let account_id = self.account_id; + self.client + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + entity.update_folder_invite( + &account_id, + invite_status, + &from_public_key, + &folder_id, + ) + }) + .await?; + + // Must reload the folders when an invite + // is accepted so clients can immediately fetch + // the folder events + if let InviteStatus::Accepted = invite_status { + self.load_folders().await?; + } + + Ok(()) + } + + async fn search_recipients( + &mut self, + query: String, + limit: Option, + ) -> Result> { + let records = self + .client + .conn_and_then(move |conn| { + let mut entity = RecipientEntity::new(&conn); + entity.search_recipients(&query, limit) + }) + .await?; + + let mut recipients = Vec::with_capacity(records.len()); + for record in records { + recipients.push(record.try_into()?); + } + + Ok(recipients) + } + + async fn delete_shared_folder( + &mut self, + folder_id: &VaultId, + ) -> Result { + let account_id = self.account_id; + let outcome = SharedFolderEntity::delete_shared_folder( + &self.client, + &account_id, + folder_id, + ) + .await?; + + // Delete in-memory folder event logs for the caller + self.delete_folder(folder_id).await?; + + // Clean up shared folder event log when actually removing + // the folder data + if outcome.is_creator { + let mut shared_folders = self.shared_folder_events.lock().await; + shared_folders.remove(folder_id); + } + + Ok(outcome) + } } #[async_trait] diff --git a/crates/storage/server/src/filesystem.rs b/crates/storage/server/src/filesystem.rs index ac59cdd0cb..5eb81d3476 100644 --- a/crates/storage/server/src/filesystem.rs +++ b/crates/storage/server/src/filesystem.rs @@ -15,8 +15,10 @@ use sos_core::{ patch::{FolderDiff, FolderPatch}, AccountEvent, EventLog, }, - AccountId, Paths, VaultFlags, VaultId, + AccountId, FolderInvite, InviteStatus, Paths, Recipient, VaultFlags, + VaultId, }; +use sos_database::entity::DeleteSharedFolderOutcome; use sos_reducers::{DeviceReducer, FolderReducer}; use sos_sync::{CreateSet, StorageEventLogs}; use sos_vault::{EncryptedEntry, Header, Summary, Vault}; @@ -466,6 +468,62 @@ impl ServerAccountStorage for ServerFileStorage { vfs::remove_file(&identity_event).await?; Ok(()) } + + async fn set_recipient(&mut self, _recipient: Recipient) -> Result<()> { + unimplemented!(); + } + + async fn get_recipient(&mut self) -> Result> { + unimplemented!(); + } + + async fn create_shared_folder( + &mut self, + _vault: &[u8], + _recipients: &[Recipient], + ) -> Result<()> { + unimplemented!(); + } + + async fn sent_folder_invites( + &mut self, + _invite_status: Option, + _limit: Option, + ) -> Result> { + unimplemented!(); + } + + async fn received_folder_invites( + &mut self, + _invite_status: Option, + _limit: Option, + ) -> Result> { + unimplemented!(); + } + + async fn update_folder_invite( + &mut self, + _invite_status: InviteStatus, + _from_public_key: String, + _folder_id: VaultId, + ) -> Result<()> { + unimplemented!(); + } + + async fn search_recipients( + &mut self, + _query: String, + _limit: Option, + ) -> Result> { + unimplemented!(); + } + + async fn delete_shared_folder( + &mut self, + _folder_id: &VaultId, + ) -> Result { + unimplemented!(); + } } #[async_trait] diff --git a/crates/storage/server/src/lib.rs b/crates/storage/server/src/lib.rs index 5d2974ef1f..9c4345dbcb 100644 --- a/crates/storage/server/src/lib.rs +++ b/crates/storage/server/src/lib.rs @@ -1,9 +1,9 @@ +//! Server storage for a backend target. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #![allow(clippy::large_enum_variant)] -//! Server storage for a backend target. mod error; mod database; @@ -13,6 +13,7 @@ mod storage; mod sync; mod traits; +pub use database::SharedFolderEvents; pub use error::Error; pub use storage::ServerStorage; pub use traits::ServerAccountStorage; diff --git a/crates/storage/server/src/storage.rs b/crates/storage/server/src/storage.rs index 3e13b3f369..4d38cb5ed2 100644 --- a/crates/storage/server/src/storage.rs +++ b/crates/storage/server/src/storage.rs @@ -1,6 +1,7 @@ use crate::{ - database::ServerDatabaseStorage, filesystem::ServerFileStorage, Error, - Result, ServerAccountStorage, + database::{ServerDatabaseStorage, SharedFolderEvents}, + filesystem::ServerFileStorage, + Error, Result, ServerAccountStorage, }; use async_trait::async_trait; use indexmap::IndexSet; @@ -14,9 +15,10 @@ use sos_core::{ patch::{AccountDiff, CheckedPatch, DeviceDiff, FolderDiff}, EventLog, WriteEvent, }, - AccountId, Paths, VaultFlags, VaultId, + AccountId, FolderInvite, InviteStatus, Paths, Recipient, VaultFlags, + VaultId, }; -use sos_database::entity::AccountEntity; +use sos_database::entity::{AccountEntity, DeleteSharedFolderOutcome}; use sos_sync::{ CreateSet, ForceMerge, Merge, MergeOutcome, StorageEventLogs, SyncStatus, SyncStorage, @@ -46,6 +48,7 @@ impl ServerStorage { pub async fn new( target: BackendTarget, account_id: &AccountId, + shared_folder_events: SharedFolderEvents, ) -> Result { match target { BackendTarget::FileSystem(paths) => { @@ -61,6 +64,7 @@ impl ServerStorage { Self::new_db( BackendTarget::Database(paths, client), account_id, + shared_folder_events, ) .await } @@ -96,6 +100,7 @@ impl ServerStorage { target: BackendTarget, account_id: &AccountId, account_data: &CreateSet, + shared_folder_events: SharedFolderEvents, ) -> Result { match target { BackendTarget::FileSystem(paths) => { @@ -111,6 +116,7 @@ impl ServerStorage { BackendTarget::Database(paths, client), account_id, account_data, + shared_folder_events, ) .await } @@ -156,6 +162,7 @@ impl ServerStorage { async fn new_db( target: BackendTarget, account_id: &AccountId, + shared_folder_events: SharedFolderEvents, ) -> Result { debug_assert!(matches!(target, BackendTarget::Database(_, _))); let BackendTarget::Database(paths, client) = &target else { @@ -181,6 +188,7 @@ impl ServerStorage { target, account_id, Arc::new(RwLock::new(event_log)), + shared_folder_events, ) .await?, ))) @@ -191,6 +199,7 @@ impl ServerStorage { target: BackendTarget, account_id: &AccountId, account_data: &CreateSet, + shared_folder_events: SharedFolderEvents, ) -> Result { let BackendTarget::Database(paths, _) = &target else { panic!("database backend expected"); @@ -209,6 +218,7 @@ impl ServerStorage { target, account_id, Arc::new(RwLock::new(identity_log)), + shared_folder_events, ) .await?; storage.import_account(account_data).await?; @@ -390,6 +400,122 @@ impl ServerAccountStorage for ServerStorage { ServerStorage::Database(db) => db.delete_account().await, } } + + async fn set_recipient(&mut self, recipient: Recipient) -> Result<()> { + match self { + ServerStorage::FileSystem(fs) => { + fs.set_recipient(recipient).await + } + ServerStorage::Database(db) => db.set_recipient(recipient).await, + } + } + + async fn get_recipient(&mut self) -> Result> { + match self { + ServerStorage::FileSystem(fs) => fs.get_recipient().await, + ServerStorage::Database(db) => db.get_recipient().await, + } + } + + async fn create_shared_folder( + &mut self, + vault: &[u8], + recipients: &[Recipient], + ) -> Result<()> { + match self { + ServerStorage::FileSystem(fs) => { + fs.create_shared_folder(vault, recipients).await + } + ServerStorage::Database(db) => { + db.create_shared_folder(vault, recipients).await + } + } + } + + async fn sent_folder_invites( + &mut self, + invite_status: Option, + limit: Option, + ) -> Result> { + match self { + ServerStorage::FileSystem(fs) => { + fs.sent_folder_invites(invite_status, limit).await + } + ServerStorage::Database(db) => { + db.sent_folder_invites(invite_status, limit).await + } + } + } + + async fn received_folder_invites( + &mut self, + invite_status: Option, + limit: Option, + ) -> Result> { + match self { + ServerStorage::FileSystem(fs) => { + fs.received_folder_invites(invite_status, limit).await + } + ServerStorage::Database(db) => { + db.received_folder_invites(invite_status, limit).await + } + } + } + + async fn update_folder_invite( + &mut self, + invite_status: InviteStatus, + from_public_key: String, + folder_id: VaultId, + ) -> Result<()> { + match self { + ServerStorage::FileSystem(fs) => { + fs.update_folder_invite( + invite_status, + from_public_key, + folder_id, + ) + .await + } + ServerStorage::Database(db) => { + db.update_folder_invite( + invite_status, + from_public_key, + folder_id, + ) + .await + } + } + } + + async fn search_recipients( + &mut self, + query: String, + limit: Option, + ) -> Result> { + match self { + ServerStorage::FileSystem(fs) => { + fs.search_recipients(query, limit).await + } + ServerStorage::Database(db) => { + db.search_recipients(query, limit).await + } + } + } + + async fn delete_shared_folder( + &mut self, + folder_id: &VaultId, + ) -> Result { + match self { + ServerStorage::FileSystem(fs) => { + fs.delete_shared_folder(folder_id).await + } + ServerStorage::Database(db) => { + db.delete_shared_folder(folder_id).await + } + } + } } #[async_trait] diff --git a/crates/storage/server/src/traits.rs b/crates/storage/server/src/traits.rs index 94647d7754..ba8ced03d4 100644 --- a/crates/storage/server/src/traits.rs +++ b/crates/storage/server/src/traits.rs @@ -6,8 +6,10 @@ use sos_backend::FolderEventLog; use sos_core::{ device::{DevicePublicKey, TrustedDevice}, events::patch::FolderDiff, - AccountId, Paths, VaultFlags, VaultId, + AccountId, FolderInvite, InviteStatus, Paths, Recipient, VaultFlags, + VaultId, }; +use sos_database::entity::DeleteSharedFolderOutcome; use sos_sync::CreateSet; use sos_vault::{Summary, Vault}; use std::collections::{HashMap, HashSet}; @@ -107,4 +109,52 @@ pub trait ServerAccountStorage { /// Delete this account. async fn delete_account(&mut self) -> Result<()>; + + /// Set account recipient information. + async fn set_recipient(&mut self, recipient: Recipient) -> Result<()>; + + /// Get account recipient information. + async fn get_recipient(&mut self) -> Result>; + + /// Create a shared folder. + async fn create_shared_folder( + &mut self, + vault: &[u8], + recipients: &[Recipient], + ) -> Result<()>; + + /// List sent folder invites for this account. + async fn sent_folder_invites( + &mut self, + invite_status: Option, + limit: Option, + ) -> Result>; + + /// List received folder invites for this account. + async fn received_folder_invites( + &mut self, + invite_status: Option, + limit: Option, + ) -> Result>; + + /// Update a folder invite for this account. + async fn update_folder_invite( + &mut self, + invite_status: InviteStatus, + from_public_key: String, + folder_id: VaultId, + ) -> Result<()>; + + /// Search for recipients. + async fn search_recipients( + &mut self, + query: String, + limit: Option, + ) -> Result>; + + /// Delete a shared folder. + async fn delete_shared_folder( + &mut self, + folder_id: &VaultId, + ) -> Result; } diff --git a/crates/sync/Cargo.toml b/crates/sync/Cargo.toml index 5e6ed7987c..62009a4a8e 100644 --- a/crates/sync/Cargo.toml +++ b/crates/sync/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-sync" version = "0.17.3" -edition = "2021" +edition = "2024" description = "Sync types and traits for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -31,6 +31,3 @@ indexmap.workspace = true tokio.workspace = true serde.workspace = true tracing.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/sync/build.rs b/crates/sync/build.rs deleted file mode 100644 index 5976a1c6d5..0000000000 --- a/crates/sync/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel); -} diff --git a/crates/sync/src/lib.rs b/crates/sync/src/lib.rs index 96ebd76f96..5c7d129377 100644 --- a/crates/sync/src/lib.rs +++ b/crates/sync/src/lib.rs @@ -1,11 +1,11 @@ +//! Core types and traits for sync and merge operations; part of the +//! [Save Our Secrets](https://saveoursecrets.com) SDK. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] #![allow(clippy::field_reassign_with_default)] #![allow(clippy::collapsible_match)] -//! Core types and traits for sync and merge operations; part of the -//! [Save Our Secrets](https://saveoursecrets.com) SDK. mod error; mod traits; mod types; diff --git a/crates/sync/src/traits.rs b/crates/sync/src/traits.rs index 77b13cfc4f..7bd583d9f6 100644 --- a/crates/sync/src/traits.rs +++ b/crates/sync/src/traits.rs @@ -7,16 +7,16 @@ use crate::{ use async_trait::async_trait; use indexmap::{IndexMap, IndexSet}; use sos_backend::{AccountEventLog, DeviceEventLog, FolderEventLog}; +use sos_core::AccountId; use sos_core::commit::CommitHash; use sos_core::events::WriteEvent; -use sos_core::AccountId; use sos_core::{ + VaultId, commit::{CommitState, CommitTree, Comparison}, events::{ - patch::{AccountDiff, CheckedPatch, DeviceDiff, FolderDiff}, EventLog, + patch::{AccountDiff, CheckedPatch, DeviceDiff, FolderDiff}, }, - VaultId, }; use sos_vault::Summary; use std::{ @@ -28,7 +28,7 @@ use tokio::sync::RwLock; #[cfg(feature = "files")] use { sos_backend::FileEventLog, - sos_core::{events::patch::FileDiff, ExternalFile}, + sos_core::{ExternalFile, events::patch::FileDiff}, }; macro_rules! debug_tree_events { diff --git a/crates/sync/src/types.rs b/crates/sync/src/types.rs index aa6583b7d9..680f268d45 100644 --- a/crates/sync/src/types.rs +++ b/crates/sync/src/types.rs @@ -3,17 +3,17 @@ use crate::Result; use indexmap::{IndexMap, IndexSet}; use serde::{Deserialize, Serialize}; use sos_core::{ - commit::{CommitHash, CommitState, Comparison}, AccountId, SecretId, VaultId, + commit::{CommitHash, CommitState, Comparison}, }; use sos_core::{ device::DevicePublicKey, events::{ + AccountEvent, DeviceEvent, WriteEvent, patch::{ AccountDiff, AccountPatch, DeviceDiff, DevicePatch, FolderDiff, FolderPatch, }, - AccountEvent, DeviceEvent, WriteEvent, }, }; use sos_vault::Summary; @@ -21,11 +21,11 @@ use std::collections::HashMap; #[cfg(feature = "files")] use sos_core::{ + ExternalFile, ExternalFileName, SecretPath, events::{ - patch::{FileDiff, FilePatch}, FileEvent, + patch::{FileDiff, FilePatch}, }, - ExternalFile, ExternalFileName, SecretPath, }; /// Debug snapshot of an account events at a point in time. @@ -399,14 +399,14 @@ impl TrackedChanges { dest, from, } = event + && moved_name == &name + && dest == &owner { - if moved_name == &name && dest == &owner { - return Some(TrackedFileChange::Moved { - name: *moved_name, - from: *from, - dest: *dest, - }); - } + return Some(TrackedFileChange::Moved { + name: *moved_name, + from: *from, + dest: *dest, + }); } None }); diff --git a/crates/system_messages/Cargo.toml b/crates/system_messages/Cargo.toml index a9ff4ffe9f..44ac194a8b 100644 --- a/crates/system_messages/Cargo.toml +++ b/crates/system_messages/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-system-messages" version = "0.17.4" -edition = "2021" +edition = "2024" description = "System message notifications for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -16,6 +16,3 @@ tokio.workspace = true urn.workspace = true serde_with.workspace = true tracing.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/system_messages/build.rs b/crates/system_messages/build.rs deleted file mode 100644 index 5976a1c6d5..0000000000 --- a/crates/system_messages/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel); -} diff --git a/crates/system_messages/src/lib.rs b/crates/system_messages/src/lib.rs index 1c0499286f..68382aaa31 100644 --- a/crates/system_messages/src/lib.rs +++ b/crates/system_messages/src/lib.rs @@ -1,6 +1,3 @@ -#![deny(missing_docs)] -#![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] //! System messages are persistent user notifications. //! //! They can be used to surface information such as @@ -16,6 +13,10 @@ //! changes to the underlying collection. This allows //! an interface to show the number of unread system //! messages. +#![deny(missing_docs)] +#![forbid(unsafe_code)] +#![cfg_attr(docsrs, feature(doc_cfg))] + mod error; mod system_messages; diff --git a/crates/system_messages/src/system_messages.rs b/crates/system_messages/src/system_messages.rs index 7e07a04ed3..cefcd653ac 100644 --- a/crates/system_messages/src/system_messages.rs +++ b/crates/system_messages/src/system_messages.rs @@ -1,7 +1,7 @@ use crate::Error; use async_trait::async_trait; use serde::{Deserialize, Serialize}; -use serde_with::{serde_as, DisplayFromStr}; +use serde_with::{DisplayFromStr, serde_as}; use std::collections::hash_map::{IntoIter, Iter, IterMut}; use std::{cmp::Ordering, collections::HashMap}; use time::OffsetDateTime; @@ -437,7 +437,9 @@ where // Hotfix for https://github.com/saveoursecrets/sdk/issues/811 mod serde_hotfix { use serde::{Deserialize, Deserializer}; - pub(super) fn bool_or_int<'de, D>(deserializer: D) -> Result + pub(super) fn bool_or_int<'de, D>( + deserializer: D, + ) -> Result where D: Deserializer<'de>, { diff --git a/crates/vault/Cargo.toml b/crates/vault/Cargo.toml index d51e1f59fb..18dfdba308 100644 --- a/crates/vault/Cargo.toml +++ b/crates/vault/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-vault" -version = "0.17.4" -edition = "2021" +version = "0.17.5" +edition = "2024" description = "Vault encrypted storage for the Save Our Secrets SDK" homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -34,6 +34,3 @@ ed25519-dalek.workspace = true tracing.workspace = true serde_json.workspace = true hex.workspace = true - -[build-dependencies] -rustc_version.workspace = true diff --git a/crates/vault/build.rs b/crates/vault/build.rs deleted file mode 100644 index 238c609d89..0000000000 --- a/crates/vault/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel) -} diff --git a/crates/vault/src/access_point.rs b/crates/vault/src/access_point.rs index feaacc2ece..6906ceea2d 100644 --- a/crates/vault/src/access_point.rs +++ b/crates/vault/src/access_point.rs @@ -1,15 +1,15 @@ //! Access point manages access to a vault. use crate::{ - secret::{Secret, SecretMeta, SecretRow}, EncryptedEntry, Error, SharedAccess, Summary, Vault, VaultMeta, + secret::{Secret, SecretMeta, SecretRow}, }; use async_trait::async_trait; use sos_core::{ + AuthenticationError, SecretId, VaultCommit, VaultEntry, VaultFlags, + VaultId, crypto::{AccessKey, AeadPack, KeyDerivation, PrivateKey}, decode, encode, events::{ReadEvent, WriteEvent}, - AuthenticationError, SecretId, VaultCommit, VaultEntry, VaultFlags, - VaultId, }; use sos_vfs as vfs; use std::path::Path; diff --git a/crates/vault/src/builder.rs b/crates/vault/src/builder.rs index 0d003212e4..f1f70cb9bf 100644 --- a/crates/vault/src/builder.rs +++ b/crates/vault/src/builder.rs @@ -2,9 +2,10 @@ use crate::{EncryptedEntry, Result, Vault, VaultMeta}; use age::x25519::{Identity, Recipient}; use secrecy::SecretString; use sos_core::{ + VaultFlags, VaultId, constants::DEFAULT_VAULT_NAME, crypto::{Cipher, KeyDerivation, PrivateKey, Seed}, - encode, VaultFlags, VaultId, + encode, }; /// Credentials for a new vault. diff --git a/crates/vault/src/change_password.rs b/crates/vault/src/change_password.rs index 8396a88a0a..7fd18689e0 100644 --- a/crates/vault/src/change_password.rs +++ b/crates/vault/src/change_password.rs @@ -1,12 +1,12 @@ //! Flow for changing a vault password. use crate::{ - vault::{EncryptedEntry, Vault}, Error, Result, + vault::{EncryptedEntry, Vault}, }; use sos_core::{ + VaultCommit, VaultEntry, crypto::{AccessKey, KeyDerivation, PrivateKey, Seed}, events::WriteEvent, - VaultCommit, VaultEntry, }; /// Builder that changes a vault password. diff --git a/crates/vault/src/encoding/secret.rs b/crates/vault/src/encoding/secret.rs index 2850fb9875..6c03e0c6d4 100644 --- a/crates/vault/src/encoding/secret.rs +++ b/crates/vault/src/encoding/secret.rs @@ -8,8 +8,8 @@ use binary_stream::futures::{ }; use secrecy::ExposeSecret; use sos_core::{ - encoding::{decode_uuid, encoding_error}, UtcDateTime, + encoding::{decode_uuid, encoding_error}, }; use std::{ collections::HashMap, diff --git a/crates/vault/src/encoding/vault.rs b/crates/vault/src/encoding/vault.rs index 9c27cb3f8d..d984660bcc 100644 --- a/crates/vault/src/encoding/vault.rs +++ b/crates/vault/src/encoding/vault.rs @@ -6,11 +6,11 @@ use binary_stream::futures::{ BinaryReader, BinaryWriter, Decodable, Encodable, }; use sos_core::{ + SecretId, UtcDateTime, VaultCommit, VaultFlags, constants::VAULT_IDENTITY, crypto::{AeadPack, Seed}, encoding::{decode_uuid, encoding_error}, file_identity::FileIdentity, - SecretId, UtcDateTime, VaultCommit, VaultFlags, }; use std::io::{Error, Result, SeekFrom}; use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite}; diff --git a/crates/vault/src/lib.rs b/crates/vault/src/lib.rs index 97d718efa6..0524a8b06e 100644 --- a/crates/vault/src/lib.rs +++ b/crates/vault/src/lib.rs @@ -1,7 +1,8 @@ +//! Vault encrypted storage and access. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -//! Vault encrypted storage and access. +#![cfg_attr(docsrs, feature(doc_cfg))] + mod access_point; mod builder; mod change_password; @@ -21,7 +22,7 @@ pub use vault::{ pub(crate) type Result = std::result::Result; pub(crate) use vault::Auth; -use sos_core::{constants::VAULT_EXT, Paths, PublicIdentity}; +use sos_core::{Paths, PublicIdentity, constants::VAULT_EXT}; use sos_vfs as vfs; use std::{ path::{Path, PathBuf}, @@ -86,11 +87,11 @@ pub async fn list_local_folders( let mut vaults = Vec::new(); let mut dir = vfs::read_dir(vaults_dir).await?; while let Some(entry) = dir.next_entry().await? { - if let Some(extension) = entry.path().extension() { - if extension == VAULT_EXT { - let summary = Header::read_summary_file(entry.path()).await?; - vaults.push((summary, entry.path().to_path_buf())); - } + if let Some(extension) = entry.path().extension() + && extension == VAULT_EXT + { + let summary = Header::read_summary_file(entry.path()).await?; + vaults.push((summary, entry.path().to_path_buf())); } } Ok(vaults) diff --git a/crates/vault/src/secret.rs b/crates/vault/src/secret.rs index 7931c6095d..c58f8c62d3 100644 --- a/crates/vault/src/secret.rs +++ b/crates/vault/src/secret.rs @@ -5,10 +5,10 @@ use ed25519_dalek::SECRET_KEY_LENGTH; use pem::Pem; use secrecy::{ExposeSecret, SecretBox, SecretString}; use serde::{ - ser::{SerializeMap, SerializeSeq}, Deserialize, Serialize, Serializer, + ser::{SerializeMap, SerializeSeq}, }; -use sos_core::{basename, guess_mime, UtcDateTime}; +use sos_core::{UtcDateTime, basename, guess_mime}; use sos_signer::{ ecdsa::{self, BoxedEcdsaSigner}, ed25519::{self, BoxedEd25519Signer}, @@ -65,7 +65,7 @@ where S: Serializer, { match secret { - Some(ref value) => ser.serialize_some(value.expose_secret()), + Some(value) => ser.serialize_some(value.expose_secret()), None => ser.serialize_none(), } } diff --git a/crates/vault/src/vault.rs b/crates/vault/src/vault.rs index e7c0240380..091af5c32c 100644 --- a/crates/vault/src/vault.rs +++ b/crates/vault/src/vault.rs @@ -7,17 +7,17 @@ use secrecy::SecretString; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use sos_core::{ + AuthenticationError, SecretId, UtcDateTime, VaultCommit, VaultEntry, + VaultFlags, VaultId, commit::CommitHash, constants::{DEFAULT_VAULT_NAME, URN_NID, VAULT_IDENTITY, VAULT_NSS}, crypto::{ AccessKey, AeadPack, Cipher, Deriver, KeyDerivation, PrivateKey, Seed, }, decode, encode, - encoding::{encoding_options, VERSION}, + encoding::{VERSION, encoding_options}, events::{ReadEvent, WriteEvent}, file_identity::FileIdentity, - AuthenticationError, SecretId, UtcDateTime, VaultCommit, VaultEntry, - VaultFlags, VaultId, }; use sos_vfs::File; use std::io::Cursor; @@ -399,6 +399,11 @@ impl Header { self.auth.seed = seed; } + /// Set shared access permissions. + pub fn set_shared_access(&mut self, shared_access: SharedAccess) { + self.shared_access = shared_access; + } + /// Read the content offset for a vault file verifying /// the identity bytes first. pub async fn read_content_offset>(path: P) -> Result { @@ -519,7 +524,8 @@ impl SharedAccess { } } - fn parse_recipients(access: &Vec) -> Result> { + /// Parse recipeients list. + pub fn parse_recipients(access: &Vec) -> Result> { let mut recipients = Vec::new(); for recipient in access { let recipient = recipient.parse().map_err(|s: &str| { @@ -918,7 +924,7 @@ impl Vault { self.contents .data .iter() - .map(|(k, v)| (k, &v.1 .0)) + .map(|(k, v)| (k, &v.1.0)) .collect::>() } diff --git a/crates/vfs/Cargo.toml b/crates/vfs/Cargo.toml index f350cbf7cd..6ef632cbd3 100644 --- a/crates/vfs/Cargo.toml +++ b/crates/vfs/Cargo.toml @@ -1,12 +1,16 @@ [package] name = "sos-vfs" version = "0.3.2" -edition = "2021" +edition = "2024" description = "Virtual file system same as tokio::fs." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" repository = "https://github.com/saveoursecrets/sdk" +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] + [features] mem-fs = [] diff --git a/crates/vfs/src/lib.rs b/crates/vfs/src/lib.rs index 3b12052041..2a70d9d6ab 100644 --- a/crates/vfs/src/lib.rs +++ b/crates/vfs/src/lib.rs @@ -1,4 +1,3 @@ -#![allow(clippy::len_without_is_empty)] //! Virtual file system. //! //! The API is designed to match the `tokio::fs` module which in @@ -43,6 +42,8 @@ //! `symlink()`, `symlink_metadata()`, `symlink_file()` and //! `symlink_dir()` functions are not available. //! +#![cfg_attr(docsrs, feature(doc_cfg))] +#![allow(clippy::len_without_is_empty)] #[cfg(any( feature = "mem-fs", diff --git a/crates/vfs/src/memory/dir_builder.rs b/crates/vfs/src/memory/dir_builder.rs index d02042d620..7f5c179b9b 100644 --- a/crates/vfs/src/memory/dir_builder.rs +++ b/crates/vfs/src/memory/dir_builder.rs @@ -6,8 +6,8 @@ use std::{ }; use super::fs::{ - has_parent, resolve, resolve_parent, root_fs, MemoryFd, Parent, - PathTarget, + MemoryFd, Parent, PathTarget, has_parent, resolve, resolve_parent, + root_fs, }; /// A builder for creating directories in various manners. @@ -86,12 +86,14 @@ impl DirBuilder { return Err( ErrorKind::PermissionDenied .into(), - ) + ); } } } PathTarget::Root(_) => { - return Err(ErrorKind::PermissionDenied.into()) + return Err( + ErrorKind::PermissionDenied.into() + ); } } } else { diff --git a/crates/vfs/src/memory/file.rs b/crates/vfs/src/memory/file.rs index 1b12b04ab8..4d9ec67f28 100644 --- a/crates/vfs/src/memory/file.rs +++ b/crates/vfs/src/memory/file.rs @@ -14,7 +14,7 @@ use tokio::{ use std::cmp; use std::fmt; -use std::io::{self, prelude::*, ErrorKind, Seek, SeekFrom}; +use std::io::{self, ErrorKind, Seek, SeekFrom, prelude::*}; use std::path::{Path, PathBuf}; use std::pin::Pin; use std::task::Context; @@ -22,10 +22,10 @@ use std::task::Poll; use std::task::Poll::*; use super::{ + Metadata, OpenOptions, Permissions, fs::{Fd, FileContent, MemoryFd}, metadata, open_options::OpenFlags, - Metadata, OpenOptions, Permissions, }; /// A reference to an open file on the filesystem. diff --git a/crates/vfs/src/memory/mod.rs b/crates/vfs/src/memory/mod.rs index 70582be205..786d1ed09d 100644 --- a/crates/vfs/src/memory/mod.rs +++ b/crates/vfs/src/memory/mod.rs @@ -10,7 +10,7 @@ mod meta_data; mod open_options; mod read_dir; -pub use dir_builder::{create_dir, create_dir_all, DirBuilder}; +pub use dir_builder::{DirBuilder, create_dir, create_dir_all}; pub use file::File; pub use fs::{ canonicalize, copy, metadata, read, read_to_string, remove_dir, @@ -18,4 +18,4 @@ pub use fs::{ }; pub use meta_data::{FileType, Metadata, Permissions}; pub use open_options::OpenOptions; -pub use read_dir::{read_dir, DirEntry, ReadDir}; +pub use read_dir::{DirEntry, ReadDir, read_dir}; diff --git a/crates/vfs/src/memory/open_options.rs b/crates/vfs/src/memory/open_options.rs index d1dee09f49..22e96e8c13 100644 --- a/crates/vfs/src/memory/open_options.rs +++ b/crates/vfs/src/memory/open_options.rs @@ -2,8 +2,8 @@ use std::io::{ErrorKind, Result}; use std::{path::Path, sync::Arc}; use super::{ - fs::{create_file, resolve, MemoryFd, PathTarget}, File, + fs::{MemoryFd, PathTarget, create_file, resolve}, }; use bitflags::bitflags; diff --git a/crates/vfs/src/memory/read_dir.rs b/crates/vfs/src/memory/read_dir.rs index 6e47d2a485..dd6d65b1d6 100644 --- a/crates/vfs/src/memory/read_dir.rs +++ b/crates/vfs/src/memory/read_dir.rs @@ -5,8 +5,9 @@ use std::task::Context; use std::task::Poll; use super::{ - fs::{resolve, Fd, MemoryFd, PathTarget}, - metadata, FileType, Metadata, + FileType, Metadata, + fs::{Fd, MemoryFd, PathTarget, resolve}, + metadata, }; /// Returns a stream over the entries within a directory. diff --git a/crates/vfs/src/tests.rs b/crates/vfs/src/tests.rs index 91fc369276..4f8e8f6a6f 100644 --- a/crates/vfs/src/tests.rs +++ b/crates/vfs/src/tests.rs @@ -4,7 +4,7 @@ mod tests { use std::ffi::OsString; use std::io::SeekFrom; - use std::path::{PathBuf, MAIN_SEPARATOR}; + use std::path::{MAIN_SEPARATOR, PathBuf}; use crate::memory::{self as vfs, File, OpenOptions, Permissions}; use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; diff --git a/crates/web/Cargo.toml b/crates/web/Cargo.toml index 48c76506de..cc27bf08a9 100644 --- a/crates/web/Cargo.toml +++ b/crates/web/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "sos-web" version = "0.17.2" -edition = "2021" +edition = "2024" description = "Thin client for webassembly bindings to the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" license = "MIT OR Apache-2.0" @@ -52,7 +52,4 @@ indexmap.workspace = true async-trait.workspace = true tokio.workspace = true secrecy.workspace = true - -[build-dependencies] -rustc_version.workspace = true - +age.workspace = true diff --git a/crates/web/build.rs b/crates/web/build.rs deleted file mode 100644 index 5976a1c6d5..0000000000 --- a/crates/web/build.rs +++ /dev/null @@ -1,14 +0,0 @@ -use rustc_version::{version_meta, Channel}; - -fn main() { - println!("cargo::rustc-check-cfg=cfg(CHANNEL_NIGHTLY)"); - - // Set cfg flags depending on release channel - let channel = match version_meta().unwrap().channel { - Channel::Stable => "CHANNEL_STABLE", - Channel::Beta => "CHANNEL_BETA", - Channel::Nightly => "CHANNEL_NIGHTLY", - Channel::Dev => "CHANNEL_DEV", - }; - println!("cargo:rustc-cfg={}", channel); -} diff --git a/crates/web/src/lib.rs b/crates/web/src/lib.rs index f5feee7436..c2c196a666 100644 --- a/crates/web/src/lib.rs +++ b/crates/web/src/lib.rs @@ -1,7 +1,7 @@ +//! Web accounts for the [Save Our Secrets SDK](https://saveoursecrets.com/) intended to be used in webassembly bindings. #![deny(missing_docs)] #![forbid(unsafe_code)] -#![cfg_attr(all(doc, CHANNEL_NIGHTLY), feature(doc_auto_cfg))] -//! Web accounts for the [Save Our Secrets SDK](https://saveoursecrets.com/) intended to be used in webassembly bindings. +#![cfg_attr(docsrs, feature(doc_cfg))] use sos_account::{Account, AccountSwitcher}; diff --git a/crates/web/src/linked_account.rs b/crates/web/src/linked_account.rs index e7cb494aca..95d3a20011 100644 --- a/crates/web/src/linked_account.rs +++ b/crates/web/src/linked_account.rs @@ -13,26 +13,26 @@ use sos_backend::{ }; use sos_client_storage::{AccessOptions, NewFolderOptions}; use sos_core::{ + AccountId, Origin, SecretId, VaultId, commit::{CommitHash, CommitState, Comparison}, events::{ - patch::{AccountDiff, CheckedPatch, DeviceDiff, FolderDiff}, WriteEvent, + patch::{AccountDiff, CheckedPatch, DeviceDiff, FolderDiff}, }, - AccountId, Origin, SecretId, VaultId, }; use sos_core::{ + FolderRef, Paths, PublicIdentity, UtcDateTime, VaultCommit, VaultFlags, crypto::{AccessKey, Cipher, KeyDerivation}, device::{DevicePublicKey, TrustedDevice}, events::{AccountEvent, DeviceEvent, EventRecord, ReadEvent}, - FolderRef, Paths, PublicIdentity, UtcDateTime, VaultCommit, VaultFlags, }; use sos_login::{ - device::{DeviceManager, DeviceSigner}, DelegatedAccess, + device::{DeviceManager, DeviceSigner}, }; use sos_protocol::{ - network_client::HttpClient, RemoteResult, RemoteSync, SyncClient, - SyncOptions, + RemoteResult, RemoteSync, SyncClient, SyncOptions, + network_client::HttpClient, }; use sos_remote_sync::{AutoMerge, RemoteSyncHandler}; use sos_sync::{ @@ -40,8 +40,8 @@ use sos_sync::{ SyncDirection, SyncStatus, SyncStorage, UpdateSet, }; use sos_vault::{ - secret::{Secret, SecretMeta, SecretRow, SecretType}, Summary, Vault, + secret::{Secret, SecretMeta, SecretRow, SecretType}, }; use sos_vfs as vfs; use std::{ @@ -53,7 +53,7 @@ use tokio::sync::{Mutex, RwLock}; #[cfg(feature = "clipboard")] use { - sos_account::{xclipboard::Clipboard, ClipboardCopyRequest}, + sos_account::{ClipboardCopyRequest, xclipboard::Clipboard}, sos_core::SecretPath, }; @@ -153,6 +153,13 @@ impl Account for LinkedAccount { account.is_authenticated().await } + async fn shared_access_public_key( + &self, + ) -> Result { + let account = self.account.lock().await; + Ok(account.shared_access_public_key().await?) + } + async fn import_account_events( &mut self, events: CreateSet, diff --git a/doc/developer/test.md b/doc/developer/test.md index 6cf562f8f8..7d0c8004a0 100644 --- a/doc/developer/test.md +++ b/doc/developer/test.md @@ -30,10 +30,10 @@ To run just the command line tests which would be included in test coverage: cargo make test-command-line ``` -Or to test with the database backend: +Or to test with the file system backend: ``` -SOS_TEST_CLIENT_DB=1 cargo make test-command-line +SOS_TEST_CLIENT_FS=1 cargo make test-command-line ``` Run with `ANTICIPATE_ECHO` to debug: @@ -50,14 +50,14 @@ To run all the CLI test specs (including for the `shell` command) using the vers ``` cargo make test-cli -SOS_TEST_CLIENT_DB=1 cargo make test-cli +SOS_TEST_CLIENT_FS=1 cargo make test-cli ``` Or to just test the shell command: ``` cargo make test-shell -SOS_TEST_CLIENT_DB=1 cargo make test-shell +SOS_TEST_CLIENT_FS=1 cargo make test-shell ``` The shell tests complete much faster as they don't need to launch an executable for each command. diff --git a/doc/shared-folders.md b/doc/shared-folders.md new file mode 100644 index 0000000000..a809fad409 --- /dev/null +++ b/doc/shared-folders.md @@ -0,0 +1,38 @@ +# Shared Folders + +Shared folders use asymmetric encryption to protect secrets that can be accessed across multiple accounts. + +The implementation uses X25519 public key encryption to multiple recipients using [AGE V1](https://github.com/C2SP/C2SP/blob/main/age.md) provided by the [AGE library](https://docs.rs/age/latest/age/). + +In order for people to discover participants for shared folders we implement simple public key infrastructure (PKI) using the existing tenant-specific SQLite database (see the [migration](/crates/database/sql_migrations/V3__shared_folders.sql) for the relevant tables). + +A shared folder implies network connectivity so the functions for shared folders are exposed on [NetworkAccount](/crates/net/src/account/network_account.rs) with the exception of the helper function `prepare_shared_folder()` which was added to [LocalAccount](/creates/accunts/src/local_account.rs) to create the vault with the correct cipher and flags. + +## Terminology + +* Recipient: an account that has enabled sharing by configuring a name (and possibly email) visible to other accounts. +* Participant: a recipient that is cryptographically participating in a shared folder. +* Owner: the creator of a shared folder. + +## Recipients, participants and discovery + +We extend on the existing X25519 [Recipient](https://docs.rs/age/latest/age/x25519/struct.Recipient.html) notion to provide a name and optional email associated with an account in the SQLite database. + +The server then provides endpoints for accounts to create and update their own recipient information which will enable folder sharing for an account. Participants may be discovered by searching for other recipients by name (or email). + +Once a recipient has been discovered the owner creating (or updating the recipients) for a shared folder can send a folder invite; when a recipient accepts a folder invite they will then have access to the shared folder which will be treated like any other folder except that it will use asymmetric encryption. + +## Updating recipients, re-encryption and sync + +When the owner of a shared folder wants to add or remove a recipient it will require re-encrypting all the secrets in the folder to cryptographically allow or disallow access to the secrets in the folder. + +These changes can be sent as a series of update events which will make non-destructive changes to the event log and allow the usual sync algorithm to apply changes for all recipients. + +However, when a recipient is removed by the owner we need to consider that fact that a removed recipient may be able to access the existing secrets using the time travel feature, for example, by accessing secrets defined in earlier versions of the event log. Therefore, we should recommend or force that when removing a recipient the event log is compacted to remove access to earlier versions of the encrypted secrets therefore denying access to the removed recipient. This will generate a hard conflict for other recipients that still have access to the folder and they would need to force pull the new version of the shared folder. + +It is worth noting though that removing a recipient from a shared folder, whilst it may (assuming compaction and sync) cryptographically deny the person access to the secrets the shared folder owner needs to consider all the secrets in the folder as compromised (as the removed recipient may have already copied the secrets elsewhere) so all the secrets in the shared folder should be rotated by the owner to truly deny access to the removed recipient and protect the shared secrets. + +## Server storage + +In order to facilitate shared folders on the server with minimal changes to the sync algorithm the server storage library now manages a collection of `shared_folders` and the folder event logs for each account refer to the shared folder event log using atomic reference counting (`Arc`). For the server to detect shared folders it is essential that the `VaultFlags::SHARED` bit is set correctly which the library code enforces in `prepare_shared_folder()`. + diff --git a/tests/command_line/runner/shell.sh b/tests/command_line/runner/shell.sh index 486057417c..cb5078b436 100755 --- a/tests/command_line/runner/shell.sh +++ b/tests/command_line/runner/shell.sh @@ -8,15 +8,15 @@ export PATH="target/debug:$PATH" command -v sos -if [ -n "$SOS_TEST_CLIENT_DB" ]; then - # env has already exported SOS_DATA_DIR so this - # will create the database file and run migrations - sos tool db migrate +if [ -z "$SOS_TEST_CLIENT_FS" ]; then + # env has already exported SOS_DATA_DIR so this + # will create the database file and run migrations + sos tool db migrate fi anticipate \ - run \ - --setup tests/command_line/scripts/setup.sh \ - --teardown tests/command_line/scripts/teardown.sh \ - --timeout 15000 \ - tests/command_line/scripts/specs/shell.sh + run \ + --setup tests/command_line/scripts/setup.sh \ + --teardown tests/command_line/scripts/teardown.sh \ + --timeout 15000 \ + tests/command_line/scripts/specs/shell.sh diff --git a/tests/command_line/runner/specs.sh b/tests/command_line/runner/specs.sh index aa927b30da..846fe4053f 100755 --- a/tests/command_line/runner/specs.sh +++ b/tests/command_line/runner/specs.sh @@ -8,7 +8,7 @@ export PATH="target/debug:$PATH" command -v sos -if [ -n "$SOS_TEST_CLIENT_DB" ]; then +if [ -z "$SOS_TEST_CLIENT_FS" ]; then # env has already exported SOS_DATA_DIR so this # will create the database file and run migrations sos tool db migrate diff --git a/tests/command_line/tests/main.rs b/tests/command_line/tests/main.rs index d8718ad7e3..8379e79bb4 100644 --- a/tests/command_line/tests/main.rs +++ b/tests/command_line/tests/main.rs @@ -63,7 +63,7 @@ async fn prepare_env() -> Result<()> { println!("prepare_env: {:#?}", data_dir); - if var("SOS_TEST_CLIENT_DB").ok().is_some() { + if var("SOS_TEST_CLIENT_FS").ok().is_none() { let paths = Paths::new_client(&data_dir); let db = paths.database_file().to_owned(); let mut client = open_file(&db).await?; diff --git a/tests/integration/tests/database_entity/mod.rs b/tests/integration/tests/database_entity/mod.rs new file mode 100644 index 0000000000..0696fbd493 --- /dev/null +++ b/tests/integration/tests/database_entity/mod.rs @@ -0,0 +1,242 @@ +use anyhow::Result; +use secrecy::SecretString; +use sos_account::Account; +use sos_backend::BackendTarget; +use sos_client_storage::NewFolderOptions; +use sos_core::{ + crypto::AccessKey, AccountId, InviteStatus, Recipient, VaultId, +}; +use sos_database::{ + async_sqlite::Client, + entity::{ + AccountEntity, AccountRecord, RecipientEntity, SharedFolderEntity, + }, +}; +use sos_net::{NetworkAccount, NetworkAccountOptions}; +use sos_password::diceware::generate_passphrase; +use sos_vault::Summary; + +mod shared_folder; + +pub const FOLDER_NAME: &str = "shared_folder"; + +async fn prepare_local_db_account( + target: &BackendTarget, + name: &str, +) -> Result<(AccountRecord, NetworkAccount, Summary, SecretString)> { + assert!( + matches!(target, BackendTarget::Database(_, _)), + "must be a database target" + ); + let account_name = name.to_string(); + let (password, _) = generate_passphrase()?; + let mut account = NetworkAccount::new_account_with_builder( + account_name.to_owned(), + password.clone(), + target.clone(), + NetworkAccountOptions::default(), + |builder| { + builder + .save_passphrase(false) + .create_archive(false) + .create_authenticator(false) + .create_contacts(false) + .create_file_password(true) + }, + ) + .await?; + + let key: AccessKey = password.clone().into(); + account.sign_in(&key).await?; + let summary = account.default_folder().await.unwrap(); + + let BackendTarget::Database(_, client) = target else { + unreachable!(); + }; + let account_id = *account.account_id(); + let account_record: AccountRecord = client + .conn_and_then(move |conn| { + let entity = AccountEntity::new(&conn); + let account_row = entity.find_one(&account_id)?; + Ok::<_, anyhow::Error>(account_row.try_into().unwrap()) + }) + .await?; + + Ok((account_record, account, summary, password)) +} + +async fn create_recipients_and_shared_folder_with_invite_status( + server: &mut Client, + account1: &mut NetworkAccount, + account2: &mut NetworkAccount, + invite_status: Option, +) -> Result<((AccountId, String), (AccountId, String), VaultId)> { + // Both accounts must have enabled sharing by + // creating recipient information + let recipients = { + let recipients_info = [ + ( + *account1.account_id(), + "name_one", + "one@example.com", + account1.shared_access_public_key().await?, + ), + ( + *account2.account_id(), + "name_two", + "two@example.com", + account2.shared_access_public_key().await?, + ), + ]; + + let mut recipients = Vec::new(); + + // Register each account as a recipient for sharing + for (account_id, name, email, public_key) in + recipients_info.into_iter() + { + let recipient = Recipient { + name: name.to_owned(), + email: Some(email.to_owned()), + public_key: public_key.clone(), + }; + server + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + let recipient_id = + entity.upsert_recipient(account_id, recipient)?; + Ok::<_, anyhow::Error>(recipient_id) + }) + .await?; + + recipients.push(Recipient { + name: name.to_string(), + email: Some(email.to_string()), + public_key, + }); + } + recipients + }; + + let options = NewFolderOptions::new(FOLDER_NAME.to_string()); + let (vault, _access_key) = account1 + .prepare_shared_folder(options, recipients.as_slice(), None) + .await?; + + let folder_id = vault.id(); + + // Create the shared folder which will also prepare invites and joins + // for the target recipients. + SharedFolderEntity::create_shared_folder( + server, + account1.account_id(), + &vault, + recipients.as_slice(), + ) + .await?; + + // Search for recipients + let mut found_recipients = server + .conn_and_then(move |conn| { + let mut entity = RecipientEntity::new(&conn); + Ok::<_, anyhow::Error>(entity.search_recipients("two", None)?) + }) + .await?; + + assert_eq!(1, found_recipients.len()); + + let from_account_id = *account1.account_id(); + let to_account_id = *account2.account_id(); + let from_recipient_public_key = + account1.shared_access_public_key().await?.to_string(); + + let to_recipient = found_recipients.remove(0); + let to_recipient_public_key = to_recipient.recipient_public_key.clone(); + + // Check the sent invites list for the sender (account1) + let mut sent_invites = server + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + Ok::<_, anyhow::Error>(entity.sent_folder_invites( + &from_account_id, + None, + None, + )?) + }) + .await?; + assert_eq!(1, sent_invites.len()); + + // Check the received invites list for the receiver (account2) + let mut received_invites = server + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + Ok::<_, anyhow::Error>(entity.received_folder_invites( + &to_account_id, + None, + None, + )?) + }) + .await?; + assert_eq!(1, received_invites.len()); + + let sent_invite = sent_invites.remove(0); + let received_invite = received_invites.remove(0); + + assert_eq!(sent_invite.row_id, received_invite.row_id); + assert_eq!(FOLDER_NAME, &sent_invite.folder_name); + assert_eq!(FOLDER_NAME, &received_invite.folder_name); + + assert_eq!(&to_recipient_public_key, &sent_invite.recipient_public_key); + assert_eq!( + &from_recipient_public_key, + &received_invite.recipient_public_key + ); + + // Name and email should be for the *other* recipient + assert_eq!("name_two", &sent_invite.recipient_name); + assert_eq!("name_one", &received_invite.recipient_name); + assert_eq!( + Some("two@example.com"), + sent_invite.recipient_email.as_deref() + ); + assert_eq!( + Some("one@example.com"), + received_invite.recipient_email.as_deref() + ); + + // Check the sent invites list for the sender (account1) that have been accepted (should be empty now) + let accepted_invites = server + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + Ok::<_, anyhow::Error>(entity.sent_folder_invites( + &from_account_id, + Some(InviteStatus::Accepted), + None, + )?) + }) + .await?; + assert!(accepted_invites.is_empty()); + + // Accept or decline the invite (account2) + if let Some(invite_status) = invite_status { + let from_public_key = from_recipient_public_key.clone(); + let invite_folder_id = *folder_id; + server + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + Ok::<_, anyhow::Error>(entity.update_folder_invite( + &to_account_id, + invite_status, + &from_public_key, + &invite_folder_id, + )?) + }) + .await?; + } + + Ok(( + (from_account_id, from_recipient_public_key), + (to_account_id, to_recipient_public_key), + *folder_id, + )) +} diff --git a/tests/integration/tests/database_entity/shared_folder/delete_shared_folder.rs b/tests/integration/tests/database_entity/shared_folder/delete_shared_folder.rs new file mode 100644 index 0000000000..ecc24020e0 --- /dev/null +++ b/tests/integration/tests/database_entity/shared_folder/delete_shared_folder.rs @@ -0,0 +1,193 @@ +use anyhow::Result; +use secrecy::SecretString; +use sos_account::Account; +use sos_backend::BackendTarget; +use sos_core::InviteStatus; +use sos_database::async_sqlite::Client; +use sos_database::{ + entity::{AccountRecord, SharedFolderEntity}, + migrations::migrate_client, + open_file, +}; +use sos_net::NetworkAccount; +use sos_sdk::prelude::*; +use sos_test_utils::{default_server_paths, setup, teardown, TestDirs}; + +use crate::database_entity::{ + create_recipients_and_shared_folder_with_invite_status, + prepare_local_db_account, FOLDER_NAME, +}; + +/// Test deleting a shared folder when the account identifer +/// is the owner. +/// +/// The shared folder should be deleted for both the owner and +/// the participant. +#[tokio::test] +async fn db_entity_shared_folder_delete_owner() -> Result<()> { + const TEST_ID: &str = "db_entity_shared_folder_delete_owner"; + // sos_test_utils::init_tracing(); + + // This test is outside of the context of the network however + // the data source must be shared between clients so we configure + // a shared database + let server_paths = default_server_paths(TEST_ID).await?; + let mut server = open_file(server_paths.database_file()).await?; + migrate_client(&mut server).await?; + + let dirs = setup(TEST_ID, 2).await?; + + #[inline(always)] + async fn prepare_db( + dirs: &TestDirs, + index: usize, + server: Client, + ) -> Result<(AccountRecord, NetworkAccount, Summary, SecretString)> { + let name = format!("{}_account_{}", TEST_ID, index); + let paths = Paths::new_client(dirs.clients.get(index).unwrap()); + let target = BackendTarget::Database(paths, server); + prepare_local_db_account(&target, &name).await + } + + let (_, mut account1, _, _) = + prepare_db(&dirs, 0, server.clone()).await?; + let (_, mut account2, _, _) = + prepare_db(&dirs, 1, server.clone()).await?; + + let ((from_account_id, _), (to_account_id, _), folder_id) = + create_recipients_and_shared_folder_with_invite_status( + &mut server, + &mut account1, + &mut account2, + Some(InviteStatus::Accepted), + ) + .await?; + + SharedFolderEntity::delete_shared_folder( + &server, + &from_account_id, + &folder_id, + ) + .await?; + + // Receiver no longer sees the shared folder + let receiver_shared_folder_rows = server + .conn_mut_and_then(move |conn| { + let entity = SharedFolderEntity::new(conn); + Ok::<_, anyhow::Error>( + entity.list_shared_folders(&to_account_id)?, + ) + }) + .await?; + assert!(receiver_shared_folder_rows.is_empty()); + + // Owner no longer sees the shared folder + let owner_shared_folder_rows = server + .conn_mut_and_then(move |conn| { + let entity = SharedFolderEntity::new(conn); + Ok::<_, anyhow::Error>( + entity.list_shared_folders(&from_account_id)?, + ) + }) + .await?; + assert!(owner_shared_folder_rows.is_empty()); + + // Check in-memory folders list does not contain the shared folder + let account1_folders = account1.load_folders().await?; + assert!(!account1_folders.iter().any(|s| s.name() == FOLDER_NAME)); + + // Check in-memory folders list does not contain the shared folder + let account2_folders = account2.load_folders().await?; + assert!(!account2_folders.iter().any(|s| s.name() == FOLDER_NAME)); + + teardown(TEST_ID).await; + + Ok(()) +} + +/// Test deleting a shared folder when the account identifer +/// is a participant. +/// +/// The shared folder should be deleted for the participant but +/// not for the owner. +#[tokio::test] +async fn db_entity_shared_folder_delete_participant() -> Result<()> { + const TEST_ID: &str = "db_entity_shared_folder_delete_participant"; + // sos_test_utils::init_tracing(); + + // This test is outside of the context of the network however + // the data source must be shared between clients so we configure + // a shared database + let server_paths = default_server_paths(TEST_ID).await?; + let mut server = open_file(server_paths.database_file()).await?; + migrate_client(&mut server).await?; + + let dirs = setup(TEST_ID, 2).await?; + + #[inline(always)] + async fn prepare_db( + dirs: &TestDirs, + index: usize, + server: Client, + ) -> Result<(AccountRecord, NetworkAccount, Summary, SecretString)> { + let name = format!("{}_account_{}", TEST_ID, index); + let paths = Paths::new_client(dirs.clients.get(index).unwrap()); + let target = BackendTarget::Database(paths, server); + prepare_local_db_account(&target, &name).await + } + + let (_, mut account1, _, _) = + prepare_db(&dirs, 0, server.clone()).await?; + let (_, mut account2, _, _) = + prepare_db(&dirs, 1, server.clone()).await?; + + let ((from_account_id, _), (to_account_id, _), folder_id) = + create_recipients_and_shared_folder_with_invite_status( + &mut server, + &mut account1, + &mut account2, + Some(InviteStatus::Accepted), + ) + .await?; + + SharedFolderEntity::delete_shared_folder( + &server, + &to_account_id, + &folder_id, + ) + .await?; + + // Receiver no longer sees the shared folder + let receiver_shared_folder_rows = server + .conn_mut_and_then(move |conn| { + let entity = SharedFolderEntity::new(conn); + Ok::<_, anyhow::Error>( + entity.list_shared_folders(&to_account_id)?, + ) + }) + .await?; + assert!(receiver_shared_folder_rows.is_empty()); + + // Owner can still see the shared folder + let owner_shared_folder_rows = server + .conn_mut_and_then(move |conn| { + let entity = SharedFolderEntity::new(conn); + Ok::<_, anyhow::Error>( + entity.list_shared_folders(&from_account_id)?, + ) + }) + .await?; + assert_eq!(1, owner_shared_folder_rows.len()); + + // Check in-memory folders list contains the shared folder (owner) + let account1_folders = account1.load_folders().await?; + assert!(account1_folders.iter().any(|s| s.name() == FOLDER_NAME)); + + // Check in-memory folders list does not contain the shared folder (participant) + let account2_folders = account2.load_folders().await?; + assert!(!account2_folders.iter().any(|s| s.name() == FOLDER_NAME)); + + // teardown(TEST_ID).await; + + Ok(()) +} diff --git a/tests/integration/tests/database_entity/shared_folder/folder_invite.rs b/tests/integration/tests/database_entity/shared_folder/folder_invite.rs new file mode 100644 index 0000000000..38499554fd --- /dev/null +++ b/tests/integration/tests/database_entity/shared_folder/folder_invite.rs @@ -0,0 +1,209 @@ +use anyhow::Result; +use secrecy::SecretString; +use sos_account::Account; +use sos_backend::BackendTarget; +use sos_core::InviteStatus; +use sos_database::async_sqlite::Client; +use sos_database::{ + entity::{AccountRecord, SharedFolderEntity}, + migrations::migrate_client, + open_file, +}; +use sos_net::NetworkAccount; +use sos_sdk::prelude::*; +use sos_test_utils::{default_server_paths, setup, teardown, TestDirs}; +use sos_vault::SharedAccess; + +use crate::database_entity::{ + create_recipients_and_shared_folder_with_invite_status, + prepare_local_db_account, FOLDER_NAME, +}; + +/// Test sending a folder invite to another recipient and +/// the recipient accepts the invite. +#[tokio::test] +async fn db_entity_shared_folder_accept_invite() -> Result<()> { + const TEST_ID: &str = "db_entity_shared_folder_accept_invite"; + // sos_test_utils::init_tracing(); + + // This test is outside of the context of the network however + // the data source must be shared between clients so we configure + // a shared database + let server_paths = default_server_paths(TEST_ID).await?; + let mut server = open_file(server_paths.database_file()).await?; + migrate_client(&mut server).await?; + + let dirs = setup(TEST_ID, 2).await?; + + #[inline(always)] + async fn prepare_db( + dirs: &TestDirs, + index: usize, + server: Client, + ) -> Result<(AccountRecord, NetworkAccount, Summary, SecretString)> { + let name = format!("{}_account_{}", TEST_ID, index); + let paths = Paths::new_client(dirs.clients.get(index).unwrap()); + let target = BackendTarget::Database(paths, server); + prepare_local_db_account(&target, &name).await + } + + let (_, mut account1, _, _) = + prepare_db(&dirs, 0, server.clone()).await?; + let (_, mut account2, _, _) = + prepare_db(&dirs, 1, server.clone()).await?; + + let ((from_account_id, _), (to_account_id, _), _) = + create_recipients_and_shared_folder_with_invite_status( + &mut server, + &mut account1, + &mut account2, + Some(InviteStatus::Accepted), + ) + .await?; + + // Sender can see the accepted invite + let sender_accepted_invites = server + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + Ok::<_, anyhow::Error>(entity.sent_folder_invites( + &from_account_id, + Some(InviteStatus::Accepted), + None, + )?) + }) + .await?; + assert_eq!(1, sender_accepted_invites.len()); + + // Receiver can see the accepted invite + let receiver_accepted_invites = server + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + Ok::<_, anyhow::Error>(entity.received_folder_invites( + &to_account_id, + Some(InviteStatus::Accepted), + None, + )?) + }) + .await?; + assert_eq!(1, receiver_accepted_invites.len()); + + // Receiver can list the shared folder + let shared_folder_rows = server + .conn_mut_and_then(move |conn| { + let entity = SharedFolderEntity::new(conn); + Ok::<_, anyhow::Error>( + entity.list_shared_folders(&to_account_id)?, + ) + }) + .await?; + + let mut shared_folder_records = + SharedFolderEntity::from_rows(shared_folder_rows).await?; + assert_eq!(1, shared_folder_records.len()); + let record = shared_folder_records.remove(0); + assert_eq!(FOLDER_NAME, record.folder.summary.name()); + assert!(record.folder.summary.flags().is_shared()); + assert_eq!(Cipher::X25519, *record.folder.summary.cipher()); + assert!(matches!( + record.folder.shared_access, + Some(SharedAccess::WriteAccess(_)) + )); + + // Check in-memory folders list contains the shared folder + let account1_folders = account1.load_folders().await?; + assert!(account1_folders.iter().any(|s| s.name() == FOLDER_NAME)); + + // Check in-memory folders list contains the shared folder + let account2_folders = account2.load_folders().await?; + assert!(account2_folders.iter().any(|s| s.name() == FOLDER_NAME)); + + teardown(TEST_ID).await; + + Ok(()) +} + +/// Test sending a folder invite to another recipient and +/// the recipient declines the invite. +#[tokio::test] +async fn db_entity_shared_folder_decline_invite() -> Result<()> { + const TEST_ID: &str = "db_entity_shared_folder_decline_invite"; + // sos_test_utils::init_tracing(); + + // This test is outside of the context of the network however + // the data source must be shared between clients so we configure + // a shared database + let server_paths = default_server_paths(TEST_ID).await?; + let mut server = open_file(server_paths.database_file()).await?; + migrate_client(&mut server).await?; + + let dirs = setup(TEST_ID, 2).await?; + + #[inline(always)] + async fn prepare_db( + dirs: &TestDirs, + index: usize, + server: Client, + ) -> Result<(AccountRecord, NetworkAccount, Summary, SecretString)> { + let name = format!("{}_account_{}", TEST_ID, index); + let paths = Paths::new_client(dirs.clients.get(index).unwrap()); + let target = BackendTarget::Database(paths, server); + prepare_local_db_account(&target, &name).await + } + + let (_, mut account1, _, _) = + prepare_db(&dirs, 0, server.clone()).await?; + let (_, mut account2, _, _) = + prepare_db(&dirs, 1, server.clone()).await?; + + let ((from_account_id, _), (to_account_id, _), _) = + create_recipients_and_shared_folder_with_invite_status( + &mut server, + &mut account1, + &mut account2, + Some(InviteStatus::Declined), + ) + .await?; + + // Sender can see the declined invite + let sender_declined_invites = server + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + Ok::<_, anyhow::Error>(entity.sent_folder_invites( + &from_account_id, + Some(InviteStatus::Declined), + None, + )?) + }) + .await?; + assert_eq!(1, sender_declined_invites.len()); + + // Receiver can see the declined invite + let receiver_declined_invites = server + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + Ok::<_, anyhow::Error>(entity.received_folder_invites( + &to_account_id, + Some(InviteStatus::Declined), + None, + )?) + }) + .await?; + assert_eq!(1, receiver_declined_invites.len()); + + // Receiver can list shared folders + let shared_folder_rows = server + .conn_mut_and_then(move |conn| { + let entity = SharedFolderEntity::new(conn); + Ok::<_, anyhow::Error>( + entity.list_shared_folders(&to_account_id)?, + ) + }) + .await?; + + // But we should not see any rows as the invite was declined + assert!(shared_folder_rows.is_empty()); + + teardown(TEST_ID).await; + + Ok(()) +} diff --git a/tests/integration/tests/database_entity/shared_folder/manage_recipient.rs b/tests/integration/tests/database_entity/shared_folder/manage_recipient.rs new file mode 100644 index 0000000000..5fce6bfdea --- /dev/null +++ b/tests/integration/tests/database_entity/shared_folder/manage_recipient.rs @@ -0,0 +1,113 @@ +use anyhow::Result; +use sos_account::Account; +use sos_backend::BackendTarget; +use sos_core::Recipient; +use sos_database::{ + entity::{RecipientRecord, SharedFolderEntity}, + migrations::migrate_client, + open_file, +}; +use sos_sdk::prelude::*; +use sos_test_utils::{setup, teardown}; + +use crate::database_entity::prepare_local_db_account; + +/// Test managing recipient information for an account. +#[tokio::test] +async fn db_entity_shared_folder_manage_recipient() -> Result<()> { + const TEST_ID: &str = "db_entity_shared_folder_manage_recipient"; + // sos_test_utils::init_tracing(); + + let mut dirs = setup(TEST_ID, 1).await?; + let data_dir = dirs.clients.remove(0); + + // Configure the db client + let paths = Paths::new_client(&data_dir); + let mut client = open_file(paths.database_file()).await?; + migrate_client(&mut client).await?; + let target = BackendTarget::Database(paths, client.clone()); + + let (_account_record, account, _, _) = + prepare_local_db_account(&target, TEST_ID).await?; + + let account_id = *account.account_id(); + let recipient_name = "Example"; + let recipient_email = "user@example.com"; + let recipient_public_key = account.shared_access_public_key().await?; + + let recipient = Recipient { + name: recipient_name.to_owned(), + email: Some(recipient_email.to_owned()), + public_key: recipient_public_key.clone(), + }; + + // Initial insert on creating recipient information + let recipient_id = client + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + let recipient_id = + entity.upsert_recipient(account_id, recipient)?; + Ok::<_, anyhow::Error>(recipient_id) + }) + .await?; + + // Get the recipient record + let recipient_record: RecipientRecord = client + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + entity.find_recipient(account_id) + }) + .await? + .expect("to find recipient record"); + + assert_eq!(recipient_id, recipient_record.row_id); + assert_eq!(recipient_name, &recipient_record.recipient_name); + assert_eq!( + Some(recipient_email), + recipient_record.recipient_email.as_deref() + ); + assert_eq!( + recipient_public_key.to_string(), + recipient_record.recipient_public_key + ); + + let new_recipient_name = "Example"; + let new_recipient_email = "new-user@example.com"; + + let recipient = Recipient { + name: new_recipient_name.to_owned(), + email: Some(new_recipient_email.to_owned()), + public_key: recipient_public_key.clone(), + }; + + // Update recipient information for an account + let new_recipient_id = client + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + let recipient_id = + entity.upsert_recipient(account_id, recipient)?; + Ok::<_, anyhow::Error>(recipient_id) + }) + .await?; + assert_eq!(recipient_id, new_recipient_id); + + // Get the recipient record + let recipient_record: RecipientRecord = client + .conn_mut_and_then(move |conn| { + let mut entity = SharedFolderEntity::new(conn); + entity.find_recipient(account_id) + }) + .await? + .expect("to find recipient record"); + + assert_eq!(new_recipient_id, recipient_record.row_id); + assert_eq!(new_recipient_name, &recipient_record.recipient_name); + assert_eq!( + Some(new_recipient_email), + recipient_record.recipient_email.as_deref() + ); + + teardown(TEST_ID).await; + + Ok(()) +} diff --git a/tests/integration/tests/database_entity/shared_folder/mod.rs b/tests/integration/tests/database_entity/shared_folder/mod.rs new file mode 100644 index 0000000000..ad1d5db79d --- /dev/null +++ b/tests/integration/tests/database_entity/shared_folder/mod.rs @@ -0,0 +1,3 @@ +mod delete_shared_folder; +mod folder_invite; +mod manage_recipient; diff --git a/tests/integration/tests/main.rs b/tests/integration/tests/main.rs index bb6efef2ab..ef274327d2 100644 --- a/tests/integration/tests/main.rs +++ b/tests/integration/tests/main.rs @@ -4,6 +4,7 @@ mod auto_merge; mod backup_archive; mod changes; mod database; +mod database_entity; mod diff_merge; mod event_log; mod file_transfers; @@ -14,4 +15,5 @@ mod network_account; mod network_config; mod pairing; mod preferences; +mod shared_folders; mod sign_in; diff --git a/tests/integration/tests/network_account/listen_folder_import.rs b/tests/integration/tests/network_account/listen_folder_import.rs index f757fe279c..0f86931411 100644 --- a/tests/integration/tests/network_account/listen_folder_import.rs +++ b/tests/integration/tests/network_account/listen_folder_import.rs @@ -84,7 +84,7 @@ async fn network_sync_listen_folder_import() -> Result<()> { // Pause a while to allow the first owner to sync // with the new change - sync_pause(None).await; + sync_pause(Some(1500)).await; // Expected folders on the local account must be computed // again after creating the new folder for the assertions diff --git a/tests/integration/tests/network_config/self_signed.rs b/tests/integration/tests/network_config/self_signed.rs index 9219ebd0d9..bf57764496 100644 --- a/tests/integration/tests/network_config/self_signed.rs +++ b/tests/integration/tests/network_config/self_signed.rs @@ -3,7 +3,7 @@ use sos_protocol::{network_client::NetworkConfig, reqwest::Url}; use sos_server::{SslConfig, TlsConfig}; use sos_test_utils::{ default_server_config, simulate_device_with_network_config, - spawn_with_config, + spawn_with_config, teardown, }; use std::{collections::HashMap, path::PathBuf}; @@ -59,5 +59,7 @@ async fn self_signed_server() -> Result<()> { tokio::time::sleep(std::time::Duration::from_secs(1)).await; + teardown(TEST_ID).await; + Ok(()) } diff --git a/tests/integration/tests/shared_folders/mod.rs b/tests/integration/tests/shared_folders/mod.rs new file mode 100644 index 0000000000..5cabc7ae14 --- /dev/null +++ b/tests/integration/tests/shared_folders/mod.rs @@ -0,0 +1,69 @@ +use anyhow::Result; +use secrecy::{ExposeSecret, SecretString}; +use sos_account::{Account, SecretChange}; +use sos_core::{crypto::AccessKey, SecretId, VaultId}; +use sos_net::NetworkAccount; +use sos_test_utils::mock; +use sos_vault::secret::Secret; + +mod shared_folder_delete_owner; +mod shared_folder_delete_participant; +mod shared_folder_secret_lifecycle; +// mod shared_folder_write_access; + +pub async fn assert_shared_folder_lifecycle( + owner: &mut NetworkAccount, + folder_id: &VaultId, + password: SecretString, + test_id: &str, +) -> Result> { + let (meta, secret) = mock::note(test_id, test_id); + let SecretChange { id: secret_id, .. } = + owner.create_secret(meta, secret, folder_id.into()).await?; + + let (row, _) = owner.read_secret(&secret_id, Some(folder_id)).await?; + let output_secret_id = secret_id; + + assert!(matches!(row.secret(), Secret::Note { .. })); + + let new_value = ""; + let (_, meta, _) = row.into(); + owner + .update_secret( + &secret_id, + meta, + Some(Secret::Note { + text: new_value.into(), + user_data: Default::default(), + }), + folder_id.into(), + ) + .await?; + + // Sign out and then re-authenticate to check + // shared folder access from sign in + + owner.sign_out().await?; + + let key: AccessKey = password.into(); + owner.sign_in(&key).await?; + + // Check we can read a secret created in the previous session + let (row, _) = owner.read_secret(&secret_id, Some(folder_id)).await?; + let value = if let Secret::Note { text, .. } = row.secret() { + text.expose_secret().to_owned() + } else { + panic!("expecting a secret note"); + }; + assert_eq!(new_value, &value); + + // Create another secret + let (meta, secret) = mock::note(test_id, test_id); + let SecretChange { id: secret_id, .. } = + owner.create_secret(meta, secret, folder_id.into()).await?; + + // Check secret deletion + owner.delete_secret(&secret_id, folder_id.into()).await?; + + Ok(vec![output_secret_id]) +} diff --git a/tests/integration/tests/shared_folders/shared_folder_delete_owner.rs b/tests/integration/tests/shared_folders/shared_folder_delete_owner.rs new file mode 100644 index 0000000000..2ec99dbb16 --- /dev/null +++ b/tests/integration/tests/shared_folders/shared_folder_delete_owner.rs @@ -0,0 +1,186 @@ +use anyhow::Result; +use sos_account::{Account, FolderCreate}; +use sos_client_storage::NewFolderOptions; +use sos_core::InviteStatus; +use sos_protocol::AccountSync; +use sos_test_utils::{simulate_device, spawn, teardown}; + +/// Tests creating a shared folder and having the owner +/// delete the folder. Once the partcipant has called sync() +/// the folder will also be unavailable to the participant. +#[tokio::test] +async fn shared_folder_delete_owner() -> Result<()> { + const TEST_ID: &str = "shared_folder_delete_owner"; + // sos_test_utils::init_tracing(); + + // Spawn a backend server and wait for it to be listening + let server = spawn(TEST_ID, None, None).await?; + let origin = server.origin.clone(); + + let test_id_owner: String = format!("{}_owner", TEST_ID); + let test_id_participant: String = format!("{}_participant", TEST_ID); + + // Prepare mock device(s0) + let mut account1 = + simulate_device(&test_id_owner, 1, Some(&server)).await?; + let account1_password = account1.password.clone(); + + let mut account2 = + simulate_device(&test_id_participant, 1, Some(&server)).await?; + let account2_password = account2.password.clone(); + + // Need both accounts to have set public recipient information + // for PKI and discovery + let account1_info = ("name_one", "one@example.com"); + let account2_info = ("name_two", "two@example.com"); + let recipient1 = account1 + .owner + .set_recipient( + &origin, + account1_info.0.to_string(), + Some(account1_info.1.to_string()), + ) + .await?; + let recipient2 = account2 + .owner + .set_recipient( + &origin, + account2_info.0.to_string(), + Some(account2_info.1.to_string()), + ) + .await?; + + // Check fetching recipient information from the server. + let server_recipient1 = account1.owner.find_recipient(&origin).await?; + let server_recipient2 = account2.owner.find_recipient(&origin).await?; + assert_eq!(Some(recipient1.clone()), server_recipient1); + assert_eq!(Some(recipient2.clone()), server_recipient2); + + let mut recipients = vec![recipient1]; + + // Perform a search to find the target recipient + let results = account1 + .owner + .search_recipients(&origin, "two".to_owned(), None) + .await?; + recipients.extend(results.into_iter()); + + let folder_name = "shared_folder"; + let options = NewFolderOptions::new(folder_name.to_string()); + let FolderCreate { + folder: shared_folder, + .. + } = account1 + .owner + .create_shared_folder(options, &origin, recipients.as_slice(), None) + .await?; + + let sent_invites = account1 + .owner + .sent_folder_invites(&origin, Some(InviteStatus::Pending), None) + .await?; + assert!(!sent_invites.is_empty()); + + let mut received_invites = account2 + .owner + .received_folder_invites(&origin, Some(InviteStatus::Pending), None) + .await?; + assert!(!received_invites.is_empty()); + + let folder_invite = received_invites.remove(0); + assert_eq!(shared_folder.id(), &folder_invite.folder_id); + + let folders = account1.owner.list_folders().await?; + assert!(folders.iter().any(|f| f.name() == folder_name)); + + // Ensure the owner can manage secrets in the folder + let mut secret_ids = super::assert_shared_folder_lifecycle( + &mut account1.owner, + shared_folder.id(), + account1_password, + &test_id_owner, + ) + .await?; + + // Accept the folder invite which will + // prepare the local copy of the shared + // folder + account2 + .owner + .accept_folder_invite( + &origin, + folder_invite.recipient_public_key, + folder_invite.folder_id, + ) + .await?; + + let folders = account2.owner.load_folders().await?; + assert!(folders.iter().any(|f| f.name() == shared_folder.name())); + + // Check the participant in the shared folder + // can read the existing secrets + for secret_id in &secret_ids { + account2 + .owner + .read_secret(secret_id, Some(shared_folder.id())) + .await?; + } + + // Ensure the participant can manage secrets in the folder + super::assert_shared_folder_lifecycle( + &mut account2.owner, + shared_folder.id(), + account2_password, + &test_id_participant, + ) + .await?; + + // Owner deletes the shared folder + account1 + .owner + .delete_shared_folder(&origin, shared_folder.id()) + .await?; + + // Immediately not available to the owner + assert!(!account1 + .owner + .list_folders() + .await? + .iter() + .any(|f| f.name() == folder_name)); + + // The participant can still access the shared secret for now + let target_secret_id = secret_ids.remove(0); + account2 + .owner + .read_secret(&target_secret_id, Some(shared_folder.id())) + .await?; + + // Now the participant performs a sync + let sync_result = account2.owner.sync().await; + assert!(sync_result.first_error().is_none()); + + // Now the shared folder is no longer accessible to the participant + assert!(!account2 + .owner + .list_folders() + .await? + .iter() + .any(|f| f.name() == folder_name)); + + // Check the secret is no longer accessible to the participant + assert!(account2 + .owner + .read_secret(&target_secret_id, Some(shared_folder.id())) + .await + .is_err()); + + account1.owner.sign_out().await?; + account2.owner.sign_out().await?; + + teardown(TEST_ID).await; + teardown(&test_id_owner).await; + teardown(&test_id_participant).await; + + Ok(()) +} diff --git a/tests/integration/tests/shared_folders/shared_folder_delete_participant.rs b/tests/integration/tests/shared_folders/shared_folder_delete_participant.rs new file mode 100644 index 0000000000..4687efcf92 --- /dev/null +++ b/tests/integration/tests/shared_folders/shared_folder_delete_participant.rs @@ -0,0 +1,183 @@ +use anyhow::Result; +use sos_account::{Account, FolderCreate}; +use sos_client_storage::NewFolderOptions; +use sos_core::InviteStatus; +use sos_protocol::AccountSync; +use sos_test_utils::{simulate_device, spawn, teardown}; + +/// Tests creating a shared folder and having a participant +/// delete the folder. After a participant has deleted a +/// shared folder it should still be available to the owner +/// and other participants. +#[tokio::test] +async fn shared_folder_delete_participant() -> Result<()> { + const TEST_ID: &str = "shared_folder_delete_participant"; + sos_test_utils::init_tracing(); + + // Spawn a backend server and wait for it to be listening + let server = spawn(TEST_ID, None, None).await?; + let origin = server.origin.clone(); + + let test_id_owner: String = format!("{}_owner", TEST_ID); + let test_id_participant: String = format!("{}_participant", TEST_ID); + + // Prepare mock device(s0) + let mut account1 = + simulate_device(&test_id_owner, 1, Some(&server)).await?; + let account1_password = account1.password.clone(); + + let mut account2 = + simulate_device(&test_id_participant, 1, Some(&server)).await?; + let account2_password = account2.password.clone(); + + // Need both accounts to have set public recipient information + // for PKI and discovery + let account1_info = ("name_one", "one@example.com"); + let account2_info = ("name_two", "two@example.com"); + let recipient1 = account1 + .owner + .set_recipient( + &origin, + account1_info.0.to_string(), + Some(account1_info.1.to_string()), + ) + .await?; + let recipient2 = account2 + .owner + .set_recipient( + &origin, + account2_info.0.to_string(), + Some(account2_info.1.to_string()), + ) + .await?; + + // Check fetching recipient information from the server. + let server_recipient1 = account1.owner.find_recipient(&origin).await?; + let server_recipient2 = account2.owner.find_recipient(&origin).await?; + assert_eq!(Some(recipient1.clone()), server_recipient1); + assert_eq!(Some(recipient2.clone()), server_recipient2); + + let mut recipients = vec![recipient1]; + + // Perform a search to find the target recipient + let results = account1 + .owner + .search_recipients(&origin, "two".to_owned(), None) + .await?; + recipients.extend(results.into_iter()); + + let folder_name = "shared_folder"; + let options = NewFolderOptions::new(folder_name.to_string()); + let FolderCreate { + folder: shared_folder, + .. + } = account1 + .owner + .create_shared_folder(options, &origin, recipients.as_slice(), None) + .await?; + + let sent_invites = account1 + .owner + .sent_folder_invites(&origin, Some(InviteStatus::Pending), None) + .await?; + assert!(!sent_invites.is_empty()); + + let mut received_invites = account2 + .owner + .received_folder_invites(&origin, Some(InviteStatus::Pending), None) + .await?; + assert!(!received_invites.is_empty()); + + let folder_invite = received_invites.remove(0); + assert_eq!(shared_folder.id(), &folder_invite.folder_id); + + let folders = account1.owner.list_folders().await?; + assert!(folders.iter().any(|f| f.name() == folder_name)); + + // Ensure the owner can manage secrets in the folder + let mut secret_ids = super::assert_shared_folder_lifecycle( + &mut account1.owner, + shared_folder.id(), + account1_password, + &test_id_owner, + ) + .await?; + + // Accept the folder invite which will + // prepare the local copy of the shared + // folder + account2 + .owner + .accept_folder_invite( + &origin, + folder_invite.recipient_public_key, + folder_invite.folder_id, + ) + .await?; + + let folders = account2.owner.load_folders().await?; + assert!(folders.iter().any(|f| f.name() == shared_folder.name())); + + // Check the participant in the shared folder + // can read the existing secrets + for secret_id in &secret_ids { + account2 + .owner + .read_secret(secret_id, Some(shared_folder.id())) + .await?; + } + + // Ensure the participant can manage secrets in the folder + super::assert_shared_folder_lifecycle( + &mut account2.owner, + shared_folder.id(), + account2_password, + &test_id_participant, + ) + .await?; + + // Participant deletes the shared folder + account2 + .owner + .delete_shared_folder(&origin, shared_folder.id()) + .await?; + + // Immediately not available to the participant + assert!(!account2 + .owner + .list_folders() + .await? + .iter() + .any(|f| f.name() == folder_name)); + + // Now the owner performs a sync + let sync_result = account1.owner.sync().await; + println!("{:#?}", sync_result); + assert!(sync_result.first_error().is_none()); + + let target_secret_id = secret_ids.remove(0); + + // The shared folder is still accessible to the owner + assert!(account1 + .owner + .list_folders() + .await? + .iter() + .any(|f| f.name() == folder_name)); + + // Check the secret is still accessible to the owner + assert!(account1 + .owner + .read_secret(&target_secret_id, Some(shared_folder.id())) + .await + .is_ok()); + + account1.owner.sign_out().await?; + account2.owner.sign_out().await?; + + teardown(TEST_ID).await; + teardown(&test_id_owner).await; + teardown(&test_id_participant).await; + + Ok(()) +} diff --git a/tests/integration/tests/shared_folders/shared_folder_secret_lifecycle.rs b/tests/integration/tests/shared_folders/shared_folder_secret_lifecycle.rs new file mode 100644 index 0000000000..47bf3d3713 --- /dev/null +++ b/tests/integration/tests/shared_folders/shared_folder_secret_lifecycle.rs @@ -0,0 +1,158 @@ +use anyhow::Result; +use sos_account::{Account, FolderCreate}; +use sos_client_storage::NewFolderOptions; +use sos_core::InviteStatus; +use sos_protocol::AccountSync; +use sos_test_utils::{simulate_device, spawn, teardown}; + +/// Tests creating a shared folder and having the owner +/// and a participant perform basic secret lifecycle operations. +#[tokio::test] +async fn shared_folder_secret_lifecycle() -> Result<()> { + const TEST_ID: &str = "shared_folder_secret_lifecycle"; + // sos_test_utils::init_tracing(); + + // Spawn a backend server and wait for it to be listening + let server = spawn(TEST_ID, None, None).await?; + let origin = server.origin.clone(); + + let test_id_owner: String = format!("{}_owner", TEST_ID); + let test_id_participant: String = format!("{}_participant", TEST_ID); + + // Prepare mock device(s0) + let mut account1 = + simulate_device(&test_id_owner, 1, Some(&server)).await?; + let account1_password = account1.password.clone(); + + let mut account2 = + simulate_device(&test_id_participant, 1, Some(&server)).await?; + let account2_password = account2.password.clone(); + + // Need both accounts to have set public recipient information + // for PKI and discovery + let account1_info = ("name_one", "one@example.com"); + let account2_info = ("name_two", "two@example.com"); + let recipient1 = account1 + .owner + .set_recipient( + &origin, + account1_info.0.to_string(), + Some(account1_info.1.to_string()), + ) + .await?; + let recipient2 = account2 + .owner + .set_recipient( + &origin, + account2_info.0.to_string(), + Some(account2_info.1.to_string()), + ) + .await?; + + // Check fetching recipient information from the server. + let server_recipient1 = account1.owner.find_recipient(&origin).await?; + let server_recipient2 = account2.owner.find_recipient(&origin).await?; + assert_eq!(Some(recipient1.clone()), server_recipient1); + assert_eq!(Some(recipient2.clone()), server_recipient2); + + let mut recipients = vec![recipient1]; + + // Perform a search to find the target recipient + let results = account1 + .owner + .search_recipients(&origin, "two".to_owned(), None) + .await?; + recipients.extend(results.into_iter()); + + let folder_name = "shared_folder"; + let options = NewFolderOptions::new(folder_name.to_string()); + let FolderCreate { + folder: shared_folder, + .. + } = account1 + .owner + .create_shared_folder(options, &origin, recipients.as_slice(), None) + .await?; + + let sent_invites = account1 + .owner + .sent_folder_invites(&origin, Some(InviteStatus::Pending), None) + .await?; + assert!(!sent_invites.is_empty()); + + let mut received_invites = account2 + .owner + .received_folder_invites(&origin, Some(InviteStatus::Pending), None) + .await?; + assert!(!received_invites.is_empty()); + + let folder_invite = received_invites.remove(0); + assert_eq!(shared_folder.id(), &folder_invite.folder_id); + + let folders = account1.owner.list_folders().await?; + assert!(folders.iter().any(|f| f.name() == folder_name)); + + // Ensure the owner can manage secrets in the folder + let secret_ids = super::assert_shared_folder_lifecycle( + &mut account1.owner, + shared_folder.id(), + account1_password, + &test_id_owner, + ) + .await?; + + // Accept the folder invite which will + // prepare the local copy of the shared + // folder + account2 + .owner + .accept_folder_invite( + &origin, + folder_invite.recipient_public_key, + folder_invite.folder_id, + ) + .await?; + + let folders = account2.owner.load_folders().await?; + assert!(folders.iter().any(|f| f.name() == shared_folder.name())); + + // Check the participant in the shared folder + // can read the existing secrets + for secret_id in &secret_ids { + account2 + .owner + .read_secret(secret_id, Some(shared_folder.id())) + .await?; + } + + // Ensure the participant can manage secrets in the folder + let new_secret_ids = super::assert_shared_folder_lifecycle( + &mut account2.owner, + shared_folder.id(), + account2_password, + &test_id_participant, + ) + .await?; + + // Ensure the owner can read the secrets that the participant creates + // after calling sync(). + let sync_result = account1.owner.sync().await; + assert!(sync_result.first_error().is_none()); + + for secret_id in &new_secret_ids { + let (row, _) = account1 + .owner + .read_secret(secret_id, Some(shared_folder.id())) + .await?; + println!("{row:?}"); + } + + account1.owner.sign_out().await?; + account2.owner.sign_out().await?; + + teardown(TEST_ID).await; + teardown(&test_id_owner).await; + teardown(&test_id_participant).await; + + Ok(()) +} diff --git a/tests/integration/tests/shared_folders/shared_folder_write_access.rs b/tests/integration/tests/shared_folders/shared_folder_write_access.rs new file mode 100644 index 0000000000..d4bd0eb968 --- /dev/null +++ b/tests/integration/tests/shared_folders/shared_folder_write_access.rs @@ -0,0 +1,25 @@ +use anyhow::Result; +use sos_account::Account; +use sos_test_utils::{simulate_device, spawn, teardown}; + +/// Tests creating a shared folder and having the owner +/// and another user perform write operations. +#[tokio::test] +async fn shared_folder_write_access() -> Result<()> { + const TEST_ID: &str = "shared_folder_write_access"; + //sos_test_utils::init_tracing(); + + // Spawn a backend server and wait for it to be listening + let server = spawn(TEST_ID, None, None).await?; + + // Prepare mock devices with different accounts + let mut device1 = simulate_device(TEST_ID, 1, Some(&server)).await?; + let mut device2 = simulate_device(TEST_ID, 1, Some(&server)).await?; + + device1.owner.sign_out().await?; + device2.owner.sign_out().await?; + + teardown(TEST_ID).await; + + Ok(()) +} diff --git a/tests/unit/src/tests/server_storage.rs b/tests/unit/src/tests/server_storage.rs index 089dfa4e5d..81384b1bf0 100644 --- a/tests/unit/src/tests/server_storage.rs +++ b/tests/unit/src/tests/server_storage.rs @@ -1,3 +1,5 @@ +use std::{collections::HashMap, sync::Arc}; + use anyhow::Result; use rand::{rngs::OsRng, Rng}; use sos_backend::BackendTarget; @@ -18,6 +20,7 @@ use sos_sync::{ use sos_test_utils::mock::{insert_database_vault, memory_database}; use sos_vault::Vault; use tempfile::tempdir_in; +use tokio::sync::Mutex; #[tokio::test] async fn fs_server_storage() -> Result<()> { @@ -25,9 +28,11 @@ async fn fs_server_storage() -> Result<()> { Paths::scaffold(&temp.path().to_owned()).await?; let account_id = AccountId::random(); + let shared_folder_events = Arc::new(Mutex::new(HashMap::default())); let mut storage = ServerStorage::new( BackendTarget::FileSystem(Paths::new_server(temp.path())), &account_id, + shared_folder_events, ) .await?; assert_server_storage(&mut storage, &account_id).await?; @@ -46,9 +51,11 @@ async fn db_server_storage() -> Result<()> { insert_database_vault(&mut client, &vault, true).await?; let paths = Paths::new_server(temp.path()); + let shared_folder_events = Arc::new(Mutex::new(HashMap::default())); let mut storage = ServerStorage::new( BackendTarget::Database(paths, client), &account_id, + shared_folder_events, ) .await?; assert_server_storage(&mut storage, &account_id).await?; diff --git a/tests/utils/src/lib.rs b/tests/utils/src/lib.rs index ec07a70ce4..4f13d5a37d 100644 --- a/tests/utils/src/lib.rs +++ b/tests/utils/src/lib.rs @@ -47,7 +47,10 @@ pub fn init_tracing() { pub async fn make_client_backend( paths: &Arc, ) -> Result { - Ok(if std::env::var("SOS_TEST_CLIENT_DB").ok().is_some() { + Ok(if std::env::var("SOS_TEST_CLIENT_FS").ok().is_some() { + Paths::scaffold(paths.documents_dir()).await?; + BackendTarget::FileSystem(paths.clone()) + } else { let db_file = paths.database_file(); /* @@ -60,9 +63,6 @@ pub async fn make_client_backend( let mut client = open_file(&db_file).await?; sos_database::migrations::migrate_client(&mut client).await?; BackendTarget::Database(paths.clone(), client) - } else { - Paths::scaffold(paths.documents_dir()).await?; - BackendTarget::FileSystem(paths.clone()) }) } @@ -116,7 +116,7 @@ impl MockServer { // using the test identifier config.storage.path = self.path.clone(); - if std::env::var("SOS_TEST_SERVER_DB").ok().is_some() { + if std::env::var("SOS_TEST_SERVER_FS").ok().is_none() { let db_file = self.path.join(DATABASE_FILE); // Make sure each server test run is pristine @@ -223,6 +223,42 @@ pub async fn spawn( spawn_with_config(test_id, addr, server_id, None).await } +/// Build server paths. +pub async fn default_server_paths(test_id: &str) -> Result> { + server_paths(test_id, None, None).await +} + +/// Build server paths for a test id. +async fn server_paths( + test_id: &str, + server_id: Option<&str>, + addr: Option, +) -> Result> { + let current_dir = std::env::current_dir() + .expect("failed to get current working directory"); + + // Prepare server storage + let target = current_dir.join("../../target/integration-test"); + vfs::create_dir_all(&target).await?; + let target = target.canonicalize()?; + + let server_id = server_id.unwrap_or("server"); + + // Ensure test runner is pristine + let path = target.join(test_id).join(server_id); + + // Some tests need to restart a server so we should + // not wipe out the data (eg: sync offline manual) + if addr.is_none() { + let _ = vfs::remove_dir_all(&path).await; + } + + // Setup required sub-directories + vfs::create_dir_all(&path).await?; + + Ok(Paths::new_server(path)) +} + /// Spawn a mock server using the given config. pub async fn spawn_with_config( test_id: &str, @@ -230,6 +266,7 @@ pub async fn spawn_with_config( server_id: Option<&str>, config: Option, ) -> Result { + /* let current_dir = std::env::current_dir() .expect("failed to get current working directory"); @@ -251,14 +288,21 @@ pub async fn spawn_with_config( // Setup required sub-directories vfs::create_dir_all(&path).await?; + */ + let paths = server_paths(test_id, server_id, addr).await?; let (tx, rx) = oneshot::channel::(); - let handle = MockServer::launch(addr, path.clone(), tx, config)?; + let handle = MockServer::launch( + addr, + paths.documents_dir().to_owned(), + tx, + config, + )?; let addr = rx.await?; let url = socket_addr_url(&addr); Ok(TestServer { test_id: test_id.to_owned(), - paths: Paths::new_server(path), + paths, // path, origin: url.into(), addr, diff --git a/tests/utils/src/network.rs b/tests/utils/src/network.rs index 0c1181336a..576b5340f4 100644 --- a/tests/utils/src/network.rs +++ b/tests/utils/src/network.rs @@ -24,6 +24,7 @@ use sos_sync::SyncStorage; use sos_vault::Summary; use sos_vfs as vfs; use std::{ + collections::HashMap, path::PathBuf, sync::Arc, time::{Duration, SystemTime}, @@ -199,8 +200,9 @@ pub async fn simulate_device_with_builder( // Sync the local account to create the account on remote let sync_result = owner.sync().await; - println!("{:#?}", sync_result.first_error_ref()); - assert!(sync_result.first_error().is_none()); + if let Some(err) = sync_result.first_error() { + panic!("initial sync failed with error: {:#?}", err); + } (origin, server.account_path(owner.account_id())) } else { @@ -287,7 +289,10 @@ pub async fn assert_local_remote_vaults_eq>( let client = ClientStorage::new_unauthenticated(client_target, &account_id) .await?; - let server = ServerStorage::new(server_target, &account_id).await?; + let shared_folder_events = Arc::new(Mutex::new(HashMap::default())); + let server = + ServerStorage::new(server_target, &account_id, shared_folder_events) + .await?; // Compare vaults for summary in expected_summaries {