From 34e782f65b0efda24273f54a5d14ca6b245b2c8c Mon Sep 17 00:00:00 2001 From: muji Date: Tue, 23 Jul 2024 11:47:51 +0800 Subject: [PATCH] Authenticator sync (#496) * Draft authenticator sync test spec. The merge_folder() function now also returns the decoded WriteEvent events on the client implementation. * Improve authenticator sync test spec. * Diff only respects is_sync_disabled() client-side. Otherwise enabling sync for an authenticator after it was disabled will result in an empty event log on the newly synced device as the server would not return the data. * Update prost/prost-build to v0.13. * Update lock file. * Create failing test case for sign in. When no folder password exists. * Update signature for find_folder_password(). * Tidy error variant. * No folder password test spec now passes. * Switch to nextest, update github action. Halves the execution time for tests. * Tweak checks workflow for PR. * Revert PR trigger for workflow. * Invert is_client to is_server. So that local folders with NO_SYNC flag will be sent to the server but wouldn't be included in the diff sent to clients. * Use LOCAL flag to indicate a folder is local first. * Update no_sync test spec for inverted condition. * Use pending directory for LOCAL folder stubs. So that other client devices can respond to AccountEvent::CreateFolder when a LOCAL folder is created on another device by creating stub files in the pending directory. When a merge is attempted on a folder we first try to promote the folder in case it is pending. This allows the NO_SYNC flag to backup folders to remote server(s) but not share the contents with other devices until the NO_SYNC flag is removed. * Update condition for is_local_folder. * Fix handling of NO_SYNC flag, update test spec. * Do not support remove_local_folder(). * Support authenticator migrate import/export zip archive. * Tidy obsolete constant in test spec. * Move commands to tools command. * Add tools authenticator command. Supports export and import of unencrypted TOTP secrets. * Update lock file. * Bump patch version. * Update lock file. --- .github/workflows/checks.yml | 10 +- Cargo.lock | 291 +++++++++--------- Cargo.toml | 2 +- Makefile.toml | 2 +- .../tests/auto_merge/delete_secrets.rs | 1 + .../tests/local_account/main.rs | 2 - .../network_account/authenticator_sync.rs | 83 +++++ .../network_account/listen_folder_import.rs | 7 +- .../tests/network_account/main.rs | 2 +- .../tests/network_account/no_sync.rs | 13 +- .../network_account/send_folder_import.rs | 7 +- .../identity_login.rs | 2 +- .../integration_tests/tests/sign_in/main.rs | 4 + .../tests/sign_in/no_folder_password.rs | 49 +++ crates/net/Cargo.toml | 6 +- crates/net/src/account/auto_merge.rs | 2 +- crates/net/src/account/network_account.rs | 20 +- crates/net/src/account/sync.rs | 4 + crates/protocol/Cargo.toml | 6 +- crates/protocol/src/error.rs | 4 + crates/protocol/src/sync/folder.rs | 23 +- crates/protocol/src/sync/local_account.rs | 83 +++-- crates/protocol/src/sync/primitives.rs | 46 ++- crates/sdk/Cargo.toml | 2 +- crates/sdk/src/account/account.rs | 97 +++--- crates/sdk/src/account/archive/backup.rs | 6 +- crates/sdk/src/account/convert.rs | 7 +- crates/sdk/src/constants.rs | 8 + crates/sdk/src/error.rs | 31 +- crates/sdk/src/identity/identity.rs | 2 +- crates/sdk/src/identity/identity_folder.rs | 62 ++-- .../sdk/src/migrate/authenticator/export.rs | 74 +++++ .../sdk/src/migrate/authenticator/import.rs | 61 ++++ crates/sdk/src/migrate/authenticator/mod.rs | 19 ++ crates/sdk/src/migrate/error.rs | 4 + crates/sdk/src/migrate/mod.rs | 2 + crates/sdk/src/storage/client.rs | 183 ++++++++--- crates/sdk/src/storage/paths.rs | 23 +- crates/sdk/src/vault/vault.rs | 9 +- .../sdk/tests/authenticator_export_import.rs | 54 ++++ crates/sdk/tests/event_logs.rs | 21 +- crates/server/Cargo.toml | 4 +- crates/server/src/handlers/account.rs | 3 +- crates/server/src/storage/filesystem/sync.rs | 23 +- crates/sos/Cargo.toml | 4 +- crates/sos/src/cli/sos.rs | 69 +---- crates/sos/src/commands/mod.rs | 7 - crates/sos/src/commands/{ => tools}/audit.rs | 0 .../sos/src/commands/tools/authenticator.rs | 113 +++++++ crates/sos/src/commands/{ => tools}/check.rs | 1 + crates/sos/src/commands/{ => tools}/events.rs | 0 .../src/commands/{tools.rs => tools/mod.rs} | 79 ++++- .../commands/{ => tools}/security_report.rs | 0 crates/sos/src/error.rs | 4 + 54 files changed, 1169 insertions(+), 472 deletions(-) create mode 100644 crates/integration_tests/tests/network_account/authenticator_sync.rs rename crates/integration_tests/tests/{local_account => sign_in}/identity_login.rs (97%) create mode 100644 crates/integration_tests/tests/sign_in/main.rs create mode 100644 crates/integration_tests/tests/sign_in/no_folder_password.rs create mode 100644 crates/sdk/src/migrate/authenticator/export.rs create mode 100644 crates/sdk/src/migrate/authenticator/import.rs create mode 100644 crates/sdk/src/migrate/authenticator/mod.rs create mode 100644 crates/sdk/tests/authenticator_export_import.rs rename crates/sos/src/commands/{ => tools}/audit.rs (100%) create mode 100644 crates/sos/src/commands/tools/authenticator.rs rename crates/sos/src/commands/{ => tools}/check.rs (98%) rename crates/sos/src/commands/{ => tools}/events.rs (100%) rename crates/sos/src/commands/{tools.rs => tools/mod.rs} (67%) rename crates/sos/src/commands/{ => tools}/security_report.rs (100%) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4df66c5a3e..f95ddd86c8 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -2,7 +2,7 @@ name: Checks on: workflow_call: - #pull_request: + # pull_request: env: RUST_TEST_TIME_INTEGRATION: "120000,300000" @@ -50,10 +50,10 @@ jobs: security list-keychains -s $HOME/Library/Keychains/sos-mock.keychain-db security list-keychains - - name: Install cargo-make - run: | - cargo install cargo-make + - uses: taiki-e/install-action@v2 + with: + tool: nextest - name: Run tests run: | - cargo make test-lite + cargo nextest run diff --git a/Cargo.lock b/Cargo.lock index f515d142a0..e41655f9b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,7 +259,7 @@ checksum = "9fb4009533e8ff8f1450a5bcbc30f4242a1d34442221f72314bea1f5dc9c7f89" dependencies = [ "clipboard-win", "core-graphics", - "image 0.25.1", + "image 0.25.2", "log", "objc2", "objc2-app-kit", @@ -295,9 +295,9 @@ checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711" [[package]] name = "async-compression" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd066d0b4ef8ecb03a55319dc13aa6910616d0f44008a045bb1835af830abff5" +checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" dependencies = [ "flate2", "futures-core", @@ -320,7 +320,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -342,18 +342,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] name = "async-trait" -version = "0.1.80" +version = "0.1.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -473,7 +473,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -677,17 +677,23 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" -version = "1.6.0" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" +checksum = "a12916984aab3fa6e39d655a33e09c0071eb36d6ab3aea5c2d78551f1df6d952" [[package]] name = "cc" -version = "1.0.104" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b6a57f98764a267ff415d50a25e6e166f3831a5071af4995296ea97d210490" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" [[package]] name = "cesu8" @@ -775,9 +781,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.8" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b3edb18336f4df585bc9aa31dd99c036dfa5dc5e9a2939a722a188f3a8970d" +checksum = "64acc1846d54c1fe936a78dc189c34e28d3f5afc348403f28ecf53660b9b8462" dependencies = [ "clap_builder", "clap_derive", @@ -785,9 +791,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.8" +version = "4.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1c09dd5ada6c6c78075d6fd0da3f90d8080651e2d6cc8eb2f1aaa4034ced708" +checksum = "6fb8393d67ba2e7bfaf28a23458e4e2b543cc73a99595511eb207fdb8aede942" dependencies = [ "anstream", "anstyle", @@ -805,7 +811,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -816,9 +822,9 @@ checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" [[package]] name = "clipboard-win" -version = "5.3.1" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad" +checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892" dependencies = [ "error-code", ] @@ -1095,7 +1101,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -1110,12 +1116,12 @@ dependencies = [ [[package]] name = "darling" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83b2eb4d90d12bdda5ed17de686c2acb4c57914f8f921b8da7e112b5a36f3fe1" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" dependencies = [ - "darling_core 0.20.9", - "darling_macro 0.20.9", + "darling_core 0.20.10", + "darling_macro 0.20.10", ] [[package]] @@ -1134,16 +1140,16 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", "proc-macro2 1.0.86", "quote 1.0.36", "strsim 0.11.1", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -1159,13 +1165,13 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.9" +version = "0.20.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ - "darling_core 0.20.9", + "darling_core 0.20.10", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -1254,10 +1260,10 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d48cda787f839151732d396ac69e3473923d54312c070ee21e9effcaa8ca0b1d" dependencies = [ - "darling 0.20.9", + "darling 0.20.10", "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -1277,7 +1283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "206868b8242f27cecce124c19fd88157fbd0dd334df2587f36417bafbc85097b" dependencies = [ "derive_builder_core 0.20.0", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -1306,7 +1312,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -1397,7 +1403,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -1629,7 +1635,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -1722,7 +1728,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -1942,9 +1948,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", "http", @@ -1983,9 +1989,9 @@ checksum = "91f255a4535024abf7640cb288260811fc14794f62b063652ed349f9a6c2348e" [[package]] name = "hyper" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4fe55fb7a772d59a5ff1dfbff4fe0258d19b89fec4b233e75d35d5d2316badc" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" dependencies = [ "bytes", "futures-channel", @@ -2012,7 +2018,7 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls 0.23.10", + "rustls 0.23.11", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -2050,7 +2056,7 @@ dependencies = [ "serde", "serde_derive", "thiserror", - "toml 0.8.14", + "toml 0.8.15", "unic-langid", ] @@ -2092,7 +2098,7 @@ dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", "strsim 0.10.0", - "syn 2.0.68", + "syn 2.0.72", "unic-langid", ] @@ -2106,7 +2112,7 @@ dependencies = [ "i18n-config", "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -2163,12 +2169,12 @@ dependencies = [ [[package]] name = "image" -version = "0.25.1" +version = "0.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd54d660e773627692c524beaad361aca785a4f9f5730ce91f42aabe5bce3d11" +checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10" dependencies = [ "bytemuck", - "byteorder", + "byteorder-lite", "num-traits", "png", "tiff", @@ -2384,7 +2390,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5e25f9b861a88faa9d272ca4376e1a13c9a37d36de623f013c7bbb0ae2baa1" dependencies = [ "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -2482,7 +2488,7 @@ dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", "regex-syntax 0.8.4", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -2828,7 +2834,7 @@ dependencies = [ "proc-macro2 1.0.86", "proc-macro2-diagnostics", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -2887,7 +2893,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.5.2", + "redox_syscall 0.5.3", "smallvec", "windows-targets 0.52.6", ] @@ -2956,7 +2962,7 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -3059,7 +3065,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ "proc-macro2 1.0.86", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -3154,16 +3160,16 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", "version_check", "yansi 1.0.1", ] [[package]] name = "prost" -version = "0.12.6" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +checksum = "e13db3d3fde688c61e2446b4d843bc27a7e8af269a69440c0308021dc92333cc" dependencies = [ "bytes", "prost-derive", @@ -3171,13 +3177,13 @@ dependencies = [ [[package]] name = "prost-build" -version = "0.12.6" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" +checksum = "5bb182580f71dd070f88d01ce3de9f4da5021db7115d2e1c3605a754153b77c1" dependencies = [ "bytes", "heck 0.5.0", - "itertools 0.12.1", + "itertools 0.13.0", "log", "multimap", "once_cell", @@ -3186,28 +3192,28 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.68", + "syn 2.0.72", "tempfile", ] [[package]] name = "prost-derive" -version = "0.12.6" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +checksum = "18bec9b0adc4eba778b33684b7ba3e7137789434769ee3ce3930463ef904cfca" dependencies = [ "anyhow", - "itertools 0.12.1", + "itertools 0.13.0", "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] name = "prost-types" -version = "0.12.6" +version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +checksum = "cee5168b05f49d4b0ca581206eb14a7b22fafd963efe729ac48eb03266e25cc2" dependencies = [ "prost", ] @@ -3308,7 +3314,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.10", + "rustls 0.23.11", "thiserror", "tokio", "tracing", @@ -3324,7 +3330,7 @@ dependencies = [ "rand", "ring", "rustc-hash", - "rustls 0.23.10", + "rustls 0.23.11", "slab", "thiserror", "tinyvec", @@ -3333,14 +3339,13 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.2" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9096629c45860fc7fb143e125eb826b5e721e10be3263160c7d60ca832cf8c46" +checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" dependencies = [ "libc", "once_cell", "socket2", - "tracing", "windows-sys 0.52.0", ] @@ -3425,9 +3430,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" dependencies = [ "bitflags 2.6.0", ] @@ -3500,7 +3505,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.10", + "rustls 0.23.11", "rustls-pemfile", "rustls-pki-types", "serde", @@ -3575,9 +3580,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "8.4.0" +version = "8.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19549741604902eb99a7ed0ee177a0663ee1eda51a29f71401f166e47e77806a" +checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -3586,22 +3591,22 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "8.4.0" +version = "8.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb9f96e283ec64401f30d3df8ee2aaeb2561f34c824381efa24a35f79bf40ee4" +checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", "rust-embed-utils", - "syn 2.0.68", + "syn 2.0.72", "walkdir", ] [[package]] name = "rust-embed-utils" -version = "8.4.0" +version = "8.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38c74a686185620830701348de757fd36bef4aa9680fd23c49fc539ddcc1af32" +checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" dependencies = [ "sha2", "walkdir", @@ -3675,14 +3680,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.10" +version = "0.23.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +checksum = "4828ea528154ae444e5a642dbb7d5623354030dc9822b83fd9bb79683c7399d0" dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.102.5", + "rustls-webpki 0.102.6", "subtle", "zeroize", ] @@ -3728,9 +3733,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.102.5" +version = "0.102.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a6fccd794a42c2c105b513a2f62bc3fd8f3ba57a4593677ceb0bd035164d78" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" dependencies = [ "ring", "rustls-pki-types", @@ -3773,7 +3778,7 @@ checksum = "e5af959c8bf6af1aff6d2b463a57f71aae53d1332da58419e30ad8dc7011d951" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -3872,9 +3877,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.6.0", "core-foundation", @@ -3885,9 +3890,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -3919,22 +3924,22 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.203" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -3981,9 +3986,9 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.8.3" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e73139bc5ec2d45e6c5fd85be5a46949c1c39a4c18e56915f5eb4c12f975e377" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" dependencies = [ "base64 0.22.1", "chrono", @@ -3999,14 +4004,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.8.3" +version = "3.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b80d3d6b56b64335c0180e5ffde23b3c5e08c14c585b51a15bd0e95393f46703" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" dependencies = [ - "darling 0.20.9", + "darling 0.20.10", "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -4151,7 +4156,7 @@ dependencies = [ [[package]] name = "sos" -version = "0.14.6" +version = "0.14.7" dependencies = [ "arboard", "async-recursion", @@ -4179,7 +4184,7 @@ dependencies = [ "terminal-banner", "thiserror", "tokio", - "toml 0.8.14", + "toml 0.8.15", "tracing", "tracing-subscriber", "unicode-width", @@ -4255,7 +4260,7 @@ dependencies = [ [[package]] name = "sos-net" -version = "0.14.6" +version = "0.14.7" dependencies = [ "anyhow", "async-recursion", @@ -4295,7 +4300,7 @@ dependencies = [ [[package]] name = "sos-protocol" -version = "0.14.6" +version = "0.14.7" dependencies = [ "anyhow", "async-trait", @@ -4317,7 +4322,7 @@ dependencies = [ [[package]] name = "sos-sdk" -version = "0.14.6" +version = "0.14.7" dependencies = [ "aes-gcm", "age", @@ -4392,7 +4397,7 @@ dependencies = [ [[package]] name = "sos-server" -version = "0.14.6" +version = "0.14.7" dependencies = [ "async-trait", "axum", @@ -4416,7 +4421,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", - "toml 0.8.14", + "toml 0.8.15", "tower", "tower-http", "tracing", @@ -4546,9 +4551,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.68" +version = "2.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "901fa70d88b9d6c98022e23b4136f9f3e54e4662c3bc1bd1d84a42a9a0f0c1e9" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", @@ -4630,22 +4635,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.61" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -4743,9 +4748,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.6.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c55115c6fbe2d2bef26eb09ad74bde02d8255476fc0c7b515ef09fbb35742d82" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" dependencies = [ "tinyvec_macros", ] @@ -4758,9 +4763,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.38.0" +version = "1.38.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a" +checksum = "eb2caba9f80616f438e09748d5acda951967e1ea58508ef53d9c6402485a46df" dependencies = [ "backtrace", "bytes", @@ -4783,7 +4788,7 @@ checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -4802,7 +4807,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.10", + "rustls 0.23.11", "rustls-pki-types", "tokio", ] @@ -4838,7 +4843,7 @@ checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" dependencies = [ "futures-util", "log", - "rustls 0.23.10", + "rustls 0.23.11", "rustls-native-certs", "rustls-pki-types", "tokio", @@ -4871,14 +4876,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.8.14" +version = "0.8.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335" +checksum = "ac2caab0bf757388c6c0ae23b3293fdb463fee59434529014f85e3263b995c28" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit 0.22.14", + "toml_edit 0.22.16", ] [[package]] @@ -4903,15 +4908,15 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.14" +version = "0.22.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38" +checksum = "278f3d518e152219c994ce877758516bca5e118eaed6996192a774fb9fbf0788" dependencies = [ "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.13", + "winnow 0.6.15", ] [[package]] @@ -5009,7 +5014,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -5102,7 +5107,7 @@ dependencies = [ "httparse", "log", "rand", - "rustls 0.23.10", + "rustls 0.23.11", "rustls-pki-types", "sha1", "thiserror", @@ -5308,7 +5313,7 @@ dependencies = [ "proc-macro-error", "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", "uuid", ] @@ -5326,9 +5331,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.9.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de17fd2f7da591098415cff336e12965a28061ddace43b59cb3c430179c9439" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", "serde", @@ -5433,7 +5438,7 @@ dependencies = [ "once_cell", "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", "wasm-bindgen-shared", ] @@ -5467,7 +5472,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5792,9 +5797,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.13" +version = "0.6.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" +checksum = "557404e450152cd6795bb558bca69e43c585055f4606e3bcae5894fc6dac9ba0" dependencies = [ "memchr", ] @@ -5882,7 +5887,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] @@ -5902,7 +5907,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.68", + "syn 2.0.72", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 67dc53124f..3c0aee1471 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ bitflags = { version = "2", features = ["serde"] } enum-iterator = "2" file-guard = "0.2" tempfile = "3.5" -prost = "0.12.6" +prost = "0.13" clap = { version = "4.3.19", features = ["derive", "wrap_help", "env"] } colored = "2" diff --git a/Makefile.toml b/Makefile.toml index 2fe78b630c..0cafadf0e1 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -101,7 +101,7 @@ dependencies = ["clean-cli"] [tasks.test-lite] command = "cargo" -args = ["test", "--all"] +args = ["nextest", "run"] dependencies = ["clean-cli"] [tasks.genhtml] diff --git a/crates/integration_tests/tests/auto_merge/delete_secrets.rs b/crates/integration_tests/tests/auto_merge/delete_secrets.rs index f6c52f5be4..6d34947cef 100644 --- a/crates/integration_tests/tests/auto_merge/delete_secrets.rs +++ b/crates/integration_tests/tests/auto_merge/delete_secrets.rs @@ -43,6 +43,7 @@ async fn auto_merge_delete_secrets() -> Result<()> { .owner .create_secret(meta, secret, Default::default()) .await?; + println!("{:#?}", result2.sync_error); assert!(result2.sync_error.is_none()); // Oh no, the server has gone offline! diff --git a/crates/integration_tests/tests/local_account/main.rs b/crates/integration_tests/tests/local_account/main.rs index 62a8e6d9b9..485e66333a 100644 --- a/crates/integration_tests/tests/local_account/main.rs +++ b/crates/integration_tests/tests/local_account/main.rs @@ -5,7 +5,6 @@ mod contacts; mod custom_fields; mod external_files; mod folder_lifecycle; -mod identity_login; mod migrate_export; mod migrate_import; mod move_secret; @@ -15,5 +14,4 @@ mod security_report; mod time_travel; mod update_file; -#[cfg(not(target_arch = "wasm32"))] pub use sos_test_utils as test_utils; diff --git a/crates/integration_tests/tests/network_account/authenticator_sync.rs b/crates/integration_tests/tests/network_account/authenticator_sync.rs new file mode 100644 index 0000000000..aadb3c8b93 --- /dev/null +++ b/crates/integration_tests/tests/network_account/authenticator_sync.rs @@ -0,0 +1,83 @@ +use crate::test_utils::{mock, simulate_device, spawn, teardown}; +use anyhow::Result; +use sos_net::{sdk::prelude::*, RemoteSync}; + +/// Tests syncing an authenticator folder after +/// disabling the NO_SYNC flag. +#[tokio::test] +async fn network_authenticator_sync() -> Result<()> { + const TEST_ID: &str = "authenticator_sync"; + // crate::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 + let mut desktop = simulate_device(TEST_ID, 2, Some(&server)).await?; + let mut mobile = desktop.connect(1, None).await?; + + // Create folder with AUTHENTICATOR | LOCAL | NO_SYNC flags + let options = NewFolderOptions { + flags: VaultFlags::AUTHENTICATOR + | VaultFlags::LOCAL + | VaultFlags::NO_SYNC, + ..Default::default() + }; + let FolderCreate { folder, .. } = mobile + .owner + .create_folder(TEST_ID.to_owned(), options) + .await?; + + // Create a TOTP secret in the new authenticator folder + let (meta, secret) = mock::totp(TEST_ID); + let SecretChange { id, .. } = mobile + .owner + .create_secret(meta, secret, folder.clone().into()) + .await?; + + // Desktop syncs before NO_SYNC flag has been removed + let sync_error = desktop.owner.sync().await; + assert!(sync_error.is_none()); + + // Try to read the secret but can't as the server + // will not send events when NO_SYNC is set + assert!(desktop + .owner + .read_secret(&id, Some(folder.clone())) + .await + .is_err()); + + // Update the folder with new flags so it can be synced + mobile + .owner + .update_folder_flags( + &folder, + VaultFlags::AUTHENTICATOR | VaultFlags::LOCAL, + ) + .await?; + + // Sync the account on the desktop device + let sync_error = desktop.owner.sync().await; + assert!(sync_error.is_none()); + + // Should be able to read the TOTP on the synced desktop device + let (data, _) = + desktop.owner.read_secret(&id, Some(folder.clone())).await?; + assert_eq!(TEST_ID, data.meta().label()); + + // Desktop now has an auth folder + let auth_folder = desktop.owner.authenticator_folder().await; + assert!(auth_folder.is_some()); + + // Auth folder flags should be updated and correct + let auth_folder = auth_folder.unwrap(); + assert!(auth_folder.flags().is_authenticator()); + assert!(!auth_folder.flags().is_sync_disabled()); + + desktop.owner.sign_out().await?; + mobile.owner.sign_out().await?; + + teardown(TEST_ID).await; + + Ok(()) +} diff --git a/crates/integration_tests/tests/network_account/listen_folder_import.rs b/crates/integration_tests/tests/network_account/listen_folder_import.rs index 729a36296b..a438cb0659 100644 --- a/crates/integration_tests/tests/network_account/listen_folder_import.rs +++ b/crates/integration_tests/tests/network_account/listen_folder_import.rs @@ -48,8 +48,11 @@ async fn network_sync_listen_folder_import() -> Result<()> { }; // Need the vault passphrase to import a buffer - let vault_passphrase = - device1.owner.find_folder_password(new_folder.id()).await?; + let vault_passphrase = device1 + .owner + .find_folder_password(new_folder.id()) + .await? + .unwrap(); // Make a change so we can assert on the new value vault.set_name("sync_folder_imported".to_string()); diff --git a/crates/integration_tests/tests/network_account/main.rs b/crates/integration_tests/tests/network_account/main.rs index 5f810f3058..38e1f431d4 100644 --- a/crates/integration_tests/tests/network_account/main.rs +++ b/crates/integration_tests/tests/network_account/main.rs @@ -1,4 +1,5 @@ mod archive_unarchive; +mod authenticator_sync; mod create_account; mod delete_account; mod folder_description; @@ -43,5 +44,4 @@ mod change_folder_password; mod compact_account; mod compact_folder; -#[cfg(not(target_arch = "wasm32"))] pub use sos_test_utils as test_utils; diff --git a/crates/integration_tests/tests/network_account/no_sync.rs b/crates/integration_tests/tests/network_account/no_sync.rs index 33ecaa9a79..5867985847 100644 --- a/crates/integration_tests/tests/network_account/no_sync.rs +++ b/crates/integration_tests/tests/network_account/no_sync.rs @@ -75,7 +75,7 @@ async fn network_no_sync_update_account() -> Result<()> { let origin = server.origin.clone(); // Prepare mock device - let mut device = simulate_device(TEST_ID, 1, Some(&server)).await?; + let mut device = simulate_device(TEST_ID, 2, Some(&server)).await?; // Create folder with AUTHENTICATOR flag let options = NewFolderOptions { @@ -103,9 +103,11 @@ async fn network_no_sync_update_account() -> Result<()> { // to the new folder that has now been marked with NO_SYNC assert!(device.owner.sync().await.is_none()); - // Local should be ahead of remote now as it has - // the extra event for modifying the flags when - // update_folder_flags() was called + // Local should be equal with remote now. + // + // The folder is set to NO_SYNC which means it should not be + // shared with other devices but we still want a copy on server(s) + // for redundancy. let local_status = device.owner.sync_status().await?; let bridge = device.owner.remove_server(&origin).await?.unwrap(); let remote_status = bridge.client().sync_status().await?; @@ -115,8 +117,7 @@ async fn network_no_sync_update_account() -> Result<()> { let local_proof = &local_folder.1; let remote_proof = &remote_folder.1; - assert_ne!(local_proof.root, remote_proof.root); - assert!(local_proof.length > remote_proof.length); + assert_eq!(local_proof.root, remote_proof.root); device.owner.sign_out().await?; teardown(TEST_ID).await; diff --git a/crates/integration_tests/tests/network_account/send_folder_import.rs b/crates/integration_tests/tests/network_account/send_folder_import.rs index 29091415f0..6565c9f4a7 100644 --- a/crates/integration_tests/tests/network_account/send_folder_import.rs +++ b/crates/integration_tests/tests/network_account/send_folder_import.rs @@ -44,8 +44,11 @@ async fn network_sync_folder_import() -> Result<()> { }; // Need the vault passphrase to import a buffer - let vault_passphrase = - device.owner.find_folder_password(new_folder.id()).await?; + let vault_passphrase = device + .owner + .find_folder_password(new_folder.id()) + .await? + .unwrap(); // Make a change so we can assert on the new value vault.set_name("sync_folder_imported".to_string()); diff --git a/crates/integration_tests/tests/local_account/identity_login.rs b/crates/integration_tests/tests/sign_in/identity_login.rs similarity index 97% rename from crates/integration_tests/tests/local_account/identity_login.rs rename to crates/integration_tests/tests/sign_in/identity_login.rs index 4ec074864b..6ae949d5a8 100644 --- a/crates/integration_tests/tests/local_account/identity_login.rs +++ b/crates/integration_tests/tests/sign_in/identity_login.rs @@ -5,7 +5,7 @@ use sos_net::sdk::{prelude::*, vfs}; /// Tests creating an identity vault and logging in /// with the new vault and managing delegated passwords. #[tokio::test] -async fn local_identity_login() -> Result<()> { +async fn sign_in_identity_login() -> Result<()> { const TEST_ID: &str = "identity_login"; //crate::test_utils::init_tracing(); diff --git a/crates/integration_tests/tests/sign_in/main.rs b/crates/integration_tests/tests/sign_in/main.rs new file mode 100644 index 0000000000..3ae06d4d20 --- /dev/null +++ b/crates/integration_tests/tests/sign_in/main.rs @@ -0,0 +1,4 @@ +mod identity_login; +mod no_folder_password; + +pub use sos_test_utils as test_utils; diff --git a/crates/integration_tests/tests/sign_in/no_folder_password.rs b/crates/integration_tests/tests/sign_in/no_folder_password.rs new file mode 100644 index 0000000000..8e728f7a6e --- /dev/null +++ b/crates/integration_tests/tests/sign_in/no_folder_password.rs @@ -0,0 +1,49 @@ +use crate::test_utils::{setup, teardown}; +use anyhow::Result; +use sos_net::sdk::prelude::*; + +/// Tests sign in when a folder password is missing. +#[tokio::test] +async fn sign_in_no_folder_password() -> Result<()> { + const TEST_ID: &str = "no_folder_password"; + // crate::test_utils::init_tracing(); + + let mut dirs = setup(TEST_ID, 1).await?; + let data_dir = dirs.clients.remove(0); + + let account_name = TEST_ID.to_string(); + let (password, _) = generate_passphrase()?; + + let mut account = LocalAccount::new_account( + account_name.clone(), + password.clone(), + Some(data_dir.clone()), + ) + .await?; + + let key: AccessKey = password.clone().into(); + account.sign_in(&key).await?; + + // Create folder + let FolderCreate { folder, .. } = account + .create_folder(TEST_ID.to_owned(), Default::default()) + .await?; + + // Remove the folder password + account + .user_mut()? + .remove_folder_password(folder.id()) + .await?; + + account.sign_out().await?; + + // Should be able to sign in when the folder password + // is missing so that we can still access other folders + // that can be unlocked + account.sign_in(&key).await?; + + account.sign_out().await?; + teardown(TEST_ID).await; + + Ok(()) +} diff --git a/crates/net/Cargo.toml b/crates/net/Cargo.toml index ddc29ea1ae..4cd16bdaef 100644 --- a/crates/net/Cargo.toml +++ b/crates/net/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sos-net" -version = "0.14.6" +version = "0.14.7" edition = "2021" description = "Networking library for the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" @@ -79,11 +79,11 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync"] } tokio-tungstenite = { version = "0.23", features = ["rustls-tls-native-roots"] , optional = true} [dependencies.sos-sdk] -version = "0.14.6" +version = "0.14.7" path = "../sdk" [dependencies.sos-protocol] -version = "0.14.6" +version = "0.14.7" path = "../protocol" features = ["account"] diff --git a/crates/net/src/account/auto_merge.rs b/crates/net/src/account/auto_merge.rs index 3fe82af4ce..50ee83eda5 100644 --- a/crates/net/src/account/auto_merge.rs +++ b/crates/net/src/account/auto_merge.rs @@ -501,7 +501,7 @@ impl RemoteBridge { checkpoint: proof, patch, }; - account.merge_folder(id, diff, &mut outcome).await? + account.merge_folder(id, diff, &mut outcome).await?.0 } } }; diff --git a/crates/net/src/account/network_account.rs b/crates/net/src/account/network_account.rs index 367600932b..c7fa2f3912 100644 --- a/crates/net/src/account/network_account.rs +++ b/crates/net/src/account/network_account.rs @@ -677,7 +677,7 @@ impl Account for NetworkAccount { async fn find_folder_password( &self, folder_id: &VaultId, - ) -> Result { + ) -> Result> { let account = self.account.lock().await; Ok(account.find_folder_password(folder_id).await?) } @@ -1549,24 +1549,6 @@ impl Account for NetworkAccount { Ok(result) } - async fn remove_local_folder( - &mut self, - summary: &Summary, - ) -> Result> { - let result = { - let mut account = self.account.lock().await; - account.remove_local_folder(summary).await? - }; - - let result = FolderDelete { - events: result.events, - commit_state: result.commit_state, - sync_error: None, - }; - - Ok(result) - } - #[cfg(feature = "contacts")] async fn load_avatar( &mut self, diff --git a/crates/net/src/account/sync.rs b/crates/net/src/account/sync.rs index 8fa25bab3b..a7bf5cbcfe 100644 --- a/crates/net/src/account/sync.rs +++ b/crates/net/src/account/sync.rs @@ -223,6 +223,10 @@ impl StorageEventLogs for NetworkAccount { #[async_trait] impl SyncStorage for NetworkAccount { + fn is_client_storage(&self) -> bool { + true + } + async fn sync_status(&self) -> Result { let account = self.account.lock().await; account.sync_status().await diff --git a/crates/protocol/Cargo.toml b/crates/protocol/Cargo.toml index f0187c8190..e92fb3590c 100644 --- a/crates/protocol/Cargo.toml +++ b/crates/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sos-protocol" -version = "0.14.6" +version = "0.14.7" edition = "2021" description = "Networking and sync protocol types for the Save Our Secrets SDK." homepage = "https://saveoursecrets.com" @@ -33,7 +33,7 @@ prost.workspace = true tokio = { version = "1", features = ["rt", "macros"] } [dependencies.sos-sdk] -version = "0.14.6" +version = "0.14.7" path = "../sdk" [dev-dependencies] @@ -41,5 +41,5 @@ anyhow = "1" [build-dependencies] rustc_version = "0.4.0" -prost-build = "0.12.6" +prost-build = "0.13" protoc-bin-vendored = "3" diff --git a/crates/protocol/src/error.rs b/crates/protocol/src/error.rs index ba01806e24..34b959f3f8 100644 --- a/crates/protocol/src/error.rs +++ b/crates/protocol/src/error.rs @@ -21,6 +21,10 @@ pub enum Error { #[error(transparent)] ProtoBufDecode(#[from] prost::DecodeError), + /// Error generated by the protobuf library when converting enums. + #[error(transparent)] + ProtoEnum(#[from] prost::UnknownEnumValue), + /// Error generated by the SDK library. #[error(transparent)] Sdk(#[from] crate::sdk::Error), diff --git a/crates/protocol/src/sync/folder.rs b/crates/protocol/src/sync/folder.rs index b429315863..ab35a33a57 100644 --- a/crates/protocol/src/sync/folder.rs +++ b/crates/protocol/src/sync/folder.rs @@ -26,7 +26,10 @@ use crate::{ #[async_trait] pub(crate) trait IdentityFolderMerge { /// Checked merge. - async fn merge(&mut self, diff: &FolderDiff) -> Result; + async fn merge( + &mut self, + diff: &FolderDiff, + ) -> Result<(CheckedPatch, Vec)>; /// Unchecked merge. async fn force_merge(&mut self, diff: &FolderDiff) -> Result<()>; @@ -40,7 +43,7 @@ pub(crate) trait FolderMerge { &mut self, diff: &FolderDiff, options: FolderMergeOptions<'a>, - ) -> Result; + ) -> Result<(CheckedPatch, Vec)>; /// Unchecked merge. async fn force_merge(&mut self, diff: &FolderDiff) -> Result<()>; @@ -54,7 +57,10 @@ where W: AsyncWrite + Unpin + Send + Sync, D: Clone + Send + Sync, { - async fn merge(&mut self, diff: &FolderDiff) -> Result { + async fn merge( + &mut self, + diff: &FolderDiff, + ) -> Result<(CheckedPatch, Vec)> { let id = *self.folder_id(); let index = &mut self.index; @@ -80,7 +86,8 @@ where &mut self, diff: &FolderDiff, mut options: FolderMergeOptions<'a>, - ) -> Result { + ) -> Result<(CheckedPatch, Vec)> { + let mut events = Vec::new(); let checked_patch = { let event_log = self.event_log(); let mut event_log = event_log.write().await; @@ -94,9 +101,7 @@ where let event = record.decode_event::().await?; tracing::debug!(event_kind = %event.event_kind()); match &event { - WriteEvent::Noop => { - tracing::error!("merge got noop event"); - } + WriteEvent::Noop => unreachable!(), WriteEvent::CreateVault(_) => { tracing::warn!("merge got create vault event"); } @@ -241,10 +246,12 @@ where } } } + + events.push(event); } } - Ok(checked_patch) + Ok((checked_patch, events)) } async fn force_merge(&mut self, diff: &FolderDiff) -> Result<()> { diff --git a/crates/protocol/src/sync/local_account.rs b/crates/protocol/src/sync/local_account.rs index b9ed8f8d85..29fd90272c 100644 --- a/crates/protocol/src/sync/local_account.rs +++ b/crates/protocol/src/sync/local_account.rs @@ -1,8 +1,6 @@ //! Implements merging into a local account. //! -use std::collections::HashSet; - // Ideally we want this code to be in the `sos-net` // crate but we also need to share some traits with the // server so we have to implement here otherwise we @@ -14,7 +12,7 @@ use crate::{ decode, events::{ AccountDiff, AccountEvent, CheckedPatch, EventLogExt, FolderDiff, - LogEvent, + LogEvent, WriteEvent, }, storage::StorageEventLogs, vault::{Vault, VaultId}, @@ -25,6 +23,7 @@ use crate::{ }; use async_trait::async_trait; use indexmap::IndexMap; +use std::collections::HashSet; use crate::sdk::events::{DeviceDiff, DeviceReducer}; @@ -176,7 +175,7 @@ impl Merge for LocalAccount { "identity", ); - let checked_patch = + let (checked_patch, _) = self.user_mut()?.identity_mut()?.merge(&diff).await?; if let CheckedPatch::Success(_) = &checked_patch { @@ -249,7 +248,7 @@ impl Merge for LocalAccount { // in the same sequence of events then the folder // password won't exist after merging the identity // events so we need to skip the operation. - if let Ok(key) = self + if let Ok(Some(key)) = self .user()? .identity()? .find_folder_password(id) @@ -349,9 +348,6 @@ impl Merge for LocalAccount { outcome.changes += diff.patch.len() as u64; outcome.tracked.device = TrackedChanges::new_device_records(&diff.patch).await?; - } else { - // FIXME: handle conflict situation - println!("todo! device patch could not be merged"); } Ok(checked_patch) @@ -426,31 +422,47 @@ impl Merge for LocalAccount { folder_id: &VaultId, diff: FolderDiff, outcome: &mut MergeOutcome, - ) -> Result { + ) -> Result<(CheckedPatch, Vec)> { let len = diff.patch.len() as u64; - let storage = self.storage().await?; - let mut storage = storage.write().await; + let (checked_patch, events) = { + let storage = self.storage().await?; + let mut storage = storage.write().await; - #[cfg(feature = "search")] - let search = { - let index = storage.index.as_ref().ok_or(Error::NoSearchIndex)?; - index.search() - }; + #[cfg(feature = "search")] + let search = { + let index = + storage.index.as_ref().ok_or(Error::NoSearchIndex)?; + index.search() + }; - tracing::debug!( - folder_id = %folder_id, - checkpoint = ?diff.checkpoint, - num_events = len, - "folder", - ); + tracing::debug!( + folder_id = %folder_id, + checkpoint = ?diff.checkpoint, + num_events = len, + "folder", + ); - let folder = storage - .cache_mut() - .get_mut(folder_id) - .ok_or_else(|| Error::CacheNotAvailable(*folder_id))?; + // Try to promote a pending folder when we receive + // events for a folder. + // + // Relies on the server never including events when + // the NO_SYNC flag has been set. + let promoted = + storage.try_promote_pending_folder(folder_id).await?; + if promoted { + let key = self + .find_folder_password(folder_id) + .await? + .ok_or(Error::NoFolderPassword(*folder_id))?; + storage.unlock_folder(folder_id, &key).await?; + } + + let folder = storage + .cache_mut() + .get_mut(folder_id) + .ok_or_else(|| Error::CacheNotAvailable(*folder_id))?; - let checked_patch = { #[cfg(feature = "search")] { let mut search = search.write().await; @@ -477,6 +489,17 @@ impl Merge for LocalAccount { }; if let CheckedPatch::Success(_) = &checked_patch { + let flags_changed = events + .iter() + .find(|e| matches!(e, WriteEvent::SetVaultFlags(_))) + .is_some(); + + // If the flags changed ensure the in-memory summaries + // are up to date + if flags_changed { + self.load_folders().await?; + } + outcome.changes += len; outcome.tracked.add_tracked_folder_changes( folder_id, @@ -484,7 +507,7 @@ impl Merge for LocalAccount { ); } - Ok(checked_patch) + Ok((checked_patch, events)) } async fn compare_folder( @@ -507,6 +530,10 @@ impl Merge for LocalAccount { #[async_trait] impl SyncStorage for LocalAccount { + fn is_client_storage(&self) -> bool { + true + } + async fn sync_status(&self) -> Result { // NOTE: the order for computing the cumulative // NOTE: root hash must be identical to the logic diff --git a/crates/protocol/src/sync/primitives.rs b/crates/protocol/src/sync/primitives.rs index 0e13a62548..f31baf2583 100644 --- a/crates/protocol/src/sync/primitives.rs +++ b/crates/protocol/src/sync/primitives.rs @@ -1,7 +1,9 @@ //! Synchronization types that are used internally. use crate::sdk::{ commit::{CommitState, Comparison}, - events::{AccountDiff, CheckedPatch, EventLogExt, FolderDiff}, + events::{ + AccountDiff, CheckedPatch, EventLogExt, FolderDiff, WriteEvent, + }, storage::StorageEventLogs, vault::VaultId, Error, Result, @@ -83,6 +85,18 @@ pub(crate) enum FolderMergeOptions<'a> { Search(VaultId, &'a mut crate::sdk::storage::search::SearchIndex), } +/* +impl FolderMergeOptions<'_> { + /// Folder identifier. + pub fn folder_id(&self) -> &VaultId { + match self { + Self::Urn(id, _) => id, + Self::Search(id, _) => id, + } + } +} +*/ + /// Information about possible conflicts. #[derive(Debug, Default, Eq, PartialEq)] pub struct MaybeConflict { @@ -385,16 +399,7 @@ impl SyncComparison { _ => {} } - let storage_folders = storage.folder_details().await?; for (id, folder) in &self.folders { - if let Some(folder) = - storage_folders.iter().find(|s| s.id() == id) - { - if folder.flags().is_sync_disabled() { - continue; - } - } - let commit_state = self .remote_status .folders @@ -463,6 +468,9 @@ impl SyncComparison { /// Storage implementations that can synchronize. #[async_trait] pub trait SyncStorage: StorageEventLogs { + /// Determine if this is client-side storage. + fn is_client_storage(&self) -> bool; + /// Get the sync status. async fn sync_status(&self) -> Result; @@ -627,7 +635,7 @@ pub trait Merge { folder_id: &VaultId, diff: FolderDiff, outcome: &mut MergeOutcome, - ) -> Result; + ) -> Result<(CheckedPatch, Vec)>; /// Compare folder events. async fn compare_folder( @@ -771,6 +779,20 @@ pub async fn diff( }; let needs_sync = comparison.needs_sync(); - let diff = comparison.diff(storage).await?; + let mut diff = comparison.diff(storage).await?; + + let is_server = !storage.is_client_storage(); + if is_server { + let storage_folders = storage.folder_details().await?; + diff.folders.retain(|k, _| { + if let Some(folder) = storage_folders.iter().find(|s| s.id() == k) + { + !folder.flags().is_sync_disabled() + } else { + true + } + }); + } + Ok((needs_sync, comparison.local_status, diff)) } diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 62749baeb8..2c0f343732 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sos-sdk" -version = "0.14.6" +version = "0.14.7" edition = "2021" description = "Distributed, encrypted database for private secrets." homepage = "https://saveoursecrets.com" diff --git a/crates/sdk/src/account/account.rs b/crates/sdk/src/account/account.rs index 8f65c739ae..6bdbbfbe7f 100644 --- a/crates/sdk/src/account/account.rs +++ b/crates/sdk/src/account/account.rs @@ -286,7 +286,7 @@ pub trait Account { async fn find_folder_password( &self, folder_id: &VaultId, - ) -> std::result::Result; + ) -> std::result::Result, Self::Error>; /// Generate the password for a folder. async fn generate_folder_password( @@ -704,18 +704,6 @@ pub trait Account { summary: &Summary, ) -> std::result::Result, Self::Error>; - /// Remove a local folder. - /// - /// Does not log the account event so the folder will not - /// be removed from other devices. - /// - /// Useful for certain folders like an authenticator - /// that requires special handling. - async fn remove_local_folder( - &mut self, - summary: &Summary, - ) -> std::result::Result, Self::Error>; - /// Try to load an avatar JPEG image for a contact. /// /// Looks in the current open folder if no specified folder is given. @@ -1033,7 +1021,11 @@ impl LocalAccount { use crate::passwd::ChangePassword; let paths = self.paths().clone(); // Get the current vault passphrase from the identity vault - let current_key = self.user()?.find_folder_password(vault_id).await?; + let current_key = self + .user()? + .find_folder_password(vault_id) + .await? + .ok_or(Error::NoFolderPassword(*vault_id))?; // Find the local vault for the account let (vault, _) = Identity::load_local_vault(&paths, vault_id).await?; @@ -1326,10 +1318,15 @@ impl LocalAccount { let folders = reader.list_folders(); let mut keys = HashMap::new(); for folder in folders { - keys.insert( - folder.clone(), - self.user()?.find_folder_password(folder.id()).await?, - ); + if let Some(key) = + self.user()?.find_folder_password(folder.id()).await? + { + keys.insert(folder.clone(), key); + } else { + tracing::warn!( + folder_id = %folder.id(), + "folder_keys::no_folder_key"); + } } Ok(FolderKeys(keys)) } @@ -1633,8 +1630,8 @@ impl Account for LocalAccount { async fn find_folder_password( &self, folder_id: &VaultId, - ) -> Result { - Ok(self.user()?.find_folder_password(folder_id).await?) + ) -> Result> { + self.user()?.find_folder_password(folder_id).await } async fn generate_folder_password(&self) -> Result { @@ -1940,7 +1937,11 @@ impl Account for LocalAccount { &mut self, summary: &Summary, ) -> Result<(AccountEvent, u64, u64)> { - let key = self.user()?.find_folder_password(summary.id()).await?; + let key = self + .user()? + .find_folder_password(summary.id()) + .await? + .ok_or(Error::NoFolderPassword(*summary.id()))?; let (event, old_size, new_size) = { let storage = self.storage().await?; @@ -1958,8 +1959,11 @@ impl Account for LocalAccount { ) -> Result<()> { self.authenticated.as_ref().ok_or(Error::NotAuthenticated)?; - let current_key = - self.user()?.find_folder_password(folder.id()).await?; + let current_key = self + .user()? + .find_folder_password(folder.id()) + .await? + .ok_or(Error::NoFolderPassword(*folder.id()))?; let vault = { let storage = self.storage().await?; @@ -1998,7 +2002,11 @@ impl Account for LocalAccount { .get(summary.id()) .ok_or_else(|| Error::CacheNotAvailable(*summary.id()))?; - let key = self.user()?.find_folder_password(summary.id()).await?; + let key = self + .user()? + .find_folder_password(summary.id()) + .await? + .ok_or(Error::NoFolderPassword(*summary.id()))?; let event_log = folder.event_log(); let log_file = event_log.read().await; @@ -2726,33 +2734,6 @@ impl Account for LocalAccount { }) } - async fn remove_local_folder( - &mut self, - summary: &Summary, - ) -> Result> { - let options = AccessOptions { - folder: Some(summary.clone()), - ..Default::default() - }; - let (summary, commit_state) = - self.compute_folder_state(&options).await?; - - let events = { - let storage = self.storage().await?; - let mut writer = storage.write().await; - writer.remove_local_folder(&summary).await? - }; - self.user_mut()? - .remove_folder_password(summary.id()) - .await?; - - Ok(FolderDelete { - events, - commit_state, - sync_error: None, - }) - } - #[cfg(feature = "contacts")] async fn load_avatar( &mut self, @@ -2822,8 +2803,11 @@ impl Account for LocalAccount { .await .ok_or_else(|| Error::NoContactsFolder)?; - let contacts_passphrase = - self.user()?.find_folder_password(contacts.id()).await?; + let contacts_passphrase = self + .user()? + .find_folder_password(contacts.id()) + .await? + .ok_or(Error::NoFolderPassword(*contacts.id()))?; let (vault, _) = Identity::load_local_vault(&self.paths, contacts.id()).await?; let mut keeper = Gatekeeper::new(vault); @@ -2936,8 +2920,11 @@ impl Account for LocalAccount { Identity::load_local_vault(&*paths, summary.id()) .await .map_err(Box::from)?; - let vault_passphrase = - self.user()?.find_folder_password(summary.id()).await?; + let vault_passphrase = self + .user()? + .find_folder_password(summary.id()) + .await? + .ok_or(Error::NoFolderPassword(*summary.id()))?; let mut keeper = Gatekeeper::new(vault); keeper.unlock(&vault_passphrase).await?; diff --git a/crates/sdk/src/account/archive/backup.rs b/crates/sdk/src/account/archive/backup.rs index 16a22852fb..422150d4ee 100644 --- a/crates/sdk/src/account/archive/backup.rs +++ b/crates/sdk/src/account/archive/backup.rs @@ -527,8 +527,10 @@ impl AccountBackup { // Use the delegated passwords for the folders // that were restored for (_, vault) in vaults { - let vault_passphrase = - restored_user.find_folder_password(vault.id()).await?; + let vault_passphrase = restored_user + .find_folder_password(vault.id()) + .await? + .ok_or(Error::NoFolderPassword(*vault.id()))?; user.save_folder_password(vault.id(), vault_passphrase) .await?; diff --git a/crates/sdk/src/account/convert.rs b/crates/sdk/src/account/convert.rs index 555b8b086e..4a6c243994 100644 --- a/crates/sdk/src/account/convert.rs +++ b/crates/sdk/src/account/convert.rs @@ -7,7 +7,7 @@ use crate::{ secret::SecretRow, BuilderCredentials, Gatekeeper, Summary, Vault, VaultBuilder, }, - vfs, Result, + vfs, Error, Result, }; use serde::{Deserialize, Serialize}; @@ -74,7 +74,10 @@ impl LocalAccount { account_key: &AccessKey, ) -> Result<()> { for folder in &conversion.folders { - let key = self.find_folder_password(folder.id()).await?; + let key = self + .find_folder_password(folder.id()) + .await? + .ok_or(Error::NoFolderPassword(*folder.id()))?; let vault = self .convert_folder_cipher( &conversion.cipher, diff --git a/crates/sdk/src/constants.rs b/crates/sdk/src/constants.rs index 42649bd3a3..2e4ba743eb 100644 --- a/crates/sdk/src/constants.rs +++ b/crates/sdk/src/constants.rs @@ -95,6 +95,14 @@ mod folders { /// Directory to store vaults. pub const VAULTS_DIR: &str = "vaults"; + /// Directory to store pending folders. + /// + /// Pending folders are folders with the LOCAL + /// flag set that were created from an + /// [AccountEvent::CreateFolder] event but won't have any + /// events yet unless a NO_SYNC flag has been removed. + pub const PENDING_DIR: &str = "pending"; + /// Directory to store data for clients. pub const LOCAL_DIR: &str = "local"; diff --git a/crates/sdk/src/error.rs b/crates/sdk/src/error.rs index f27960eee4..c9bad28e63 100644 --- a/crates/sdk/src/error.rs +++ b/crates/sdk/src/error.rs @@ -12,6 +12,11 @@ use crate::{ /// Error thrown by the core library. #[derive(Debug, Error)] pub enum Error { + /// Generic error message used when converting from some libraries + /// that return a `String` as an error. + #[error("{0}")] + Message(String), + /// Permission denied. /// /// If a shared vault is set to private shared access and @@ -225,10 +230,6 @@ pub enum Error { #[error("vault is not initialized")] VaultNotInit, - /// Error generated when a folder access key was not found. - #[error("folder access key for '{0}' not found")] - NoFolderKey(VaultId), - /// Error generated attempting to a initialize a vault when it has already been initialized. #[error("vault is already initialized")] VaultAlreadyInit, @@ -330,10 +331,14 @@ pub enum Error { #[error("failed to parse AGE identity: {0}")] AgeIdentityParse(String), - /// Error generated when a vault entry in the identity vault could not - /// be located. - #[error("could not find vault entry for {0}")] - NoVaultEntry(String), + /// Error generated when a folder password in the identity + /// vault could not be located. + #[error("could not find folder password for '{0}'")] + NoFolderPassword(VaultId), + + /// Error generated when a file encryption password could not be found. + #[error("could not find file encryption password in identity folder")] + NoFileEncryptionPassword, /// Error generated when a vault entry in an identity vault is of /// the wrong secret kind. @@ -650,4 +655,14 @@ pub enum Error { /// Error generated by the signin notifications channel. #[error(transparent)] MpscLockedNotify(#[from] tokio::sync::mpsc::error::SendError<()>), + + /// Error generated by the TOTP library. + #[error(transparent)] + TotpUrl(#[from] totp_rs::TotpUrlError), +} + +impl From for Error { + fn from(value: String) -> Self { + Self::Message(value) + } } diff --git a/crates/sdk/src/identity/identity.rs b/crates/sdk/src/identity/identity.rs index 63bb43a41f..8187a9b05b 100644 --- a/crates/sdk/src/identity/identity.rs +++ b/crates/sdk/src/identity/identity.rs @@ -176,7 +176,7 @@ impl Identity { pub async fn find_folder_password( &self, vault_id: &VaultId, - ) -> Result { + ) -> Result> { self.identity()?.find_folder_password(vault_id).await } diff --git a/crates/sdk/src/identity/identity_folder.rs b/crates/sdk/src/identity/identity_folder.rs index bc7f754829..00c3b73b4d 100644 --- a/crates/sdk/src/identity/identity_folder.rs +++ b/crates/sdk/src/identity/identity_folder.rs @@ -183,7 +183,10 @@ where tracing::debug!(urn = %device_key_urn, "read_device_vault"); let summary = vault.summary().clone(); - let device_password = self.find_folder_password(summary.id()).await?; + let device_password = self + .find_folder_password(summary.id()) + .await? + .ok_or(Error::NoFolderPassword(*summary.id()))?; let vault_file = VaultWriter::open(&device_vault_path).await?; let mirror = VaultWriter::new(&device_vault_path, vault_file)?; @@ -335,36 +338,37 @@ where pub async fn find_folder_password( &self, vault_id: &VaultId, - ) -> Result { + ) -> Result> { let urn = Vault::vault_urn(vault_id)?; - tracing::debug!(folder = %vault_id, urn = %urn, "find_folder_password"); - - let id = self - .index - .get(&(*self.folder.id(), urn.clone())) - .ok_or_else(|| Error::NoVaultEntry(urn.to_string()))?; + tracing::debug!( + folder = %vault_id, + urn = %urn, + "find_folder_password"); - let (_, secret, _) = self - .folder - .read_secret(id) - .await? - .ok_or_else(|| Error::NoSecretId(*self.folder.id(), *id))?; + if let Some(id) = self.index.get(&(*self.folder.id(), urn.clone())) { + let (_, secret, _) = + self.folder.read_secret(id).await?.ok_or_else(|| { + Error::NoSecretId(*self.folder.id(), *id) + })?; - let key = match secret { - Secret::Password { password, .. } => { - AccessKey::Password(password) - } - Secret::Age { key, .. } => { - AccessKey::Identity(key.expose_secret().parse().map_err( - |s: &str| Error::InvalidX25519Identity(s.to_owned()), - )?) - } - _ => { - return Err(Error::VaultEntryKind(urn.to_string())); - } - }; - Ok(key) + let key = match secret { + Secret::Password { password, .. } => { + AccessKey::Password(password) + } + Secret::Age { key, .. } => { + AccessKey::Identity(key.expose_secret().parse().map_err( + |s: &str| Error::InvalidX25519Identity(s.to_owned()), + )?) + } + _ => { + return Err(Error::VaultEntryKind(urn.to_string())); + } + }; + Ok(Some(key)) + } else { + Ok(None) + } } /// Remove a folder password from this identity. @@ -379,7 +383,7 @@ where let id = self .index .get(&(*self.folder.keeper().id(), urn.clone())) - .ok_or(Error::NoVaultEntry(urn.to_string()))?; + .ok_or(Error::NoFolderPassword(*vault_id))?; (*self.folder.keeper().id(), *id, urn) }; @@ -422,7 +426,7 @@ where let id = self .index .get(&(*self.folder.id(), urn.clone())) - .ok_or_else(|| Error::NoVaultEntry(urn.to_string()))?; + .ok_or_else(|| Error::NoFileEncryptionPassword)?; let password = if let Some((_, Secret::Password { password, .. }, _)) = diff --git a/crates/sdk/src/migrate/authenticator/export.rs b/crates/sdk/src/migrate/authenticator/export.rs new file mode 100644 index 0000000000..8e4e9f25e0 --- /dev/null +++ b/crates/sdk/src/migrate/authenticator/export.rs @@ -0,0 +1,74 @@ +use super::{AuthenticatorUrls, OTP_AUTH_URLS}; +use crate::{ + vault::{secret::Secret, Gatekeeper}, + Result, +}; +use async_zip::{ + tokio::write::ZipFileWriter, Compression, ZipDateTimeBuilder, + ZipEntryBuilder, +}; +use std::{collections::HashMap, path::Path}; +use time::OffsetDateTime; +use url::Url; + +/// Export an authenticator vault to a zip archive. +/// +/// The gatekeeper for the vault must be unlocked. +pub async fn export_authenticator( + path: impl AsRef, + source: &Gatekeeper, + include_qr_codes: bool, +) -> Result<()> { + // Gather TOTP secrets + let mut totp_secrets = HashMap::new(); + for id in source.vault().keys() { + if let Some((_, Secret::Totp { totp, .. }, _)) = + source.read_secret(id).await? + { + totp_secrets.insert(*id, totp); + } + } + + let inner = tokio::fs::File::create(path.as_ref()).await?; + let mut writer = ZipFileWriter::with_tokio(inner); + + // Write the JSON otpauth: URLs + let mut auth_urls = AuthenticatorUrls::default(); + for (id, totp) in &totp_secrets { + let url: Url = totp.get_url().parse()?; + auth_urls.otp.insert(*id, url); + } + let buffer = serde_json::to_vec_pretty(&auth_urls)?; + let entry = get_entry(OTP_AUTH_URLS)?; + writer.write_entry_whole(entry, &buffer).await?; + + if include_qr_codes { + for (id, totp) in totp_secrets { + let name = format!("qr/{}.png", id); + let buffer = totp.get_qr_png()?; + let entry = get_entry(&name)?; + writer.write_entry_whole(entry, &buffer).await?; + } + } + + writer.close().await?; + Ok(()) +} + +fn get_entry(path: &str) -> Result { + let now = OffsetDateTime::now_utc(); + let (hours, minutes, seconds) = now.time().as_hms(); + let month: u8 = now.month().into(); + + let dt = ZipDateTimeBuilder::new() + .year(now.year().into()) + .month(month.into()) + .day(now.day().into()) + .hour(hours.into()) + .minute(minutes.into()) + .second(seconds.into()) + .build(); + + Ok(ZipEntryBuilder::new(path.into(), Compression::Deflate) + .last_modification_date(dt)) +} diff --git a/crates/sdk/src/migrate/authenticator/import.rs b/crates/sdk/src/migrate/authenticator/import.rs new file mode 100644 index 0000000000..95037d0407 --- /dev/null +++ b/crates/sdk/src/migrate/authenticator/import.rs @@ -0,0 +1,61 @@ +use crate::{ + migrate::Error, + vault::{ + secret::{Secret, SecretMeta, SecretRow}, + Gatekeeper, + }, + Result, +}; +use async_zip::tokio::read::seek::ZipFileReader; +use std::path::Path; +use tokio::io::BufReader; +use totp_rs::TOTP; + +use super::{AuthenticatorUrls, OTP_AUTH_URLS}; + +/// Import an authenticator vault from a zip archive. +pub async fn import_authenticator( + path: impl AsRef, + keeper: &mut Gatekeeper, +) -> Result<()> { + let inner = BufReader::new(tokio::fs::File::open(path.as_ref()).await?); + let mut reader = ZipFileReader::with_tokio(inner).await?; + + let mut urls: Option = None; + + for index in 0..reader.file().entries().len() { + let entry = reader.file().entries().get(index).unwrap(); + let file_name = entry.filename(); + let file_name = file_name.as_str()?; + if file_name == OTP_AUTH_URLS { + let mut entry = reader.reader_with_entry(index).await?; + + let mut buffer = Vec::new(); + entry.read_to_end_checked(&mut buffer).await?; + + let auth_urls: AuthenticatorUrls = + serde_json::from_slice(&buffer)?; + + urls = Some(auth_urls); + break; + } + } + + let urls = urls.ok_or(Error::NoAuthenticatorUrls( + path.as_ref().display().to_string(), + ))?; + + for (id, url) in urls.otp { + let totp = TOTP::from_url(url.to_string())?; + let label = totp.account_name.clone(); + let secret = Secret::Totp { + totp, + user_data: Default::default(), + }; + let meta = SecretMeta::new(label, secret.kind()); + let secret_data = SecretRow::new(id, meta, secret); + keeper.create_secret(&secret_data).await?; + } + + Ok(()) +} diff --git a/crates/sdk/src/migrate/authenticator/mod.rs b/crates/sdk/src/migrate/authenticator/mod.rs new file mode 100644 index 0000000000..246cdba190 --- /dev/null +++ b/crates/sdk/src/migrate/authenticator/mod.rs @@ -0,0 +1,19 @@ +use crate::vault::secret::SecretId; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use url::Url; + +mod export; +mod import; + +pub use export::export_authenticator; +pub use import::import_authenticator; + +const OTP_AUTH_URLS: &str = "otp_auth.json"; + +/// URLs for an authenticator folder. +#[derive(Default, Serialize, Deserialize)] +pub struct AuthenticatorUrls { + /// Collection of `otpauth:` URLs. + pub otp: HashMap, +} diff --git a/crates/sdk/src/migrate/error.rs b/crates/sdk/src/migrate/error.rs index ff1ea8a0e7..ceb2cc9f11 100644 --- a/crates/sdk/src/migrate/error.rs +++ b/crates/sdk/src/migrate/error.rs @@ -8,6 +8,10 @@ pub enum Error { #[error(r#"import format "{0}" is not supported"#)] UnknownImportFormat(String), + /// Error generated when no authenticator URLs were found. + #[error("no authenticator URLs found in zip archive '{0}'")] + NoAuthenticatorUrls(String), + /// Error generated by the keychain access integration. #[cfg(target_os = "macos")] #[error(transparent)] diff --git a/crates/sdk/src/migrate/mod.rs b/crates/sdk/src/migrate/mod.rs index b42b8b9dcd..e2e6a5ebe7 100644 --- a/crates/sdk/src/migrate/mod.rs +++ b/crates/sdk/src/migrate/mod.rs @@ -6,8 +6,10 @@ use async_trait::async_trait; use crate::{crypto::AccessKey, vault::Vault}; +mod authenticator; mod error; +pub use authenticator::*; pub use error::Error; /// Result type for the migration library. diff --git a/crates/sdk/src/storage/client.rs b/crates/sdk/src/storage/client.rs index 1ed1848223..e4d2507d8d 100644 --- a/crates/sdk/src/storage/client.rs +++ b/crates/sdk/src/storage/client.rs @@ -10,7 +10,7 @@ use crate::{ }, identity::FolderKeys, passwd::{diceware::generate_passphrase, ChangePassword}, - prelude::VaultFlags, + prelude::{VaultFlags, EVENT_LOG_EXT}, signer::ecdsa::Address, storage::{AccessOptions, AccountPack, DiscFolder, NewFolderOptions}, vault::{ @@ -413,7 +413,7 @@ impl ClientStorage { // Refresh the in-memory and disc-based mirror let key = folder_keys .find(vault.id()) - .ok_or(Error::NoFolderKey(*vault.id()))?; + .ok_or(Error::NoFolderPassword(*vault.id()))?; self.refresh_vault(vault.summary(), key).await?; } @@ -464,6 +464,89 @@ impl ClientStorage { Ok(()) } + /// Create the event log on disc for a pending folder. + async fn create_pending_event_log( + &self, + summary: &Summary, + vault: Vault, + creation_time: Option<&UtcDateTime>, + ) -> Result<()> { + let vault_path = self.paths.pending_vault_path(summary.id()); + let mut event_log = DiscFolder::new(&vault_path).await?; + + // Must truncate the event log so that importing vaults + // does not end up with multiple create vault events + event_log.clear().await?; + + let (_, events) = FolderReducer::split(vault).await?; + + let mut records = Vec::with_capacity(events.len()); + for event in events.iter() { + records.push(event.default_record().await?); + } + + if let (Some(creation_time), Some(event)) = + (creation_time, records.get_mut(0)) + { + event.set_time(creation_time.to_owned()); + } + event_log.apply_records(records).await?; + + Ok(()) + } + + /// Promote a pending folder. + /// + /// A pending folder is one that was created with the `LOCAL` + /// and `NO_SYNC` flags set. + /// + /// The `LOCAL` flag indicates the folder is local-first and + /// other devices when they see the `LOCAL` flag set when an + /// [AccountEvent::CreateFolder] event is merged must create + /// stub files in the pending folder. + /// + /// Later, if a device decides to remove the `NO_SYNC` flag + /// then the server will include the folder in diff contents. + /// + /// Clients should then try to merge the diff contents but the + /// folder may not exist so they can + /// call `try_promote_pending_folder` first. + /// + /// Returns a boolean indicating whether a folder was promoted + /// or not. + #[doc(hidden)] + pub async fn try_promote_pending_folder( + &mut self, + folder_id: &VaultId, + ) -> Result { + let pending_vault = self.paths.pending_vault_path(folder_id); + let mut pending_event = pending_vault.clone(); + pending_event.set_extension(EVENT_LOG_EXT); + + let vault = self.paths.vault_path(folder_id); + let event = self.paths.event_log_path(folder_id); + + let has_pending_folder = vfs::try_exists(&pending_vault).await? + && vfs::try_exists(&pending_event).await?; + + if has_pending_folder { + // Move the files into place + vfs::rename(&pending_vault, &vault).await?; + vfs::rename(&pending_event, &event).await?; + + let summary = Header::read_summary_file(&vault).await?; + let folder = DiscFolder::new(&vault).await?; + let event_log = folder.event_log(); + let mut event_log = event_log.write().await; + event_log.load_tree().await?; + self.cache.insert(*summary.id(), folder); + self.add_summary(summary); + Ok(true) + } else { + Ok(false) + } + } + /// Refresh the in-memory vault from the contents /// of the current event log file. /// @@ -515,6 +598,17 @@ impl ClientStorage { Ok(()) } + /// Write the buffer for a local vault placeholder to disc. + async fn write_pending_vault_file( + &self, + vault_id: &VaultId, + buffer: impl AsRef<[u8]>, + ) -> Result<()> { + let vault_path = self.paths.pending_vault_path(vault_id); + vfs::write(vault_path, buffer.as_ref()).await?; + Ok(()) + } + /// Create a cache entry for each summary if it does not /// already exist. async fn load_caches(&mut self, summaries: &[Summary]) -> Result<()> { @@ -530,8 +624,14 @@ impl ClientStorage { /// Unlock all folders. pub async fn unlock(&mut self, keys: &FolderKeys) -> Result<()> { for (id, folder) in self.cache.iter_mut() { - let key = keys.find(id).ok_or(Error::NoFolderKey(*id))?; - folder.unlock(key).await?; + if let Some(key) = keys.find(id) { + folder.unlock(key).await?; + } else { + tracing::error!( + folder_id = %id, + "unlock::no_folder_key", + ); + } } Ok(()) } @@ -737,11 +837,6 @@ impl ClientStorage { &mut self, name: String, options: NewFolderOptions, - /* - key: Option, - cipher: Option, - kdf: Option, - */ ) -> Result<(Vec, AccessKey, Summary, AccountEvent)> { let (buf, key, summary) = self.prepare_folder(Some(name), options).await?; @@ -772,6 +867,9 @@ impl ClientStorage { let exists = self.find(|s| s.id() == vault.id()).is_some(); let summary = vault.summary().clone(); + let is_local_folder = + summary.flags.is_local() && summary.flags().is_sync_disabled(); + #[cfg(feature = "search")] if exists { if let Some(index) = self.index.as_mut() { @@ -781,38 +879,49 @@ impl ClientStorage { } // Always write out the updated buffer - self.write_vault_file(summary.id(), &buffer).await?; - - if !exists { - // Add the summary to the vaults we are managing - self.add_summary(summary.clone()); + if is_local_folder { + self.write_pending_vault_file(summary.id(), &buffer).await?; } else { - // Otherwise update with the new summary - if let Some(position) = - self.summaries.iter().position(|s| s.id() == summary.id()) - { - let existing = self.summaries.get_mut(position).unwrap(); - *existing = summary.clone(); - } + self.write_vault_file(summary.id(), &buffer).await?; } - #[cfg(feature = "search")] - if let Some(key) = key { - if let Some(index) = self.index.as_mut() { - // Ensure the imported secrets are in the search index - index.add_vault(vault.clone(), key).await?; + if !is_local_folder { + if !exists { + // Add the summary to the vaults we are managing + self.add_summary(summary.clone()); + } else { + // Otherwise update with the new summary + if let Some(position) = + self.summaries.iter().position(|s| s.id() == summary.id()) + { + let existing = self.summaries.get_mut(position).unwrap(); + *existing = summary.clone(); + } + } + + #[cfg(feature = "search")] + if let Some(key) = key { + if let Some(index) = self.index.as_mut() { + // Ensure the imported secrets are in the search index + index.add_vault(vault.clone(), key).await?; + } } } let event = vault.into_event().await?; - // Initialize the local cache for event log - self.create_cache_entry(&summary, Some(vault), creation_time) - .await?; + if is_local_folder { + self.create_pending_event_log(&summary, vault, creation_time) + .await?; + } else { + // Initialize the local cache for event log + self.create_cache_entry(&summary, Some(vault), creation_time) + .await?; - // Must ensure the folder is unlocked - if let Some(key) = key { - self.unlock_folder(summary.id(), key).await?; + // Must ensure the folder is unlocked + if let Some(key) = key { + self.unlock_folder(summary.id(), key).await?; + } } Ok((exists, event, summary)) @@ -966,16 +1075,6 @@ impl ClientStorage { Ok(events) } - /// Remove a local folder and do not register the - /// account event so the folder will not be removed - /// from other devices. - pub async fn remove_local_folder( - &mut self, - summary: &Summary, - ) -> Result> { - self.delete_folder(summary, false).await - } - /// Update the in-memory name for a folder. pub fn set_folder_name( &mut self, diff --git a/crates/sdk/src/storage/paths.rs b/crates/sdk/src/storage/paths.rs index 1c3c89eb15..62fa91e2a7 100644 --- a/crates/sdk/src/storage/paths.rs +++ b/crates/sdk/src/storage/paths.rs @@ -19,8 +19,8 @@ use crate::{ constants::{ ACCOUNT_EVENTS, APP_AUTHOR, APP_NAME, AUDIT_FILE_NAME, DEVICE_EVENTS, DEVICE_FILE, EVENT_LOG_EXT, FILES_DIR, FILE_EVENTS, IDENTITY_DIR, - JSON_EXT, LOCAL_DIR, LOCK_FILE, LOGS_DIR, REMOTES_FILE, REMOTE_DIR, - VAULTS_DIR, VAULT_EXT, + JSON_EXT, LOCAL_DIR, LOCK_FILE, LOGS_DIR, PENDING_DIR, REMOTES_FILE, + REMOTE_DIR, VAULTS_DIR, VAULT_EXT, }, vault::{secret::SecretId, VaultId}, vfs, @@ -75,6 +75,8 @@ pub struct Paths { files_dir: PathBuf, /// User vault storage. vaults_dir: PathBuf, + /// Pending folders dir. + pending_dir: PathBuf, /// User devices storage. device_file: PathBuf, } @@ -125,6 +127,7 @@ impl Paths { let user_dir = local_dir.join(user_id.as_ref()); let files_dir = user_dir.join(FILES_DIR); let vaults_dir = user_dir.join(VAULTS_DIR); + let pending_dir = user_dir.join(PENDING_DIR); let device_file = user_dir.join(format!("{}.{}", DEVICE_FILE, VAULT_EXT)); Self { @@ -137,6 +140,7 @@ impl Paths { user_dir, files_dir, vaults_dir, + pending_dir, device_file, } } @@ -151,6 +155,7 @@ impl Paths { vfs::create_dir_all(&self.user_dir).await?; vfs::create_dir_all(&self.files_dir).await?; vfs::create_dir_all(&self.vaults_dir).await?; + vfs::create_dir_all(&self.pending_dir).await?; } Ok(()) } @@ -306,6 +311,20 @@ impl Paths { vault_path } + /// Path to a pending vault file from it's identifier. + /// + /// # Panics + /// + /// If this set of paths are global (no user identifier). + pub fn pending_vault_path(&self, id: &VaultId) -> PathBuf { + if self.is_global() { + panic!("pending vault path is not accessible for global paths"); + } + let mut vault_path = self.pending_dir.join(id.to_string()); + vault_path.set_extension(VAULT_EXT); + vault_path + } + /// Path to an event log file from it's identifier. /// /// # Panics diff --git a/crates/sdk/src/vault/vault.rs b/crates/sdk/src/vault/vault.rs index 0c8aa0f37f..5b2d5e2ce6 100644 --- a/crates/sdk/src/vault/vault.rs +++ b/crates/sdk/src/vault/vault.rs @@ -71,8 +71,8 @@ bitflags! { /// /// This is useful for storing device specific keys. const NO_SYNC = 0b0000000010000000; - /// Reserved flag. - const _RESERVED = 0b0000000100000000; + /// Indicates the folder is intended to be local first. + const LOCAL = 0b0000000100000000; /// Indicates this vault is shared using asymmetric /// encryption. const SHARED = 0b0000001000000000; @@ -121,6 +121,11 @@ impl VaultFlags { self.contains(VaultFlags::NO_SYNC) } + /// Determine if this vault is local first. + pub fn is_local(&self) -> bool { + self.contains(VaultFlags::LOCAL) + } + /// Determine if this vault is shared. pub fn is_shared(&self) -> bool { self.contains(VaultFlags::SHARED) diff --git a/crates/sdk/tests/authenticator_export_import.rs b/crates/sdk/tests/authenticator_export_import.rs new file mode 100644 index 0000000000..f06ea1390b --- /dev/null +++ b/crates/sdk/tests/authenticator_export_import.rs @@ -0,0 +1,54 @@ +use anyhow::Result; +use secrecy::SecretString; +use sos_sdk::prelude::*; +use sos_test_utils::mock; +use tempfile::NamedTempFile; + +async fn create_mock_authenticator( +) -> Result<(Gatekeeper, SecretString, SecretRow)> { + let (folder_key, _) = generate_passphrase()?; + let vault = VaultBuilder::new() + .flags(VaultFlags::AUTHENTICATOR) + .build(BuilderCredentials::Password(folder_key.clone(), None)) + .await?; + + let key: AccessKey = folder_key.clone().into(); + let mut keeper = Gatekeeper::new(vault); + keeper.unlock(&key).await?; + + let (meta, secret) = mock::totp("mock@example.com"); + let secret_data = SecretRow::new(SecretId::new_v4(), meta, secret); + keeper.create_secret(&secret_data).await?; + Ok((keeper, folder_key, secret_data)) +} + +#[tokio::test] +async fn authenticator_export_import() -> Result<()> { + let (auth, folder_password, secret_data) = + create_mock_authenticator().await?; + + let archive = NamedTempFile::new()?; + + export_authenticator(archive.path(), &auth, true).await?; + + let vault = VaultBuilder::new() + .flags(VaultFlags::AUTHENTICATOR) + .build(BuilderCredentials::Password(folder_password.clone(), None)) + .await?; + + let key: AccessKey = folder_password.into(); + let mut keeper = Gatekeeper::new(vault); + keeper.unlock(&key).await?; + + import_authenticator(archive.path(), &mut keeper).await?; + + assert_eq!(1, keeper.vault().len()); + + let (meta, secret, _) = + keeper.read_secret(secret_data.id()).await?.unwrap(); + + assert_eq!(secret_data.meta().label(), meta.label()); + assert_eq!(secret_data.secret(), &secret); + + Ok(()) +} diff --git a/crates/sdk/tests/event_logs.rs b/crates/sdk/tests/event_logs.rs index 31c88bf1e0..fce48da1fd 100644 --- a/crates/sdk/tests/event_logs.rs +++ b/crates/sdk/tests/event_logs.rs @@ -1,9 +1,8 @@ use anyhow::Result; use sos_sdk::prelude::*; +use std::path::Path; use uuid::Uuid; -const PATH: &str = "target/event_log_standalone.events"; - async fn mock_secret<'a>() -> Result<(SecretId, VaultCommit)> { let id = Uuid::new_v4(); let entry = VaultEntry(Default::default(), Default::default()); @@ -13,9 +12,11 @@ async fn mock_secret<'a>() -> Result<(SecretId, VaultCommit)> { Ok((id, result)) } -async fn mock_event_log_standalone() -> Result<(FolderEventLog, SecretId)> { - if vfs::try_exists(PATH).await? { - vfs::remove_file(PATH).await?; +async fn mock_event_log_standalone( + path: impl AsRef, +) -> Result<(FolderEventLog, SecretId)> { + if vfs::try_exists(path.as_ref()).await? { + vfs::remove_file(path.as_ref()).await?; } let mut vault: Vault = Default::default(); @@ -25,7 +26,7 @@ async fn mock_event_log_standalone() -> Result<(FolderEventLog, SecretId)> { let (id, data) = mock_secret().await?; // Create a simple event log - let mut event_log = FolderEventLog::new(PATH).await?; + let mut event_log = FolderEventLog::new(path.as_ref()).await?; event_log .apply(vec![ &WriteEvent::CreateVault(vault_buffer), @@ -109,7 +110,8 @@ async fn event_log_compare() -> Result<()> { // // This can happen if a client compacts its event log which would create // a new commit tree. - let (standalone, _) = mock_event_log_standalone().await?; + let (standalone, _) = + mock_event_log_standalone("target/event_log_compare.events").await?; let proof = standalone.tree().head()?; let comparison = server.tree().compare(&proof)?; assert_eq!(Comparison::Unknown, comparison); @@ -119,9 +121,10 @@ async fn event_log_compare() -> Result<()> { #[tokio::test] async fn event_log_file_load() -> Result<()> { - mock_event_log_standalone().await?; + let path = "target/event_log_file_load.events"; + mock_event_log_standalone(path).await?; - let event_log = FolderEventLog::new(PATH).await?; + let event_log = FolderEventLog::new(path).await?; let mut it = event_log.iter(false).await?; while let Some(record) = it.next().await? { let _event = event_log.decode_event(&record).await?; diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml index 7533ec290a..00a0203285 100644 --- a/crates/server/Cargo.toml +++ b/crates/server/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sos-server" -version = "0.14.6" +version = "0.14.7" edition = "2021" description = "Server for the Save Our Secrets sync protocol." homepage = "https://saveoursecrets.com" @@ -49,7 +49,7 @@ utoipa-rapidoc = { version = "3", features = ["axum"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync", "macros"] } [dependencies.sos-protocol] -version = "0.14.6" +version = "0.14.7" path = "../protocol" features = ["files"] diff --git a/crates/server/src/handlers/account.rs b/crates/server/src/handlers/account.rs index d7285b340e..1502042d16 100644 --- a/crates/server/src/handlers/account.rs +++ b/crates/server/src/handlers/account.rs @@ -1061,7 +1061,8 @@ mod handlers { writer .storage .merge_folder(&id, diff, &mut outcome) - .await?, + .await? + .0, outcome, records, ) diff --git a/crates/server/src/storage/filesystem/sync.rs b/crates/server/src/storage/filesystem/sync.rs index c5c410bcc0..bbfe76dbc4 100644 --- a/crates/server/src/storage/filesystem/sync.rs +++ b/crates/server/src/storage/filesystem/sync.rs @@ -9,7 +9,7 @@ use sos_protocol::{ events::{ AccountDiff, AccountEvent, AccountEventLog, CheckedPatch, EventLogExt, FolderDiff, FolderEventLog, FolderPatch, - FolderReducer, LogEvent, + FolderReducer, LogEvent, WriteEvent, }, storage::StorageEventLogs, vault::{Header, Summary, VaultAccess, VaultId, VaultWriter}, @@ -504,7 +504,7 @@ impl Merge for ServerStorage { folder_id: &VaultId, diff: FolderDiff, outcome: &mut MergeOutcome, - ) -> Result { + ) -> Result<(CheckedPatch, Vec)> { let len = diff.patch.len() as u64; tracing::debug!( @@ -524,6 +524,19 @@ impl Merge for ServerStorage { log.patch_checked(&diff.checkpoint, &diff.patch).await?; if let CheckedPatch::Success(_) = &checked_patch { + // Must update files on disc when we encounter a change + // to the vault flags so that the NO_SYNC flag will be + // respected + let events = diff.patch.into_events::().await?; + for event in events { + if let WriteEvent::SetVaultFlags(flags) = event { + let path = self.paths.vault_path(folder_id); + let file = VaultWriter::open(&path).await?; + let mut writer = VaultWriter::new(path, file)?; + writer.set_vault_flags(flags).await?; + } + } + outcome.changes += len; outcome.tracked.add_tracked_folder_changes( folder_id, @@ -531,7 +544,7 @@ impl Merge for ServerStorage { ); } - Ok(checked_patch) + Ok((checked_patch, vec![])) } async fn compare_folder( @@ -593,6 +606,10 @@ impl StorageEventLogs for ServerStorage { #[async_trait] impl SyncStorage for ServerStorage { + fn is_client_storage(&self) -> bool { + false + } + async fn sync_status(&self) -> Result { // NOTE: the order for computing the cumulative // NOTE: root hash must be identical to the logic diff --git a/crates/sos/Cargo.toml b/crates/sos/Cargo.toml index 1fb458f29c..e32b6b37b1 100644 --- a/crates/sos/Cargo.toml +++ b/crates/sos/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "sos" -version = "0.14.6" +version = "0.14.7" edition = "2021" description = "Distributed, encrypted database for private secrets." homepage = "https://saveoursecrets.com" @@ -48,7 +48,7 @@ rustyline = "14" rustyline-derive = "0.10" [dependencies.sos-net] -version = "0.14.6" +version = "0.14.7" features = ["full"] path = "../net" diff --git a/crates/sos/src/cli/sos.rs b/crates/sos/src/cli/sos.rs index 8dfbbe1642..daa352ebfb 100644 --- a/crates/sos/src/cli/sos.rs +++ b/crates/sos/src/cli/sos.rs @@ -4,13 +4,10 @@ use std::path::PathBuf; use crate::{ commands::{ - account, audit, check, device, environment, events, folder, - preferences, secret, - security_report::{self, SecurityReportFormat}, - server, shell, sync, tools, AccountCommand, AuditCommand, - CheckCommand, DeviceCommand, EnvironmentCommand, EventsCommand, - FolderCommand, PreferenceCommand, SecretCommand, ServerCommand, - SyncCommand, ToolsCommand, + account, device, environment, folder, preferences, secret, server, + shell, sync, tools, AccountCommand, DeviceCommand, + EnvironmentCommand, FolderCommand, PreferenceCommand, SecretCommand, + ServerCommand, SyncCommand, ToolsCommand, }, helpers::{PROGRESS_MONITOR, USER}, CommandTree, Result, @@ -84,51 +81,6 @@ pub enum Command { cmd: FileCommand, }, */ - /// Generate a security report. - /// - /// Inspect all passwords in an account and report - /// passwords with an entropy score less than 3 or - /// passwords that are breached. - SecurityReport { - /// Force overwrite if the file exists. - #[clap(long)] - force: bool, - - /// Account name or address. - #[clap(short, long)] - account: Option, - - /// Include all entries. - /// - /// Security reports by default only include - /// entries that fail, use this option to include - /// entries that passed the security threshold. - #[clap(short, long)] - include_all: bool, - - /// Output format: csv or json. - #[clap(short, long, default_value = "csv")] - format: SecurityReportFormat, - - /// Write report to this file. - file: PathBuf, - }, - /// Print and monitor audit logs. - Audit { - #[clap(subcommand)] - cmd: AuditCommand, - }, - /// Check file status and integrity. - Check { - #[clap(subcommand)] - cmd: CheckCommand, - }, - /// Inspect event records. - #[clap(alias = "event")] - Events { - #[clap(subcommand)] - cmd: EventsCommand, - }, /// Interactive login shell. Shell { /// Folder name or identifier. @@ -201,19 +153,6 @@ pub async fn run() -> Result<()> { Command::Server { cmd } => server::run(cmd).await?, Command::Sync { cmd } => sync::run(cmd).await?, // Command::File { cmd } => file::run(cmd).await?, - Command::SecurityReport { - account, - force, - format, - include_all, - file, - } => { - security_report::run(account, force, format, include_all, file) - .await? - } - Command::Audit { cmd } => audit::run(cmd).await?, - Command::Check { cmd } => check::run(cmd).await?, - Command::Events { cmd } => events::run(cmd).await?, Command::Shell { account, folder } => { shell::run(account, folder).await? } diff --git a/crates/sos/src/commands/mod.rs b/crates/sos/src/commands/mod.rs index 6e05e2ddce..e0216dacad 100644 --- a/crates/sos/src/commands/mod.rs +++ b/crates/sos/src/commands/mod.rs @@ -1,25 +1,18 @@ pub mod account; -pub mod audit; -pub mod check; pub mod device; pub mod environment; -pub mod events; // pub mod file; pub mod folder; pub mod preferences; pub mod secret; -pub mod security_report; pub mod server; pub mod shell; pub mod sync; pub mod tools; pub use account::Command as AccountCommand; -pub use audit::Command as AuditCommand; -pub use check::Command as CheckCommand; pub use device::Command as DeviceCommand; pub use environment::Command as EnvironmentCommand; -pub use events::Command as EventsCommand; // pub use file::Command as FileCommand; pub use folder::Command as FolderCommand; pub use preferences::Command as PreferenceCommand; diff --git a/crates/sos/src/commands/audit.rs b/crates/sos/src/commands/tools/audit.rs similarity index 100% rename from crates/sos/src/commands/audit.rs rename to crates/sos/src/commands/tools/audit.rs diff --git a/crates/sos/src/commands/tools/authenticator.rs b/crates/sos/src/commands/tools/authenticator.rs new file mode 100644 index 0000000000..ef1a226bdb --- /dev/null +++ b/crates/sos/src/commands/tools/authenticator.rs @@ -0,0 +1,113 @@ +use crate::{ + helpers::{ + account::resolve_user, messages::success, readline::read_flag, + }, + Error, Result, +}; +use clap::Subcommand; +use sos_net::sdk::prelude::{ + export_authenticator, import_authenticator, Account, AccountRef, + FolderCreate, NewFolderOptions, VaultFlags, +}; +use std::path::PathBuf; + +#[derive(Subcommand, Debug)] +pub enum Command { + /// Export the TOTP secrets in an authenticator folder + Export { + /// Account name or address + #[clap(short, long)] + account: Option, + + /// Include PNG images of the QR codes in the zip archive + #[clap(short, long)] + qr_codes: bool, + + /// Output zip archive + file: PathBuf, + }, + /// Import the TOTP secrets from a zip archive + Import { + /// Account name or address + #[clap(short, long)] + account: Option, + + /// Name used when creating a new authenticator folder + #[clap(short, long)] + folder_name: Option, + + /// Input zip archive + file: PathBuf, + }, +} + +pub async fn run(cmd: Command) -> Result<()> { + match cmd { + Command::Export { + account, + file, + qr_codes, + } => { + let user = resolve_user(account.as_ref(), false).await?; + let owner = user.write().await; + let authenticator = owner + .authenticator_folder() + .await + .ok_or(Error::NoAuthenticatorFolder)?; + + let storage = owner.storage().await?; + let storage = storage.read().await; + let folder = storage.cache().get(authenticator.id()).unwrap(); + + export_authenticator(file, folder.keeper(), qr_codes).await?; + success("authenticator TOTP secrets exported"); + } + Command::Import { + account, + file, + folder_name, + } => { + let user = resolve_user(account.as_ref(), false).await?; + let mut owner = user.write().await; + + let folder = if let Some(authenticator) = + owner.authenticator_folder().await + { + let prompt = format!( + r#"Overwrite secrets in the "{}" folder (y/n)? "#, + authenticator.name() + ); + + if read_flag(Some(&prompt))? { + Some(authenticator) + } else { + None + } + } else { + let options = NewFolderOptions { + flags: VaultFlags::AUTHENTICATOR + | VaultFlags::LOCAL + | VaultFlags::NO_SYNC, + ..Default::default() + }; + let FolderCreate { folder, .. } = owner + .create_folder( + folder_name.unwrap_or("Authenticator".to_string()), + options, + ) + .await?; + Some(folder) + }; + + if let Some(folder) = folder { + let storage = owner.storage().await?; + let mut storage = storage.write().await; + let folder = + storage.cache_mut().get_mut(folder.id()).unwrap(); + import_authenticator(file, folder.keeper_mut()).await?; + success("authenticator TOTP secrets imported"); + } + } + } + Ok(()) +} diff --git a/crates/sos/src/commands/check.rs b/crates/sos/src/commands/tools/check.rs similarity index 98% rename from crates/sos/src/commands/check.rs rename to crates/sos/src/commands/tools/check.rs index 5b0d596122..7db9252060 100644 --- a/crates/sos/src/commands/check.rs +++ b/crates/sos/src/commands/tools/check.rs @@ -125,6 +125,7 @@ pub async fn header(vault: PathBuf, verbose: bool) -> Result<()> { details.push(("contact", header.flags().is_contact())); details.push(("authenticator", header.flags().is_authenticator())); details.push(("sync_disabled", header.flags().is_sync_disabled())); + details.push(("local", header.flags().is_local())); details.push(("shared", header.flags().is_shared())); let details = diff --git a/crates/sos/src/commands/events.rs b/crates/sos/src/commands/tools/events.rs similarity index 100% rename from crates/sos/src/commands/events.rs rename to crates/sos/src/commands/tools/events.rs diff --git a/crates/sos/src/commands/tools.rs b/crates/sos/src/commands/tools/mod.rs similarity index 67% rename from crates/sos/src/commands/tools.rs rename to crates/sos/src/commands/tools/mod.rs index 8f932a6581..7de9037620 100644 --- a/crates/sos/src/commands/tools.rs +++ b/crates/sos/src/commands/tools/mod.rs @@ -1,5 +1,4 @@ use crate::{ - commands::check::verify_events, helpers::{ account::resolve_account, account::resolve_user_with_password, @@ -19,10 +18,39 @@ use sos_net::sdk::{ vault::VaultId, vfs, Paths, }; +use std::path::PathBuf; use terminal_banner::{Banner, Padding}; +mod audit; +mod authenticator; +mod check; +mod events; +mod security_report; + +use audit::Command as AuditCommand; +use authenticator::Command as AuthenticatorCommand; +use check::{verify_events, Command as CheckCommand}; +use events::Command as EventsCommand; +use security_report::SecurityReportFormat; + #[derive(Subcommand, Debug)] pub enum Command { + /// Print and monitor audit logs. + Audit { + #[clap(subcommand)] + cmd: AuditCommand, + }, + /// Export and import TOTP secrets. + #[clap(alias = "auth")] + Authenticator { + #[clap(subcommand)] + cmd: AuthenticatorCommand, + }, + /// Check file status and integrity. + Check { + #[clap(subcommand)] + cmd: CheckCommand, + }, /// Convert the cipher for an account. ConvertCipher { /// Account name or address. @@ -36,6 +64,12 @@ pub enum Command { /// Convert to this cipher. cipher: Cipher, }, + /// Inspect event records. + #[clap(alias = "event")] + Events { + #[clap(subcommand)] + cmd: EventsCommand, + }, /// Repair a vault from a corresponding events file. RepairVault { /// Account name or address. @@ -44,11 +78,43 @@ pub enum Command { /// Folder identifier. folder: VaultId, }, + /// Generate a security report. + /// + /// Inspect all passwords in an account and report + /// passwords with an entropy score less than 3 or + /// passwords that are breached. + SecurityReport { + /// Force overwrite if the file exists. + #[clap(long)] + force: bool, + + /// Account name or address. + #[clap(short, long)] + account: Option, + + /// Include all entries. + /// + /// Security reports by default only include + /// entries that fail, use this option to include + /// entries that passed the security threshold. + #[clap(short, long)] + include_all: bool, + + /// Output format: csv or json. + #[clap(short, long, default_value = "csv")] + format: SecurityReportFormat, + + /// Write report to this file. + file: PathBuf, + }, } /// Handle sync commands. pub async fn run(cmd: Command) -> Result<()> { match cmd { + Command::Audit { cmd } => audit::run(cmd).await?, + Command::Authenticator { cmd } => authenticator::run(cmd).await?, + Command::Check { cmd } => check::run(cmd).await?, Command::ConvertCipher { account, cipher, @@ -93,6 +159,7 @@ pub async fn run(cmd: Command) -> Result<()> { } } } + Command::Events { cmd } => events::run(cmd).await?, Command::RepairVault { account, folder } => { let account = resolve_account(Some(&account)) .await @@ -138,6 +205,16 @@ pub async fn run(cmd: Command) -> Result<()> { _ => fail("unable to locate account"), } } + Command::SecurityReport { + account, + force, + format, + include_all, + file, + } => { + security_report::run(account, force, format, include_all, file) + .await? + } } Ok(()) } diff --git a/crates/sos/src/commands/security_report.rs b/crates/sos/src/commands/tools/security_report.rs similarity index 100% rename from crates/sos/src/commands/security_report.rs rename to crates/sos/src/commands/tools/security_report.rs diff --git a/crates/sos/src/error.rs b/crates/sos/src/error.rs index 78c7a41252..d2bc2502ef 100644 --- a/crates/sos/src/error.rs +++ b/crates/sos/src/error.rs @@ -36,6 +36,10 @@ pub enum Error { #[error(r#"initial sync has errors: {0}"#)] InitialSync(SyncError), + /// Could not find an authenticator folder. + #[error("could not find an authenticator folder")] + NoAuthenticatorFolder, + /// Sync failed. #[error(r#"sync failed"#)] SyncFail,