diff --git a/.gitignore b/.gitignore index c0024a6b..57d33ab6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target /.vscode -bin/tofnd* \ No newline at end of file +bin/tofnd* +/.tofnd diff --git a/Dockerfile b/Dockerfile index 008f97f4..535d89e7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -47,4 +47,5 @@ VOLUME [ "/.tofnd" ] ENV UNSAFE "" ENV MNEMONIC_CMD "" ENV NOPASSWORD "" +ENV TOFND_HOME "" ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md index 05b6ecef..f095ca59 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,14 @@ git clone git@github.com:axelarnetwork/tofnd.git --recursive # Running the server ``` -$ cargo run +# init tofnd +$ ./tofnd -m create + +# IMPORTANT: store the content of ./.tofnd/export file at a safe, offline place, and then delete the file +$ rm ./.tofnd/export + +# start tofnd daemon +$ ./tofnd ``` Terminate the server with `ctrl+C`. @@ -59,7 +66,8 @@ We use [clap](https://clap.rs/) to manage command line arguments. Users can specify: 1. Tofnd's root folder. Use `--directory` or `-d` to specify a full or a relative path. If no argument is provided, then the environment variable `TOFND_HOME` is used. If no environment variable is set either, the default `./tofnd` directory is used. 2. The port number of the gRPC server (default is 50051). -3. `mnemonic` operations for their `tofnd` instance (default is `Create`). +3. The option to run in _unsafe_ mode. By default, this option is off, and safe primes are used for keygen. Use the `--unsafe` flag only for testing. +4. `mnemonic` operations for their `tofnd` instance (default is `Existing`). For more information, see on mnemonic options, see [Mnemonic](#mnemonic). 4. The option to run in _unsafe_ mode. By default, this option is off, and safe primes are used for keygen. **Attention: Use the `--unsafe` flag only for testing**. 5. By default, `tofnd` expects a password from the standard input. Users that don't want to use passwords can use the `--no-password` flag. **Attention: Use `--no-password` only for testing .** @@ -79,30 +87,57 @@ FLAGS: OPTIONS: -d, --directory [env: TOFND_HOME=] [default: .tofnd] - -m, --mnemonic [default: create] [possible values: stored, create, import, update, export] + -m, --mnemonic [default: existing] [possible values: existing, create, import, export] -p, --port [default: 50051]] ``` # Docker -To run `tofnd` inside a container, run: +## Setup +To setup a `tofnd` container, use the `create` mnemonic command: ``` -docker-compose up +docker-compose run -e MNEMONIC_CMD=create tofnd ``` -For testing purposes, `docker-compose.unsafe.yml` is available which is equivelent to `cargo run -- --unsafe`. To create an unsafe `tofnd` container, run +This will initialize `tofnd`, and then exit. + +## Execution + +To run a `tofnd` daemon inside a container, run: ``` -docker-compose -f docker-compose.unsafe.yml up +docker-compose up ``` +## Storage + We use [data containers](https://docs.docker.com/engine/reference/commandline/volume_create/) to persist data across restarts. To clean up storage, remove all `tofnd` containers, and run ``` docker volume rm tofnd_tofnd ``` +## Testing + +For testing purposes, `docker-compose.test.yml` is available, which is equivelent to `./tofnd --no-password --unsafe`. To spin up a test `tofnd` container, run + +``` +docker-compose -f docker-compose.test.yml up +``` + +## The `auto` command + +In containerized environments the `auto` mnemonic command can be used. This command is implemented in `entrypoint.sh` and does the following: +1. Try to use existing mnemonic. If successful then launch `tofnd` server. +2. Try to import a mnemonic from file. If successful then launch `tofnd` server. +3. Create a new mnemonic. The newly created mnemonic is automatically written to the file `TOFND_HOME/export`---rename this file to `TOFND_HOME/import` so as to unblock future executions of tofnd. Then launch `tofnd` server. + +The rationale behind `auto` is that users can frictionlessly launch and restart their tofnd nodes without the need to execute multiple commands. +`auto` is currently the default command only in `docker-compose.test.yml`, but users can edit the `docker-compose.yml` to use it at their own discretion. + +**Attention:** `auto` leaves the mnemonic on plain text on disk. You should remove the `TOFND_HOME/import` file and store the mnemonic at a safe, offline place. + # Mnemonic `Tofnd` uses the [tiny-bip39](https://docs.rs/crate/tiny-bip39/0.8.0) crate to enable users manage mnemonic passphrases. Currently, each party can use only one passphrase. @@ -113,17 +148,13 @@ Mnemonic is used to enable _recovery_ of shares in case of unexpected loss. See The command line API supports the following commands: -* `Noop` does nothing and always succeeds; useful when the container restarts with the same mnemonic. - -* `Create` creates a new mnemonic if there none exists, otherwise does nothing. The new passphrase is written in a file named _./tofnd/export_. - -* `Import` adds a new mnemonic from file _./tofnd/import_ file; Succeeds when there is no other mnemonic already imported, fails otherwise. +* `Existing` Starts the gRPC daemon using an existing mnemonic; Fails if no mnemonic exist. -* `Export` writes the existing mnemonic to file _./tofnd/export_; Succeeds when there is an existing mnemonic, fails otherwise. +* `Create` Creates a new mnemonic, inserts it in the kv-store, exports it to a file and exits; Fails if a mnemonic already exists. -* `Update` updates existing mnemonic from file _./tofnd/import_; Succeeds when there is an existing mnemonic, fails otherwise. The old passphrase is written to file _./export_. +* `Import` Prompts user to give a new mnemonic from standard input, inserts it in the kv-store and exits; Fails if a mnemonic exists or if the provided string is not a valid bip39 mnemonic. -If a _./tofnd/export_ file already exists, then a new one is created with a new id, e.g. _./tofnd/export_2_, _./tofnd/export_3_, etc. +* `Export` Writes the existing mnemonic to _/.tofnd/export_ and exits; Succeeds when there is an existing mnemonic. Fails if no mnemonic is stored, or the export file already exists. ## Zeroization diff --git a/docker-compose.unsafe.yml b/docker-compose.test.yml similarity index 54% rename from docker-compose.unsafe.yml rename to docker-compose.test.yml index 7d96bd83..6db3f9af 100644 --- a/docker-compose.unsafe.yml +++ b/docker-compose.test.yml @@ -1,10 +1,15 @@ +# usage: +# $ docker-compose up +# or +# $ docker-compose -f docker-compose.test.yml run -e MNEMONIC_CMD= tofnd + volumes: tofnd: services: tofnd: build: . - container_name: tofnd + container_name: tofnd-test hostname: tofnd image: axelar/tofnd volumes: @@ -13,3 +18,5 @@ services: # Attention! Use UNSAFE=true only for testing - UNSAFE=true - NOPASSWORD=true + - MNEMONIC_CMD=auto + - TOFND_HOME=.tofnd diff --git a/docker-compose.yml b/docker-compose.yml index 04770a33..134b5d31 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,8 @@ +# usage: +# $ docker-compose up +# or +# $ docker-compose run -e MNEMONIC_CMD= tofnd + volumes: tofnd: @@ -10,7 +15,5 @@ services: volumes: - tofnd:/.tofnd environment: - # ATTENTION: use --unsafe only for testing - - UNSAFE=true - # available cmds: create (default), stored, update, import, export - - MNEMONIC_CMD=create + # available cmds: auto, create, existing (default), import, export + - MNEMONIC_CMD=auto diff --git a/entrypoint.sh b/entrypoint.sh index 5be8ada0..0fb482ac 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -2,14 +2,106 @@ set -e +OK=0 +ERR=1 + +# create: create a new mnemonic, export it to a file under the name "import" and continue +create_mnemonic() { + echo "Creating mnemonic ..." + ${PASSWORD} | tofnd ${ARGS} -m create && mv $EXPORT_PATH $IMPORT_PATH && echo "... ok" && return $OK + return $ERR +} + +# import: import a mnemonic from $IMPORT_PATH and continue +import_mnemonic() { + echo "Importing mnemonic ..." + + # check if import file exists + if [ ! -f "$IMPORT_PATH" ]; then \ + echo "No import file found at $IMPORT_PATH" + return $ERR + fi + + # check if password exists + if [ -n "${NOPASSWORD}" ]; then \ + echo "No password" + (cat $IMPORT_PATH | tofnd ${ARGS} -m import) || return $ERR + else + echo "With password" + # TODO: provide actual password here + ((echo $PASSWORD && cat $IMPORT_PATH) | tofnd ${ARGS} -m import) || return $ERR + fi + + echo "... ok" + return $OK +} + +# export: export the mnemonic to $EXPORT_PATH, move it to $IMPORT_PATH and exit +export_mnemonic() { + echo "Exporting mnemonic ..." + echo ${PASSWORD} | tofnd ${ARGS} -m export && mv $EXPORT_PATH $IMPORT_PATH || return $ERR + echo "... ok" + return $OK +} + +# TODO: get actual password from user. See https://github.com/axelarnetwork/axelarate/issues/269. +PASSWORD="" + +# gather user's args ARGS="" -if [ -n "${NOPASSWORD}" ]; then \ - ARGS="$ARGS --no-password"; \ -fi -if [ -n "${UNSAFE}" ]; then \ - ARGS="$ARGS --unsafe"; \ -fi + +# set tofnd root. TOFND_HOME can be set to a different path by the user. +TOFND_HOME=${TOFND_HOME:-"./.tofnd"} +IMPORT_PATH=$TOFND_HOME/import +EXPORT_PATH=$TOFND_HOME/export + +echo "Using tofnd root:" $TOFND_HOME + +# add '--no-password' and '--unsafe' flags to args if enabled +ARGS=${NOPASSWORD:+"${ARGS} --no-password"} +# add '--unsafe' flag to args if enabled +ARGS=${UNSAFE:+"${ARGS} --unsafe"} + +# check mnemonic arg if [ -n "${MNEMONIC_CMD}" ]; then \ - ARGS="$ARGS -m $MNEMONIC_CMD"; \ + + case ${MNEMONIC_CMD} in + # auto: try to set up tofnd and then spin up tofnd with the existing mnemonic. + # Order of set up: 1) import mnemonic, 2) create mnemonic. + auto) + echo "Trying import" && import_mnemonic \ + || (echo "... skipping. Trying to create" && create_mnemonic) \ + || echo "... skipping" + ;; + + existing) + ;; + + create) + create_mnemonic || exit $ERR + exit $OK + ;; + + import) + import_mnemonic || exit $ERR + exit $OK + ;; + + export) + export_mnemonic || exit $ERR + exit $OK + ;; + + *) + echo "Unknown command: ${MNEMONIC_CMD}" + exit $ERR + ;; + esac + + echo "Using existing mnemonic ..." + ARGS="${ARGS} -m existing" fi -exec tofnd $ARGS "$@"; \ + +# execute tofnd daemon +exec echo ${PASSWORD} | tofnd ${ARGS} "$@"; \ + diff --git a/proto b/proto index afb7358f..ed923dbf 160000 --- a/proto +++ b/proto @@ -1 +1 @@ -Subproject commit afb7358f2051370950881220be6ae51f4dbafdb9 +Subproject commit ed923dbfcc9c12343c41f3fd38753f65f73d98b9 diff --git a/src/config/mod.rs b/src/config/mod.rs index 0481bc31..c0513fd5 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -7,9 +7,9 @@ use anyhow::anyhow; // TODO: move these into constants.rs const DEFAULT_PATH_ROOT: &str = ".tofnd"; const TOFND_HOME_ENV_VAR: &str = "TOFND_HOME"; -const DEFAULT_MNEMONIC_CMD: &str = "create"; +const DEFAULT_MNEMONIC_CMD: &str = "existing"; const DEFAULT_PORT: &str = "50051"; -const AVAILABLE_MNEMONIC_CMDS: [&str; 5] = ["stored", "create", "import", "update", "export"]; +const AVAILABLE_MNEMONIC_CMDS: [&str; 4] = ["existing", "create", "import", "export"]; #[cfg(feature = "malicious")] mod malicious; @@ -32,7 +32,7 @@ impl Default for Config { Config { port: 50051, safe_keygen: true, - mnemonic_cmd: Cmd::Noop, + mnemonic_cmd: Cmd::Existing, tofnd_path: DEFAULT_PATH_ROOT.to_string(), password_method: PasswordMethod::Prompt, #[cfg(feature = "malicious")] diff --git a/src/gg20/mnemonic/bip39_bindings.rs b/src/gg20/mnemonic/bip39_bindings.rs index 0fabf786..9fc017fe 100644 --- a/src/gg20/mnemonic/bip39_bindings.rs +++ b/src/gg20/mnemonic/bip39_bindings.rs @@ -5,11 +5,10 @@ //! //! Zeroization: //! All functions that accept and/or return structs that implement zeroization: -//! [super::Password], [super::Entropy], [bip39::Mnemonic], [bip39::Seed] +//! [crate::gg20::Password], [crate::gg20::Entropy], [bip39::Mnemonic], [bip39::Seed] -use super::error::bip39::{Bip39Error::*, Bip39Result}; -use super::Entropy; -use crate::gg20::types::Password; +use super::results::bip39::{Bip39Error::*, Bip39Result}; +use crate::gg20::types::{Entropy, Password}; use bip39::{Language, Mnemonic, Seed}; // TODO: we can enrich the API so that users can decide which language they want to use diff --git a/src/gg20/mnemonic/cmd_handler.rs b/src/gg20/mnemonic/cmd_handler.rs new file mode 100644 index 00000000..e4494be1 --- /dev/null +++ b/src/gg20/mnemonic/cmd_handler.rs @@ -0,0 +1,280 @@ +use super::{ + bip39_bindings::{bip39_from_phrase, bip39_new_w24, bip39_seed}, + results::mnemonic::{ + InnerMnemonicError::*, InnerMnemonicResult, MnemonicError::*, MnemonicResult, SeedResult, + }, +}; +use crate::{ + gg20::{ + service::Gg20Service, + types::{Entropy, Password}, + }, + kv_manager::error::{InnerKvError, KvError}, +}; +use tofn::gg20::keygen::SecretRecoveryKey; + +use rpassword::read_password; +use std::convert::TryInto; +use tracing::{error, info}; + +// default key to store mnemonic +const MNEMONIC_KEY: &str = "mnemonic"; + +#[derive(Clone, Debug)] +pub enum Cmd { + Existing, + Create, + Import, + Export, +} + +impl Cmd { + pub fn from_string(cmd_str: &str) -> MnemonicResult { + let cmd = match cmd_str { + "existing" => Self::Existing, + "create" => Self::Create, + "import" => Self::Import, + "export" => Self::Export, + _ => return Err(WrongCommand(cmd_str.to_string())), + }; + Ok(cmd) + } + /// On [Cmd::Existing], continue tofnd. + /// On [Cmd::Create], [Cmd::Import] or [Cmd::Export], exit tofnd. + pub fn exit_after_cmd(&self) -> bool { + match &self { + Cmd::Existing => false, + Cmd::Create => true, + Cmd::Import => true, + Cmd::Export => true, + } + } +} + +/// implement mnemonic-specific functions for Gg20Service +impl Gg20Service { + /// get mnemonic seed from kv-store + pub async fn seed(&self) -> SeedResult { + let mnemonic = self.mnemonic_kv.get(MNEMONIC_KEY).await?; + // A user may decide to protect their mnemonic with a passphrase. We pass an empty password for now. + // https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed + Ok(bip39_seed(mnemonic, Password("".to_owned()))? + .as_bytes() + .try_into()?) + } + + /// async function that handles all mnemonic commands + pub async fn handle_mnemonic(&self) -> MnemonicResult<()> { + match self.cfg.mnemonic_cmd { + Cmd::Existing => self.handle_existing().await.map_err(ExistingErr), + Cmd::Create => self.handle_create().await.map_err(CreateErr), + Cmd::Import => self.handle_import().await.map_err(ImportErr), + Cmd::Export => self.handle_export().await.map_err(ExportErr), + } + } + + /// use the existing mnemonic to spin up a tofnd deamon. + /// if an export file exists in the default path, returns an error. + /// if an no mnemonic record exists in the kv-store, returns an error. + async fn handle_existing(&self) -> InnerMnemonicResult<()> { + // if there is an exported mnemonic, raise an error and don't start the daemon. + // we do this to prevent users from accidentally leave their mnemonic on disk in plain text + self.io.check_if_not_exported()?; + + // try to get mnemonic from kv-store + match self.mnemonic_kv.exists(MNEMONIC_KEY).await? { + true => Ok(()), + false => Err(KvErr(KvError::ExistsErr(InnerKvError::LogicalErr( + "Mnemonic not found".to_string(), + )))), + } + } + + /// inserts entropy to the kv-store + /// takes ownership of entropy to delegate zeroization. + async fn handle_insert(&self, entropy: Entropy) -> InnerMnemonicResult<()> { + // Don't use `map_err` to make it more readable. + let reservation = self.mnemonic_kv.reserve_key(MNEMONIC_KEY.to_owned()).await; + match reservation { + // if we can reserve, try put + Ok(reservation) => match self.mnemonic_kv.put(reservation, entropy.to_owned()).await { + // if put is ok, write the phrase to a file + Ok(()) => { + info!("Mnemonic successfully added in kv store. Use the `-m export` command to retrieve it."); + Ok(()) + } + // else return failure + Err(err) => { + error!("Cannot put mnemonic in kv store: {:?}", err); + Err(KvErr(err)) + } + }, + // if we cannot reserve, return failure + Err(err) => { + error!("Cannot reserve mnemonic: {:?}", err); + Err(KvErr(err)) + } + } + } + + /// Creates a new entropy, inserts the entropy in the kv-store and exports it to a file + /// If a mnemonic already exists in the kv store or an exported file already exists in + /// the default path, an error is produced + async fn handle_create(&self) -> InnerMnemonicResult<()> { + info!("Creating mnemonic"); + // create a new entropy + let new_entropy = bip39_new_w24(); + self.handle_insert(new_entropy.clone()).await?; + Ok(self.io.entropy_to_file(new_entropy)?) + } + + /// Inserts a new mnemonic to the kv-store. + /// If a mnemonic already exists in the kv store, an error is produced by sled + /// trying to reserve an existing mnemonic key + async fn handle_import(&self) -> InnerMnemonicResult<()> { + info!("Importing mnemonic"); + let imported_phrase = Password(read_password().map_err(|e| PasswordErr(e.to_string()))?); + let imported_entropy = bip39_from_phrase(imported_phrase)?; + self.handle_insert(imported_entropy).await + } + + /// Exports the current mnemonic to a file + async fn handle_export(&self) -> InnerMnemonicResult<()> { + info!("Exporting mnemonic"); + + // try to get mnemonic from kv-store + let entropy = self.mnemonic_kv.get(MNEMONIC_KEY).await.map_err(|err| { + error!("Did not find mnemonic in kv store {:?}", err); + err + })?; + + // write to file + info!("Mnemonic found in kv store"); + Ok(self.io.entropy_to_file(entropy)?) + } +} + +#[cfg(test)] +mod tests { + use std::path::PathBuf; + use testdir::testdir; + + use crate::{ + config::Config, + encrypted_sled::get_test_password, + gg20::{ + mnemonic::{ + file_io::FileIo, + results::{file_io::FileIoError, mnemonic::InnerMnemonicError}, + }, + types::{KeySharesKv, MnemonicKv}, + }, + kv_manager::error::{InnerKvError, KvError}, + }; + + use super::*; + use tracing_test::traced_test; + + // create a service + fn get_service(testdir: PathBuf) -> Gg20Service { + // create test dirs for kvstores + let shares_kv_path = testdir.join("shares"); + let shares_kv_path = shares_kv_path.to_str().unwrap(); + let mnemonic_kv_path = testdir.join("mnemonic"); + let mnemonic_kv_path = mnemonic_kv_path.to_str().unwrap(); + + Gg20Service { + shares_kv: KeySharesKv::with_db_name(shares_kv_path.to_owned(), get_test_password()) + .unwrap(), + mnemonic_kv: MnemonicKv::with_db_name(mnemonic_kv_path.to_owned(), get_test_password()) + .unwrap(), + io: FileIo::new(testdir), + cfg: Config::default(), + } + } + + #[traced_test] + #[tokio::test] + async fn test_create() { + let testdir = testdir!(); + // create a service + let gg20 = get_service(testdir); + // first attempt should succeed + assert!(gg20.handle_create().await.is_ok()); + // second attempt should fail + assert!(matches!( + gg20.handle_create().await, + Err(InnerMnemonicError::KvErr(KvError::ReserveErr( + InnerKvError::LogicalErr(_) + ))) + )); + } + + #[traced_test] + #[tokio::test] + async fn test_insert() { + let testdir = testdir!(); + // create a service + let gg20 = get_service(testdir.clone()); + // insert should succeed + assert!(gg20.handle_insert(bip39_new_w24()).await.is_ok()); + // insert should fail + assert!(matches!( + gg20.handle_insert(bip39_new_w24()).await, + Err(InnerMnemonicError::KvErr(KvError::ReserveErr( + InnerKvError::LogicalErr(_) + ))) + )); + } + + #[traced_test] + #[tokio::test] + async fn test_export() { + let testdir = testdir!(); + // create a service + let gg20 = get_service(testdir.clone()); + // handle existing should fail + assert!(matches!( + gg20.handle_existing().await, + Err(InnerMnemonicError::KvErr(KvError::ExistsErr( + InnerKvError::LogicalErr(_) + ))) + )); + // mnemonic should not be exported + assert!(gg20.io.check_if_not_exported().is_ok()); + // create a new mnemonic + assert!(gg20.handle_create().await.is_ok()); + // mnemonic should now be exported + assert!(gg20.io.check_if_not_exported().is_err()); + // export should fail because create also exports + assert!(matches!( + gg20.handle_export().await, + Err(InnerMnemonicError::FileIoErr(FileIoError::Exists(_))) + )); + // handle existing should fail because export file exists + assert!(matches!( + gg20.handle_existing().await, + Err(InnerMnemonicError::FileIoErr(FileIoError::Exists(_))) + )); + } + + #[traced_test] + #[tokio::test] + async fn test_existing() { + let testdir = testdir!(); + // create a service + let gg20 = get_service(testdir.clone()); + // create a new mnemonic + assert!(gg20.handle_create().await.is_ok()); + // handle_existing should fail because export file exists + assert!(matches!( + gg20.handle_existing().await, + Err(InnerMnemonicError::FileIoErr(FileIoError::Exists(_))) + )); + // export should fail because export file exists + assert!(matches!( + gg20.handle_export().await, + Err(InnerMnemonicError::FileIoErr(FileIoError::Exists(_))) + )); + } +} diff --git a/src/gg20/mnemonic/file_io.rs b/src/gg20/mnemonic/file_io.rs index ec9dccc6..6dd3f1ec 100644 --- a/src/gg20/mnemonic/file_io.rs +++ b/src/gg20/mnemonic/file_io.rs @@ -1,96 +1,81 @@ -//! This module handles IO to files. More specifically, writing and reading passphrases. +//! This module handles file IO. -use std::{ - io::{Read, Write}, - path::PathBuf, -}; +use std::{io::Write, path::PathBuf}; use tracing::info; -use super::bip39_bindings::bip39_from_entropy; -use crate::gg20::types::{Entropy, Password}; +use super::{bip39_bindings::bip39_from_entropy, results::file_io::FileIoError::Exists}; +use crate::gg20::types::Entropy; -/// Standard names +/// name of export file const EXPORT_FILE: &str = "export"; -pub(super) const IMPORT_FILE: &str = "import"; -use super::error::file_io::FileIoResult; +use super::results::file_io::FileIoResult; /// FileIO wraps all IO functionality #[derive(Clone)] -pub(crate) struct FileIo { - path: PathBuf, +pub struct FileIo { + export_path: PathBuf, } impl FileIo { /// FileIO constructor - pub fn new(path: PathBuf) -> FileIo { - FileIo { path } + pub fn new(mut export_path: PathBuf) -> FileIo { + export_path.push(EXPORT_FILE); + FileIo { export_path } } - /// Get the next available "export" filename in self.path - /// If "export" exists, "export_2" is created. Then "export_3" etc. - fn next_filepath(&self) -> PathBuf { - let mut filepath = self.path.clone(); - filepath.push(EXPORT_FILE); - let mut id = 1; - while filepath.exists() { - filepath = self.path.clone(); - filepath.push(EXPORT_FILE.to_owned() + "_" + &id.to_string()); - id += 1; + /// Get the path of export file + pub fn export_path(&self) -> &PathBuf { + &self.export_path + } + + /// Check if an exported file exists in the expected path + /// Succeeds if no exported file exists, returns an error otherwise. + pub fn check_if_not_exported(&self) -> FileIoResult<()> { + if std::path::Path::new(&self.export_path()).exists() { + return Err(Exists(self.export_path().clone())); } - filepath + Ok(()) } /// Creates a file that contains an entropy in it's human-readable form - pub(super) fn entropy_to_next_file(&self, entropy: Entropy) -> FileIoResult<()> { + pub(super) fn entropy_to_file(&self, entropy: Entropy) -> FileIoResult<()> { // delegate zeroization for entropy; no need to worry about mnemonic, it is cleaned automatically let mnemonic = bip39_from_entropy(entropy)?; let phrase = mnemonic.phrase(); - let filepath = self.next_filepath(); - let mut file = std::fs::File::create(filepath.clone())?; + // if there is an existing exported file raise an error + self.check_if_not_exported()?; + let mut file = std::fs::File::create(&self.export_path())?; file.write_all(phrase.as_bytes())?; - info!("Mnemonic written in file {:?}", filepath); + info!("Mnemonic written in file {:?}", &self.export_path()); Ok(()) } - - /// Returns the phrase from a file - pub(super) fn phrase_from_file(&self, filename: &str) -> FileIoResult { - let mut filepath = self.path.clone(); - filepath.push(filename); - let mut file = std::fs::File::open(filepath)?; - let mut mnemonic_phrase = String::new(); - // if read_to_string fails, we don't need to worry about zeroizing mnemonic phrase; we never got it - file.read_to_string(&mut mnemonic_phrase)?; - Ok(Password(mnemonic_phrase)) - } } #[cfg(test)] mod tests { use super::*; use crate::gg20::mnemonic::bip39_bindings::{bip39_new_w24, tests::bip39_to_phrase}; + use std::io::Read; use testdir::testdir; use tracing_test::traced_test; #[traced_test] #[test] - fn test_write_read() { - let io = FileIo { path: testdir!() }; + fn test_write() { let entropy = bip39_new_w24(); - let entropy_copy = entropy.clone(); - let filepath = io.next_filepath(); - io.entropy_to_next_file(entropy).unwrap(); - let filename = filepath.file_name().unwrap().to_str().unwrap(); - let file_content = io.phrase_from_file(filename).unwrap(); - let expected_content = bip39_to_phrase(entropy_copy).unwrap(); - assert_eq!(file_content.0, expected_content.0); - } - #[test] - fn test_read() { - let io = FileIo { path: testdir!() }; - let file_content = io.phrase_from_file("non-existing-file"); - assert!(file_content.is_err()); + let io = FileIo::new(testdir!()); + let filepath = io.export_path(); + io.entropy_to_file(entropy.clone()).unwrap(); + let expected_content = bip39_to_phrase(entropy).unwrap(); + + let mut file = std::fs::File::open(filepath).unwrap(); + let mut file_phrase = String::new(); + file.read_to_string(&mut file_phrase).unwrap(); + let file_content = file_phrase; + + assert_eq!(file_content, expected_content.0); } } diff --git a/src/gg20/mnemonic/mod.rs b/src/gg20/mnemonic/mod.rs index 0a6e0311..c8a23787 100644 --- a/src/gg20/mnemonic/mod.rs +++ b/src/gg20/mnemonic/mod.rs @@ -1,279 +1,15 @@ -//! This module handles mnemonic-related commands. A kv-store is used to import, edit and export an [Entropy]. +//! This module handles mnemonic-related commands. A kv-store is used to insert and retrieve an [crate::gg20::Entropy]. //! //! Currently, the API supports the following [Cmd] commands: -//! [Cmd::Noop]: does nothing; Always succeeds; useful when the container restarts with the same mnemonic. -//! [Cmd::Create]: creates a new mnemonic; Creates a new mnemonic when none is already imported, otherwise does nothing. -//! [Cmd::Import]: adds a new mnemonic from "import" file; Succeeds when there is no other mnemonic already imported, fails otherwise. -//! [Cmd::Export]: writes the existing mnemonic to a file; Succeeds when there is an existing mnemonic, fails otherwise. -//! [Cmd::Update]: updates existing mnemonic from file "import"; Succeeds when there is an existing mnemonic, fails otherwise. -//! In [Cmd::Create], [Cmd::Export] commands, a new "export" file is created that contains the current phrase. -//! In [Cmd::Update] command, a new "export" file is created that contains the replaced pasphrase. - -pub mod bip39_bindings; // this also needed in tests -use bip39_bindings::{bip39_from_phrase, bip39_new_w24, bip39_seed}; - -pub(super) mod file_io; -use file_io::IMPORT_FILE; - -mod error; -use error::mnemonic::{ - InnerMnemonicError::*, InnerMnemonicResult, MnemonicError::*, MnemonicResult, SeedResult, -}; - -use super::{ - service::Gg20Service, - types::{Entropy, Password}, -}; -use tracing::{error, info}; - -// default key to store mnemonic -const MNEMONIC_KEY: &str = "mnemonic"; - -#[derive(Clone, Debug)] -pub enum Cmd { - Noop, - Create, - Import, - Update, - Export, -} - -impl Cmd { - pub fn from_string(cmd_str: &str) -> MnemonicResult { - let cmd = match cmd_str { - "stored" => Self::Noop, - "create" => Self::Create, - "import" => Self::Import, - "update" => Self::Update, - "export" => Self::Export, - _ => return Err(WrongCommand(cmd_str.to_string())), - }; - Ok(cmd) - } -} - -/// implement mnemonic-specific functions for Gg20Service -impl Gg20Service { - /// async function that handles all mnemonic commands - pub async fn handle_mnemonic(&self) -> MnemonicResult<()> { - match self.cfg.mnemonic_cmd { - Cmd::Noop => Ok(()), - Cmd::Create => self.handle_create().await.map_err(CreateErr), - Cmd::Import => self.handle_import().await.map_err(ImportErr), - Cmd::Update => self.handle_update().await.map_err(UpdateErr), - Cmd::Export => self.handle_export().await.map_err(ExportErr), - } - } - - /// inserts entropy to the kv-store and writes inserted value to an "export" file. - /// takes ownership of entropy to delegate zeroization. - async fn handle_insert(&self, entropy: Entropy) -> InnerMnemonicResult<()> { - // Don't use `map_err` to make it more readable. - let reservation = self.mnemonic_kv.reserve_key(MNEMONIC_KEY.to_owned()).await; - match reservation { - // if we can reserve, try put - Ok(reservation) => match self.mnemonic_kv.put(reservation, entropy.to_owned()).await { - // if put is ok, write the phrase to a file - Ok(()) => { - info!("Mnemonic successfully added in kv store"); - Ok(self.io.entropy_to_next_file(entropy)?) - } - // else return failure - Err(err) => { - error!("Cannot put mnemonic in kv store: {:?}", err); - Err(KvErr(err)) - } - }, - // if we cannot reserve, return failure - Err(err) => { - error!("Cannot reserve mnemonic: {:?}", err); - Err(KvErr(err)) - } - } - } - - /// Creates a new entropy and delegates 1) insertion to the kv-store, and 2) write to an "export" file - /// If a mnemonic already exists in the kv store, fall back to that - /// TODO: In the future we might want to throw an error here if mnemonic already exists. - async fn handle_create(&self) -> InnerMnemonicResult<()> { - info!("Creating mnemonic"); - // if we already have a mnemonic in kv-store, use that instead of creating a new one. - // we do this to use "create mnemonic" as the default behaviour for now. - if self.mnemonic_kv.get(MNEMONIC_KEY).await.is_ok() { - info!("Existing menomonic was found."); - return Ok(()); - } - - // create an entropy and zeroize after use - let new_entropy = bip39_new_w24(); - Ok(self.handle_insert(new_entropy).await?) - } - - // Inserts a new mnemonic to the kv-store, and writes the phrase to an "export" file - // Fails if a mnemonic already exists in the kv store - async fn handle_import(&self) -> InnerMnemonicResult<()> { - info!("Importing mnemonic"); - let imported_phrase = self.io.phrase_from_file(IMPORT_FILE)?; - let imported_entropy = bip39_from_phrase(imported_phrase)?; - self.handle_insert(imported_entropy).await - } - - /// Updates a mnemonic. - // 1. deletes the existing one - // 2. writes an "export" file with the deleted key - // 3. reads a new mnemonic from "import" file - // 4. delegates the insertions of the new mnemonics to the kv-store, and writes the phrase to an "export" file - // Fails if a mnemonic already exists in the kv store, of if no "import" file exists - async fn handle_update(&self) -> InnerMnemonicResult<()> { - info!("Updating mnemonic"); - - // try to delete the old mnemonic - let deleted_entropy = self.mnemonic_kv.remove(MNEMONIC_KEY).await.map_err(|err| { - error!("Delete error: {}", err); - KvErr(err) - })?; - - // try to write mnemonic to a new file - self.io.entropy_to_next_file(deleted_entropy)?; - - // try to insert new mnemonic - let new_phrase = self.io.phrase_from_file(IMPORT_FILE)?; - - // try to get entropy from passphrase - let new_entropy = bip39_from_phrase(new_phrase)?; - - // try to insert entropy to kv store - Ok(self.handle_insert(new_entropy).await?) - } - - /// Exports the current mnemonic to an "export" file - async fn handle_export(&self) -> InnerMnemonicResult<()> { - info!("Exporting mnemonic"); - - // try to get mnemonic from kv-store - let entropy = self.mnemonic_kv.get(MNEMONIC_KEY).await.map_err(|err| { - error!("Did not find mnemonic in kv store {:?}", err); - err - })?; - - // write to file - info!("Mnemonic found in kv store"); - Ok(self.io.entropy_to_next_file(entropy)?) - } -} - -use tofn::gg20::keygen::SecretRecoveryKey; -/// ease tofn API -impl Gg20Service { - pub async fn seed(&self) -> SeedResult { - use std::convert::TryInto; - let mnemonic = self.mnemonic_kv.get(MNEMONIC_KEY).await?; - // A user may decide to protect their mnemonic with a passphrase. If not, pass an empty password - // https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki#from-mnemonic-to-seed - Ok(bip39_seed(mnemonic, Password("".to_owned()))? - .as_bytes() - .try_into()?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::config::Config; - use crate::encrypted_sled; - use crate::gg20::mnemonic::{bip39_bindings::tests::bip39_to_phrase, file_io::FileIo}; - use crate::gg20::{KeySharesKv, MnemonicKv}; - use std::io::Write; - use std::path::PathBuf; - use testdir::testdir; - use tracing_test::traced_test; // logs for tests - - // create a service - fn get_service(testdir: PathBuf) -> Gg20Service { - // create test dirs for kvstores - let shares_kv_path = testdir.join("shares"); - let shares_kv_path = shares_kv_path.to_str().unwrap(); - let mnemonic_kv_path = testdir.join("mnemonic"); - let mnemonic_kv_path = mnemonic_kv_path.to_str().unwrap(); - - let password = encrypted_sled::get_test_password(); - Gg20Service { - shares_kv: KeySharesKv::with_db_name(shares_kv_path.to_owned(), password.clone()) - .unwrap(), - mnemonic_kv: MnemonicKv::with_db_name(mnemonic_kv_path.to_owned(), password).unwrap(), - io: FileIo::new(testdir), - cfg: Config::default(), - } - } - - fn create_import_file(mut path: PathBuf) { - path.push(IMPORT_FILE); - let create = std::fs::File::create(path); - if create.is_err() { - // file already exists. Don't do anything. - return; - } - let mut file = create.unwrap(); - - let entropy = bip39_new_w24(); - let phrase = bip39_to_phrase(entropy).unwrap(); - file.write_all(phrase.0.as_bytes()).unwrap(); - } - - #[traced_test] - #[tokio::test] - async fn test_create() { - let testdir = testdir!(); - // create a service - let gg20 = get_service(testdir); - // first attempt should succeed - assert!(gg20.handle_create().await.is_ok()); - // second attempt should also succeed - assert!(gg20.handle_create().await.is_ok()); - } - - #[traced_test] - #[tokio::test] - async fn test_import() { - let testdir = testdir!(); - // create a service - let gg20 = get_service(testdir.clone()); - create_import_file(testdir); - // first attempt should succeed - assert!(gg20.handle_import().await.is_ok()); - // second attempt should fail - assert!(gg20.handle_import().await.is_err()) - } - - #[traced_test] - #[tokio::test] - async fn test_update() { - let testdir = testdir!(); - // create a service - let gg20 = get_service(testdir.clone()); - // first attempt to update should fail - assert!(gg20.handle_update().await.is_err()); - create_import_file(testdir); - // import should succeed - assert!(gg20.handle_import().await.is_ok()); - // second attempt to update should succeed - assert!(gg20.handle_update().await.is_ok()); - // export should succeed - assert!(gg20.handle_export().await.is_ok()); - } - - #[traced_test] - #[tokio::test] - async fn test_export() { - let testdir = testdir!(); - // create a service - let gg20 = get_service(testdir.clone()); - // export should fail - assert!(gg20.handle_export().await.is_err()); - create_import_file(testdir); - // import should succeed - assert!(gg20.handle_import().await.is_ok()); - // export should now succeed - assert!(gg20.handle_export().await.is_ok()); - } -} +//! [Cmd::Existing]: Starts the gRPC daemon existing mnemonic; Fails if mnemonic does not exist. +//! [Cmd::Create]: Creates a new mnemonic, inserts it in the kv-store, exports it to a file and exits; Fails if a mnemonic exists. +//! [Cmd::Import]: Prompts user to give a new mnemonic, inserts it in the kv-store and exits; Fails if a mnemonic exists or if the provided string is not a valid bip39 mnemonic. +//! [Cmd::Export]: Writes the existing mnemonic to a file and exits; Succeeds when there is an existing mnemonic, fails otherwise. + +mod bip39_bindings; +mod cmd_handler; +mod file_io; +mod results; + +pub use cmd_handler::Cmd; +pub use file_io::FileIo; diff --git a/src/gg20/mnemonic/error.rs b/src/gg20/mnemonic/results.rs similarity index 83% rename from src/gg20/mnemonic/error.rs rename to src/gg20/mnemonic/results.rs index 0f588c73..6d3f7bf4 100644 --- a/src/gg20/mnemonic/error.rs +++ b/src/gg20/mnemonic/results.rs @@ -26,6 +26,10 @@ pub(super) mod file_io { Bip39(#[from] super::bip39::Bip39Error), #[error("File IO error {0}")] FileIo(#[from] std::io::Error), + #[error( + "File {0} already exists. Remove file to use `-m existing` or `-m export` commands." + )] + Exists(std::path::PathBuf), } pub type FileIoResult = Result; } @@ -37,10 +41,12 @@ pub(super) mod mnemonic { FileIoErr(#[from] super::file_io::FileIoError), #[error("KvStore error: {0}")] KvErr(#[from] crate::kv_manager::error::KvError), - #[error("Bip39 error: {0}")] + #[error("Invalid mnemonic. See https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki. Bip39 error: {0}")] Bip39Error(#[from] super::bip39::Bip39Error), #[error("Failed to convert to SecretRecoveryKey")] IntoSecretRecoveryKey(#[from] std::array::TryFromSliceError), + #[error("Password error: {0}")] + PasswordErr(String), } pub type InnerMnemonicResult = Result; @@ -48,14 +54,14 @@ pub(super) mod mnemonic { pub enum MnemonicError { #[error("Command not found: {0}")] WrongCommand(String), + #[error("Cannot not use existing mnemonic: {0}")] + ExistingErr(InnerMnemonicError), #[error("Cannot create mnemonic: {0}")] CreateErr(InnerMnemonicError), #[error("Cannot import mnemonic: {0}")] ImportErr(InnerMnemonicError), #[error("Cannot export mnemonic: {0}")] ExportErr(InnerMnemonicError), - #[error("Cannot update mnemonic: {0}")] - UpdateErr(InnerMnemonicError), } pub type MnemonicResult = Result; pub type SeedResult = Result; diff --git a/src/gg20/service/mod.rs b/src/gg20/service/mod.rs index 2a585657..8029d90f 100644 --- a/src/gg20/service/mod.rs +++ b/src/gg20/service/mod.rs @@ -1,6 +1,6 @@ //! This mod includes the service implementation derived from -use super::mnemonic::file_io::FileIo; +use super::mnemonic::FileIo; use super::proto; use super::types::{KeySharesKv, MnemonicKv, DEFAULT_MNEMONIC_KV_NAME, DEFAULT_SHARE_KV_NAME}; use crate::config::Config; diff --git a/src/kv_manager/error.rs b/src/kv_manager/error.rs index 7a4c47db..e3f13534 100644 --- a/src/kv_manager/error.rs +++ b/src/kv_manager/error.rs @@ -26,8 +26,6 @@ pub enum KvError { PutErr(InnerKvError), #[error("Get Error: {0}")] GetErr(InnerKvError), - #[error("Remove Error: {0}")] - RemoveErr(InnerKvError), #[error("Exits Error: {0}")] ExistsErr(InnerKvError), } diff --git a/src/kv_manager/kv.rs b/src/kv_manager/kv.rs index f90e87e8..fa5b7650 100644 --- a/src/kv_manager/kv.rs +++ b/src/kv_manager/kv.rs @@ -5,7 +5,7 @@ use crate::encrypted_sled::{self, Password}; use super::{ error::{KvError::*, KvResult}, - sled_bindings::{handle_exists, handle_get, handle_put, handle_remove, handle_reserve}, + sled_bindings::{handle_exists, handle_get, handle_put, handle_reserve}, types::{ Command::{self, *}, KeyReservation, DEFAULT_KV_PATH, @@ -106,19 +106,6 @@ where .map_err(|e| SendErr(e.to_string()))?; resp_rx.await?.map_err(ExistsErr) } - - /// Removes a key and its corresponding value from the kvstore - /// Returns [RemoveErr] or [SendErr] on failure. - pub async fn remove(&self, key: &str) -> KvResult { - let (resp_tx, resp_rx) = oneshot::channel(); - self.sender - .send(Remove { - key: key.to_string(), - resp: resp_tx, - }) - .map_err(|e| SendErr(e.to_string()))?; - resp_rx.await?.map_err(RemoveErr) - } } /// Returns the db with name `db_name`, or creates a new if such DB does not exist @@ -187,11 +174,6 @@ async fn kv_cmd_handler( warn!("receiver dropped"); } } - Remove { key, resp } => { - if resp.send(handle_remove(&kv, key)).is_err() { - warn!("receiver dropped"); - } - } } } info!("kv_manager stop"); diff --git a/src/kv_manager/sled_bindings.rs b/src/kv_manager/sled_bindings.rs index eab55047..9df33376 100644 --- a/src/kv_manager/sled_bindings.rs +++ b/src/kv_manager/sled_bindings.rs @@ -18,7 +18,7 @@ pub(super) fn handle_reserve( // If reserve key already exists inside our database, return an error if kv.contains_key(&key)? { return Err(LogicalErr(format!( - "kv_manager key <{}> already reserved", + "kv_manager key <{}> already reserved.", key ))); } @@ -46,7 +46,7 @@ where // https://docs.rs/sled/0.34.6/sled/struct.Tree.html#examples-4 if kv.get(&reservation.key)? != Some(sled::IVec::from(DEFAULT_RESERV)) { return Err(LogicalErr(format!( - "did not find reservation for key <{}> in kv store", + "did not find reservation for key <{}> in kv store.", reservation.key ))); } @@ -70,7 +70,7 @@ where let value = match kv.get(&key)? { Some(bytes) => deserialize(&bytes).ok_or(DeserializationErr)?, None => { - return Err(LogicalErr(format!("key <{}> does not have a value", key))); + return Err(LogicalErr(format!("key <{}> does not have a value.", key))); } }; @@ -88,21 +88,3 @@ pub(super) fn handle_exists(kv: &encrypted_sled::Db, key: &str) -> InnerKvResult )) }) } - -/// Deletes the key and it's value from the kv store. -/// Returns [SledErr] of [LogicalErr] on failure. -pub(super) fn handle_remove(kv: &encrypted_sled::Db, key: String) -> InnerKvResult -where - V: DeserializeOwned, -{ - // try to remove value of 'key' - let value = match kv.remove(&key)? { - Some(bytes) => deserialize(&bytes).ok_or(DeserializationErr)?, - None => { - return Err(LogicalErr(format!("key <{}> does not have a value", key))); - } - }; - - // return value - Ok(value) -} diff --git a/src/kv_manager/tests.rs b/src/kv_manager/tests.rs index afd09df3..38c895d5 100644 --- a/src/kv_manager/tests.rs +++ b/src/kv_manager/tests.rs @@ -2,7 +2,7 @@ use super::{ error::InnerKvError::LogicalErr, - sled_bindings::{handle_exists, handle_get, handle_put, handle_remove, handle_reserve}, + sled_bindings::{handle_exists, handle_get, handle_put, handle_reserve}, types::{KeyReservation, DEFAULT_RESERV}, }; use crate::encrypted_sled; @@ -177,38 +177,4 @@ fn test_exists() { let exists = handle_exists(&kv, &key); assert!(exists.is_ok()); assert!(exists.unwrap()); // check that the result is true - - // remove key - let _ = handle_remove::(&kv, key.clone()).unwrap(); - - // exists should fail - let exists = handle_exists(&kv, &key); - assert!(exists.is_ok()); - assert!(!exists.unwrap()); // check that the result is false -} - -#[test] -fn remove_success() { - let kv_name = testdir!(); - let kv = open_with_test_password(&kv_name).unwrap(); - - let key: String = "key".to_string(); - let value = "value"; - handle_reserve(&kv, key.clone()).unwrap(); - handle_put(&kv, KeyReservation { key: key.clone() }, value).unwrap(); - let res = handle_remove::(&kv, key).unwrap(); - assert_eq!(res, value); - clean_up(kv_name.to_str().unwrap(), kv); -} - -#[test] -fn remove_failure() { - let kv_name = testdir!(); - let kv = open_with_test_password(&kv_name).unwrap(); - - let key: String = "key".to_string(); - let err = handle_remove::(&kv, key).err().unwrap(); - assert!(matches!(err, LogicalErr(_))); - - clean_up(kv_name.to_str().unwrap(), kv); } diff --git a/src/kv_manager/types.rs b/src/kv_manager/types.rs index 91c60283..84087b2b 100644 --- a/src/kv_manager/types.rs +++ b/src/kv_manager/types.rs @@ -46,8 +46,4 @@ pub(super) enum Command { key: String, // TODO should be &str except lifetimes... resp: Responder, }, - Remove { - key: String, // TODO should be &str except lifetimes... - resp: Responder, - }, } diff --git a/src/main.rs b/src/main.rs index 9d08a8cf..3a8ce116 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,8 +70,14 @@ async fn main() -> TofndResult<()> { incoming.local_addr()? ); + let cmd = cfg.mnemonic_cmd.clone(); let my_service = gg20::service::new_service(cfg, password).await?; + if cmd.exit_after_cmd() { + info!("Tofnd exited after using command <{:?}>. Run `./tofnd -m existing` to execute gRPC daemon.", cmd); + return Ok(()); + } + let proto_service = proto::gg20_server::Gg20Server::new(my_service); tonic::transport::Server::builder() diff --git a/src/tests/mnemonic.rs b/src/tests/mnemonic.rs index 80f59125..85c72774 100644 --- a/src/tests/mnemonic.rs +++ b/src/tests/mnemonic.rs @@ -1,13 +1,9 @@ //! mnemonic tests at the TofndParty level -use std::path::Path; - use super::{InitParty, TofndParty}; -use crate::{gg20::mnemonic::Cmd, tests::mock::Party}; +use crate::gg20::mnemonic::Cmd; use testdir::testdir; -use tracing::info; -use tracing_test::traced_test; #[cfg(feature = "malicious")] use super::MaliciousData; @@ -20,44 +16,14 @@ fn dummy_init_party() -> InitParty { ) } -// copy "export" file to "import" file. -// File "export" constains the mnemonic created when the party was instantiated. -// Now that we restart the party, we need to import the mnemonic from "import" file. -fn copy_export_to_import(dir: &Path, party_index: usize) { - let p = format!("test-key-{:02}", party_index); - let path = dir.to_path_buf(); - let path = path.join(p); - let export_path = format!("{}/export", path.to_str().unwrap()); - let import_path = format!("{}/import", path.to_str().unwrap()); - info!("Copying {} to {}", export_path, import_path); - std::fs::copy(export_path, import_path).unwrap(); -} - -// check if export, export_2 and export_3 files exist and are the same -fn compare_export_files(dir: &Path, party_index: usize) -> bool { - let p = format!("test-key-{:02}", party_index); - let path = dir.to_path_buf(); - let path = path.join(p); - - let export_filename = format!("{}/export", path.to_str().unwrap()); - let export_2_filename = format!("{}/export_2", path.to_str().unwrap()); - let export_3_filename = format!("{}/export_3", path.to_str().unwrap()); - let export = - std::fs::read_to_string(export_filename).expect("Something went wrong reading the file"); - let export_2 = - std::fs::read_to_string(export_2_filename).expect("Something went wrong reading the file"); - let export_3 = - std::fs::read_to_string(export_3_filename).expect("Something went wrong reading the file"); - export == export_2 && export_2 == export_3 -} - +#[should_panic] #[tokio::test] -async fn mnemonic_noop() { +async fn mnemonic_existing() { let dir = testdir!(); // dummy init data let init_party = dummy_init_party(); - // Noop should succeed - let _ = TofndParty::new(init_party, Cmd::Noop, &dir).await; + // Existing should panic + let _ = TofndParty::new(init_party, Cmd::Existing, &dir).await; } #[tokio::test] @@ -69,51 +35,6 @@ async fn mnemonic_create() { let _ = TofndParty::new(init_party, Cmd::Create, &dir).await; } -#[traced_test] -#[tokio::test] -async fn mnemonic_create_update_export() { - let dir = testdir!(); - // create a mnemonic in "export" file - { - let p = TofndParty::new(dummy_init_party(), Cmd::Create, &dir).await; - p.shutdown().await; - } - // copy "export" to "import" - copy_export_to_import(&dir, 0); - // update from "import" file, save existing to "export_1" file - { - let p = TofndParty::new(dummy_init_party(), Cmd::Update, &dir).await; - p.shutdown().await; - } - // export to "export_3" file - { - let p = TofndParty::new(dummy_init_party(), Cmd::Export, &dir).await; - p.shutdown().await; - } - - assert!(compare_export_files(&dir, 0)); -} - -#[should_panic] -#[tokio::test] -async fn mnemonic_import_panic() { - let dir = testdir!(); - // dummy init data - let init_party = dummy_init_party(); - // import should fail - let _ = TofndParty::new(init_party, Cmd::Import, &dir).await; -} - -#[should_panic] -#[tokio::test] -async fn mnemonic_update_panic() { - let dir = testdir!(); - // dummy init data - let init_party = dummy_init_party(); - // update should fail - let _ = TofndParty::new(init_party, Cmd::Update, &dir).await; -} - #[should_panic] #[tokio::test] async fn mnemonic_export_panic() { diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 42c25c87..99fba105 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -23,6 +23,7 @@ use malicious::{MaliciousData, PartyMaliciousData}; mod mnemonic; +use crate::gg20::mnemonic::Cmd::{self, Create}; use proto::message_out::CriminalList; use tracing::info; @@ -218,6 +219,12 @@ async fn shutdown_party( (party_options, party_root) } +// deletes the share kv-store of a party's db path +fn delete_party_export(mut mnemonic_path: PathBuf) { + mnemonic_path.push("export"); + std::fs::remove_file(mnemonic_path).unwrap(); +} + // deletes the share kv-store of a party's db path fn delete_party_shares(mut party_db_path: PathBuf) { party_db_path.push("kvstore/shares"); @@ -226,9 +233,9 @@ fn delete_party_shares(mut party_db_path: PathBuf) { std::fs::remove_dir_all(party_db_path).unwrap(); } -// initailizes i-th party +// reinitializes i-th party // pass malicious data if we are running in malicious mode -async fn init_party( +async fn reinit_party( mut party_options: Vec>, party_index: usize, testdir: &Path, @@ -241,9 +248,8 @@ async fn init_party( malicious_data, ); - // assume party already has a mnemonic, so we pass Cmd::Noop - party_options[party_index] = - Some(TofndParty::new(init_party, crate::gg20::mnemonic::Cmd::Noop, testdir).await); + // here we assume that the party already has a mnemonic, so we pass Cmd::Existing + party_options[party_index] = Some(TofndParty::new(init_party, Cmd::Existing, testdir).await); party_options .into_iter() @@ -319,13 +325,16 @@ async fn restart_party( // shutdown party with party_index let (party_options, shutdown_db_path) = shutdown_party(parties, party_index).await; + // if we are going to restart, delete exported mnemonic to allow using Cmd::Existing + delete_party_export(shutdown_db_path.clone()); + if recover { // if we are going to recover, delete party's shares delete_party_shares(shutdown_db_path); } - // reinit party with - let mut parties = init_party( + // reinit party + let mut parties = reinit_party( party_options, party_index, dir, @@ -601,8 +610,7 @@ async fn init_parties( #[cfg(feature = "malicious")] &init_parties.malicious_data, ); - parties - .push(TofndParty::new(init_party, crate::gg20::mnemonic::Cmd::Create, testdir).await); + parties.push(TofndParty::new(init_party, Create, testdir).await); } let party_uids: Vec = (0..init_parties.party_count)