From ef4f84ff97dd9543669a8b4a37b20d718bd8d18b Mon Sep 17 00:00:00 2001 From: Philip Robinson Date: Thu, 4 Nov 2021 12:54:30 +0200 Subject: [PATCH] feat!: implement new CipherSeed and upgrade encryption KDF (#3505) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Description --- This PR adds a new CipherSeed implementation for use a seed for Key Derivation. The goal of the scheme is produce a wallet seed that is versioned, contains the birthday of the wallet, starting entropy of the wallet to seed key generation, can be enciphered with a passphrase and has a checksum. During this process it was noted that we used a naive method to derive our database encryption key from the passphrase. This PR also updates that method to use Argon2 as a proper password hashing scheme that is not vulnerable to rainbow table brute forcing and timing attacks. - Update db encryption key generation to use Argon2 KDF - Persist the Argon2 salted hash in the DB to detect when encryption has been applied - Implement a CipherSeed scheme based on aezeed that can be encoded using the Mnemonic seed words - Integrate the new CipherSeed into the KeyManagers - Update Wallet backend and Clients to use the new CipherSeeds Motivation and Context --- The CipherSeed scheme has three main benefits - It contains the seed birthday which means we perform recoveries more efficiently and not scan the whole blockchain - It contains a checksum to verify the seed phrase is correct - It can be encrypted with a passphrase and decrypted and authenticated. We don’t current’y use a passphrase on the seeds. That will be future work. How Has This Been Tested? --- Test have been updated --- Cargo.lock | 47 +- .../tari_console_wallet/src/init/mod.rs | 8 +- applications/tari_console_wallet/src/main.rs | 19 +- .../tari_console_wallet/src/recovery.rs | 22 +- base_layer/key_manager/Cargo.toml | 11 +- base_layer/key_manager/src/cipher_seed.rs | 383 ++++++++++++++++ base_layer/key_manager/src/error.rs | 62 +++ base_layer/key_manager/src/file_backup.rs | 122 ----- base_layer/key_manager/src/key_manager.rs | 179 ++------ base_layer/key_manager/src/lib.rs | 3 +- base_layer/key_manager/src/mnemonic.rs | 142 +----- base_layer/wallet/Cargo.toml | 2 + .../down.sql | 1 + .../up.sql | 2 + base_layer/wallet/src/error.rs | 11 +- .../src/output_manager_service/error.rs | 6 +- .../master_key_manager.rs | 33 +- .../wallet/src/output_manager_service/mod.rs | 12 +- .../src/output_manager_service/service.rs | 7 +- .../storage/database.rs | 5 +- .../storage/sqlite_db.rs | 77 ++-- base_layer/wallet/src/schema.rs | 2 +- base_layer/wallet/src/storage/database.rs | 78 ++-- base_layer/wallet/src/storage/sqlite_db.rs | 423 ++++++++---------- .../wallet/src/storage/sqlite_utilities.rs | 20 +- .../transaction_service/storage/sqlite_db.rs | 26 +- base_layer/wallet/src/util/encryption.rs | 17 +- base_layer/wallet/src/wallet.rs | 62 ++- .../tests/output_manager_service/service.rs | 50 ++- .../tests/output_manager_service/storage.rs | 5 +- .../tests/transaction_service/service.rs | 6 +- base_layer/wallet/tests/wallet/mod.rs | 59 +-- base_layer/wallet_ffi/src/error.rs | 4 +- base_layer/wallet_ffi/src/lib.rs | 46 +- 34 files changed, 1054 insertions(+), 898 deletions(-) create mode 100644 base_layer/key_manager/src/cipher_seed.rs create mode 100644 base_layer/key_manager/src/error.rs delete mode 100644 base_layer/key_manager/src/file_backup.rs create mode 100644 base_layer/wallet/migrations/2021-10-27-103505_update_key_manager_state/down.sql create mode 100644 base_layer/wallet/migrations/2021-10-27-103505_update_key_manager_state/up.sql diff --git a/Cargo.lock b/Cargo.lock index 0753ac7cfd..bc4e173dd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,6 +148,16 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dabe5a181f83789739c194cbe5a897dde195078fac08568d09221fd6137a7ba8" +[[package]] +name = "argon2" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5162d1b961cb589a8ca08a2aa7cabc6341e05e0bf18d66a07697900b5d2ad0" +dependencies = [ + "blake2", + "password-hash", +] + [[package]] name = "arrayref" version = "0.3.6" @@ -163,6 +173,12 @@ dependencies = [ "nodrop", ] +[[package]] +name = "arrayvec" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4dc07131ffa69b8072d35f5007352af944213cde02545e2103680baed38fcd" + [[package]] name = "async-stream" version = "0.3.2" @@ -258,6 +274,12 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6b4d9b1225d28d360ec6a231d65af1fd99a2a095154c8040689617290569c5c" + [[package]] name = "bigdecimal" version = "0.1.2" @@ -2663,7 +2685,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bafe4179722c2894288ee77a9f044f02811c86af699344c498b0840c698a2465" dependencies = [ - "arrayvec", + "arrayvec 0.4.12", "itoa", ] @@ -2903,6 +2925,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "password-hash" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e0b28ace46c5a396546bcf443bf422b57049617433d8854227352a4a9b24e7" +dependencies = [ + "base64ct", + "rand_core 0.6.3", + "subtle", +] + [[package]] name = "path-clean" version = "0.1.0" @@ -4624,11 +4657,15 @@ dependencies = [ name = "tari_key_manager" version = "0.12.0" dependencies = [ + "argon2", + "arrayvec 0.7.1", + "blake2", + "chacha20", + "chrono", + "clear_on_drop", + "crc32fast", "digest", "rand 0.8.4", - "serde 1.0.130", - "serde_derive", - "serde_json", "sha2", "tari_crypto", "thiserror", @@ -4894,10 +4931,12 @@ name = "tari_wallet" version = "0.12.0" dependencies = [ "aes-gcm 0.8.0", + "argon2", "async-trait", "bincode", "blake2", "chrono", + "clear_on_drop", "crossbeam-channel 0.3.9", "diesel", "diesel_migrations", diff --git a/applications/tari_console_wallet/src/init/mod.rs b/applications/tari_console_wallet/src/init/mod.rs index 2f2249d772..b74c1719f7 100644 --- a/applications/tari_console_wallet/src/init/mod.rs +++ b/applications/tari_console_wallet/src/init/mod.rs @@ -28,7 +28,6 @@ use rustyline::Editor; use tari_app_utilities::utilities::create_transport_type; use tari_common::{exit_codes::ExitCodes, ConfigBootstrap, GlobalConfig}; -use tari_common_types::types::PrivateKey; use tari_comms::{ peer_manager::{Peer, PeerFeatures}, types::CommsSecretKey, @@ -36,6 +35,7 @@ use tari_comms::{ }; use tari_comms_dht::{store_forward::SafConfig, DbConnectionUrl, DhtConfig}; use tari_core::transactions::CryptoFactories; +use tari_key_manager::cipher_seed::CipherSeed; use tari_p2p::{ auto_update::AutoUpdateConfig, initialization::P2pConfig, @@ -254,7 +254,7 @@ pub async fn init_wallet( config: &GlobalConfig, arg_password: Option, seed_words_file_name: Option, - recovery_master_key: Option, + recovery_seed: Option, shutdown_signal: ShutdownSignal, ) -> Result { fs::create_dir_all( @@ -411,7 +411,7 @@ pub async fn init_wallet( output_manager_backend, contacts_backend, shutdown_signal, - recovery_master_key.clone(), + recovery_seed.clone(), ) .await .map_err(|e| { @@ -453,7 +453,7 @@ pub async fn init_wallet( debug!(target: LOG_TARGET, "Wallet encrypted."); - if interactive && recovery_master_key.is_none() { + if interactive && recovery_seed.is_none() { match confirm_seed_words(&mut wallet).await { Ok(()) => { print!("\x1Bc"); // Clear the screen diff --git a/applications/tari_console_wallet/src/main.rs b/applications/tari_console_wallet/src/main.rs index 65e4f83ee7..c463d83c0d 100644 --- a/applications/tari_console_wallet/src/main.rs +++ b/applications/tari_console_wallet/src/main.rs @@ -6,7 +6,7 @@ #![deny(unreachable_patterns)] #![deny(unknown_lints)] #![recursion_limit = "1024"] -use crate::{recovery::get_private_key_from_seed_words, wallet_modes::WalletModeConfig}; +use crate::{recovery::get_seed_from_seed_words, wallet_modes::WalletModeConfig}; use init::{ boot, change_password, @@ -24,7 +24,7 @@ use recovery::prompt_private_key_from_seed_words; use std::{env, process}; use tari_app_utilities::{consts, initialization::init_configuration}; use tari_common::{configuration::bootstrap::ApplicationType, exit_codes::ExitCodes, ConfigBootstrap}; -use tari_common_types::types::PrivateKey; +use tari_key_manager::cipher_seed::CipherSeed; use tari_shutdown::Shutdown; use tracing_subscriber::{layer::SubscriberExt, Registry}; use wallet_modes::{command_mode, grpc_mode, recovery_mode, script_mode, tui_mode, WalletMode}; @@ -89,7 +89,7 @@ fn main_inner() -> Result<(), ExitCodes> { // check for recovery based on existence of wallet file let mut boot_mode = boot(&bootstrap, &global_config)?; - let recovery_master_key: Option = get_recovery_master_key(boot_mode, &bootstrap)?; + let recovery_seed: Option = get_recovery_seed(boot_mode, &bootstrap)?; if bootstrap.init { info!(target: LOG_TARGET, "Default configuration created. Done."); @@ -112,7 +112,7 @@ fn main_inner() -> Result<(), ExitCodes> { &global_config, arg_password, seed_words_file_name, - recovery_master_key, + recovery_seed, shutdown_signal, ))?; @@ -165,12 +165,9 @@ fn main_inner() -> Result<(), ExitCodes> { result } -fn get_recovery_master_key( - boot_mode: WalletBoot, - bootstrap: &ConfigBootstrap, -) -> Result, ExitCodes> { +fn get_recovery_seed(boot_mode: WalletBoot, bootstrap: &ConfigBootstrap) -> Result, ExitCodes> { if matches!(boot_mode, WalletBoot::Recovery) { - let private_key = if bootstrap.seed_words.is_some() { + let seed = if bootstrap.seed_words.is_some() { let seed_words: Vec = bootstrap .seed_words .clone() @@ -178,11 +175,11 @@ fn get_recovery_master_key( .split_whitespace() .map(|v| v.to_string()) .collect(); - get_private_key_from_seed_words(seed_words)? + get_seed_from_seed_words(seed_words)? } else { prompt_private_key_from_seed_words()? }; - Ok(Some(private_key)) + Ok(Some(seed)) } else { Ok(None) } diff --git a/applications/tari_console_wallet/src/recovery.rs b/applications/tari_console_wallet/src/recovery.rs index dde9f8a769..d3c247977a 100644 --- a/applications/tari_console_wallet/src/recovery.rs +++ b/applications/tari_console_wallet/src/recovery.rs @@ -25,9 +25,8 @@ use futures::FutureExt; use log::*; use rustyline::Editor; use tari_common::exit_codes::ExitCodes; -use tari_common_types::types::PrivateKey; use tari_crypto::tari_utilities::hex::Hex; -use tari_key_manager::mnemonic::to_secretkey; +use tari_key_manager::mnemonic::Mnemonic; use tari_shutdown::Shutdown; use tari_wallet::{ storage::sqlite_db::WalletSqliteDatabase, @@ -36,12 +35,13 @@ use tari_wallet::{ }; use crate::wallet_modes::PeerConfig; +use tari_key_manager::cipher_seed::CipherSeed; use tokio::sync::broadcast; pub const LOG_TARGET: &str = "wallet::recovery"; /// Prompt the user to input their seed words in a single line. -pub fn prompt_private_key_from_seed_words() -> Result { +pub fn prompt_private_key_from_seed_words() -> Result { debug!(target: LOG_TARGET, "Prompting for seed words."); let mut rl = Editor::<()>::new(); @@ -52,8 +52,8 @@ pub fn prompt_private_key_from_seed_words() -> Result { let input = rl.readline(">> ").map_err(|e| ExitCodes::IOError(e.to_string()))?; let seed_words: Vec = input.split_whitespace().map(str::to_string).collect(); - match to_secretkey(&seed_words) { - Ok(key) => break Ok(key), + match CipherSeed::from_mnemonic(&seed_words, None) { + Ok(seed) => break Ok(seed), Err(e) => { debug!(target: LOG_TARGET, "MnemonicError parsing seed words: {}", e); println!("Failed to parse seed words! Did you type them correctly?"); @@ -63,14 +63,14 @@ pub fn prompt_private_key_from_seed_words() -> Result { } } -/// Return secret key matching the seed words. -pub fn get_private_key_from_seed_words(seed_words: Vec) -> Result { - debug!(target: LOG_TARGET, "Return secret key matching the provided seed words"); - match to_secretkey(&seed_words) { - Ok(key) => Ok(key), +/// Return seed matching the seed words. +pub fn get_seed_from_seed_words(seed_words: Vec) -> Result { + debug!(target: LOG_TARGET, "Return seed derived from the provided seed words"); + match CipherSeed::from_mnemonic(&seed_words, None) { + Ok(seed) => Ok(seed), Err(e) => { let err_msg = format!("MnemonicError parsing seed words: {}", e); - debug!(target: LOG_TARGET, "{}", err_msg); + warn!(target: LOG_TARGET, "{}", err_msg); Err(ExitCodes::RecoveryError(err_msg)) }, } diff --git a/base_layer/key_manager/Cargo.toml b/base_layer/key_manager/Cargo.toml index 6a02245766..62e5d364ee 100644 --- a/base_layer/key_manager/Cargo.toml +++ b/base_layer/key_manager/Cargo.toml @@ -9,11 +9,16 @@ edition = "2018" [dependencies] tari_crypto = { git = "https://github.com/tari-project/tari-crypto.git", branch = "main" } + +arrayvec = "0.7.1" +argon2 = { version = "0.2", features = ["std"] } +blake2 = "0.9.1" +chacha20 = "0.7.1" +chrono = { version = "0.4.6", features = ["serde"] } +clear_on_drop = "=0.2.4" +crc32fast = "1.2.1" rand = "0.8" digest = "0.9.0" -serde = "1.0.89" -serde_derive = "1.0.89" -serde_json = "1.0.39" thiserror = "1.0.26" [dev-dependencies] diff --git a/base_layer/key_manager/src/cipher_seed.rs b/base_layer/key_manager/src/cipher_seed.rs new file mode 100644 index 0000000000..543071041d --- /dev/null +++ b/base_layer/key_manager/src/cipher_seed.rs @@ -0,0 +1,383 @@ +// Copyright 2021. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use crate::{ + error::KeyManagerError, + mnemonic::{from_bytes, to_bytes, to_bytes_with_language, Mnemonic, MnemonicLanguage}, +}; +use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; +use arrayvec::ArrayVec; +use blake2::{digest::VariableOutput, VarBlake2b}; +use chacha20::{ + cipher::{NewCipher, StreamCipher}, + ChaCha20, + Key, + Nonce, +}; +use chrono::Utc; +use crc32fast::Hasher as CrcHasher; +use digest::Update; +use rand::{rngs::OsRng, RngCore}; +use std::{convert::TryFrom, mem::size_of}; +use tari_crypto::tari_utilities::ByteArray; + +const CIPHER_SEED_VERSION: u8 = 0u8; +pub const DEFAULT_CIPHER_SEED_PASSPHRASE: &str = "TARI_CIPHER_SEED"; +pub const CIPHER_SEED_ENTROPY_BYTES: usize = 16; +pub const CIPHER_SEED_SALT_BYTES: usize = 5; +pub const CIPHER_SEED_MAC_BYTES: usize = 5; + +/// This is an implementation of a Cipher Seed based on the `aezeed` encoding scheme (https://github.com/lightningnetwork/lnd/tree/master/aezeed) +/// The goal of the scheme is produce a wallet seed that is versioned, contains the birthday of the wallet, starting +/// entropy of the wallet to seed key generation, can be enciphered with a passphrase and has a checksum. +/// The `aezeed` scheme uses a new AEZ AEAD scheme which allows for enciphering arbitrary length texts and choosing +/// custom MAC sizes. AEZ is unfortunately not available in the RustCrypto implementations yet so we use a similar AEAD +/// scheme using the primitives available in RustCrypto. +/// Our scheme must be able to be represented with the 24 word seed phrase using the BIP-39 word lists. The world lists +/// contain 2048 words which are 11 bits of information giving us a total of 33 bytes to work with for the final +/// encoding. +/// In our scheme we will have the following data: +/// version 1 byte +/// birthday 2 bytes Days after Unix Epoch +/// entropy 16 bytes +/// MAC 5 bytes Hash(birthday||entropy||version||salt||passphrase) +/// salt 5 bytes +/// checksum 4 bytes +/// +/// In it's enciphered form we will use the MAC-the-Encrypt pattern of AE so that the birthday and entropy will be +/// encrypted. The version and salt are associated data that are included in the MAC but not encrypted. +/// The enciphered data will look as follows: +/// version 1 byte +/// ciphertext 23 bytes +/// salt 5 bytes +/// checksum 4 bytes +/// +/// The final 33 byte enciphered data is what will be encoded using the Mnemonic Word lists to create a 24 word seed +/// phrase. +/// +/// The checksum allows us to confirm that a given seed phrase decodes into an intact enciphered CipherSeed. +/// The MAC allows us to confirm that a given passphrase correctly decrypts the CipherSeed and that the version and salt +/// are not tampered with. If no passphrase is provided a default string will be used +/// +/// The Birthday is included to enable more efficient recoveries. Knowing the birthday of the seed phrase means we only +/// have to scan the blocks in the chain since that day to fully recover rather than scan the entire blockchain + +#[derive(Clone, Debug, PartialEq)] +pub struct CipherSeed { + version: u8, + birthday: u16, + pub entropy: [u8; CIPHER_SEED_ENTROPY_BYTES], + salt: [u8; CIPHER_SEED_SALT_BYTES], +} + +impl CipherSeed { + pub fn new() -> Self { + let mut entropy = [0u8; CIPHER_SEED_ENTROPY_BYTES]; + OsRng.fill_bytes(&mut entropy); + let mut salt = [0u8; CIPHER_SEED_SALT_BYTES]; + OsRng.fill_bytes(&mut salt); + + let birthday = u16::try_from(Utc::now().timestamp() as u64 / (24 * 60 * 60)).unwrap_or(0u16); + + Self { + version: CIPHER_SEED_VERSION, + birthday, + entropy, + salt, + } + } + + pub fn encipher(&self, passphrase: Option) -> Result, KeyManagerError> { + let mut plaintext = self.birthday.to_le_bytes().to_vec(); + plaintext.append(&mut self.entropy.clone().to_vec()); + + let passphrase = passphrase.unwrap_or_else(|| DEFAULT_CIPHER_SEED_PASSPHRASE.to_string()); + + // Construct HMAC and include the version and salt as Associated Data + let blake2_mac_hasher: VarBlake2b = + VarBlake2b::new(CIPHER_SEED_MAC_BYTES).expect("Should be able to create blake2 hasher"); + let mut hmac = [0u8; CIPHER_SEED_MAC_BYTES]; + blake2_mac_hasher + .chain(plaintext.clone()) + .chain([CIPHER_SEED_VERSION]) + .chain(self.salt) + .chain(passphrase.as_bytes()) + .finalize_variable(|res| hmac.copy_from_slice(res)); + + plaintext.append(&mut hmac.to_vec()); + + Self::apply_stream_cipher(&mut plaintext, &passphrase, &self.salt)?; + + let mut final_seed = vec![CIPHER_SEED_VERSION]; + final_seed.append(&mut plaintext.to_vec()); + final_seed.append(&mut self.salt.to_vec()); + + let mut crc_hasher = CrcHasher::new(); + crc_hasher.update(final_seed.as_slice()); + let checksum = crc_hasher.finalize(); + final_seed.append(&mut checksum.to_le_bytes().to_vec()); + Ok(final_seed) + } + + pub fn from_enciphered_bytes(enciphered_bytes: &[u8], passphrase: Option) -> Result { + // 1 byte Version || 2 byte Birthday || 16 byte Entropy || 5 byte MAC || 5 byte salt || 4 byte CRC32 + if enciphered_bytes.len() != 7 + CIPHER_SEED_ENTROPY_BYTES + CIPHER_SEED_SALT_BYTES + CIPHER_SEED_MAC_BYTES { + return Err(KeyManagerError::InvalidData); + } + + if enciphered_bytes[0] != CIPHER_SEED_VERSION { + return Err(KeyManagerError::VersionMismatch); + } + + let passphrase = passphrase.unwrap_or_else(|| DEFAULT_CIPHER_SEED_PASSPHRASE.to_string()); + + let mut body = enciphered_bytes.to_owned(); + // extract 32 bit checksum + let checksum_vec = body.split_off(body.len() - 4); + + let mut crc_hasher = CrcHasher::new(); + crc_hasher.update(body.as_slice()); + + let calculated_checksum = crc_hasher.finalize(); + + let mut checksum_bytes: [u8; 4] = [0u8; 4]; + checksum_bytes.copy_from_slice(&checksum_vec[..4]); + let checksum = u32::from_le_bytes(checksum_bytes); + + if calculated_checksum != checksum { + return Err(KeyManagerError::CrcError); + } + + let salt = body.split_off(body.len() - CIPHER_SEED_SALT_BYTES); + let mut enciphered_seed = body.split_off(1); + let received_version = body[0]; + + Self::apply_stream_cipher(&mut enciphered_seed, &passphrase, salt.as_slice())?; + + let decrypted_hmac = enciphered_seed.split_off(enciphered_seed.len() - CIPHER_SEED_MAC_BYTES); + + let decrypted_entropy_vec: ArrayVec<_, CIPHER_SEED_ENTROPY_BYTES> = + enciphered_seed.split_off(2).into_iter().collect(); + let decrypted_entropy = decrypted_entropy_vec + .into_inner() + .map_err(|_| KeyManagerError::InvalidData)?; + + let mut birthday_bytes: [u8; 2] = [0u8; 2]; + birthday_bytes.copy_from_slice(&enciphered_seed); + let decrypted_birthday = u16::from_le_bytes(birthday_bytes); + + let blake2_mac_hasher: VarBlake2b = + VarBlake2b::new(CIPHER_SEED_MAC_BYTES).expect("Should be able to create blake2 hasher"); + let mut hmac = [0u8; CIPHER_SEED_MAC_BYTES]; + blake2_mac_hasher + .chain(&birthday_bytes) + .chain(&decrypted_entropy) + .chain([CIPHER_SEED_VERSION]) + .chain(salt.as_slice()) + .chain(passphrase.as_bytes()) + .finalize_variable(|res| hmac.copy_from_slice(res)); + + if decrypted_hmac != hmac.to_vec() { + return Err(KeyManagerError::DecryptionFailed); + } + + let salt_vec: ArrayVec<_, CIPHER_SEED_SALT_BYTES> = salt.into_iter().collect(); + let salt_bytes = salt_vec.into_inner().map_err(|_| KeyManagerError::InvalidData)?; + + Ok(Self { + version: received_version, + birthday: decrypted_birthday, + entropy: decrypted_entropy, + salt: salt_bytes, + }) + } + + fn apply_stream_cipher(data: &mut Vec, passphrase: &str, salt: &[u8]) -> Result<(), KeyManagerError> { + let argon2 = Argon2::default(); + let blake2_nonce_hasher: VarBlake2b = + VarBlake2b::new(size_of::()).expect("Should be able to create blake2 hasher"); + + let mut encryption_nonce = [0u8; size_of::()]; + blake2_nonce_hasher + .chain(salt) + .finalize_variable(|res| encryption_nonce.copy_from_slice(res)); + let nonce_ga = Nonce::from_slice(&encryption_nonce); + + // Create salt string stretched to the chacha nonce size, we only have space for 5 bytes of salt in the seed but + // will use key stretching to produce a longer nonce for the passphrase hash and the encryption nonce. + let salt_b64 = SaltString::b64_encode(&encryption_nonce)?; + + let derived_encryption_key = argon2 + .hash_password_simple(passphrase.as_bytes(), salt_b64.as_str())? + .hash + .ok_or_else(|| KeyManagerError::CryptographicError("Problem generating encryption key hash".to_string()))?; + let key = Key::from_slice(derived_encryption_key.as_bytes()); + let mut cipher = ChaCha20::new(key, nonce_ga); + cipher.apply_keystream(data.as_mut_slice()); + + Ok(()) + } +} + +impl Drop for CipherSeed { + fn drop(&mut self) { + use clear_on_drop::clear::Clear; + Clear::clear(&mut self.entropy); + } +} + +impl Default for CipherSeed { + fn default() -> Self { + Self::new() + } +} + +impl Mnemonic for CipherSeed { + /// Generates a CipherSeed that represent the provided mnemonic sequence of words, the language of the mnemonic + /// sequence is autodetected + fn from_mnemonic(mnemonic_seq: &[String], passphrase: Option) -> Result { + let bytes = to_bytes(mnemonic_seq)?; + CipherSeed::from_enciphered_bytes(&bytes, passphrase) + } + + /// Generates a SecretKey that represent the provided mnemonic sequence of words using the specified language + fn from_mnemonic_with_language( + mnemonic_seq: &[String], + language: &MnemonicLanguage, + passphrase: Option, + ) -> Result { + let bytes = to_bytes_with_language(mnemonic_seq, language)?; + CipherSeed::from_enciphered_bytes(&bytes, passphrase) + } + + /// Generates a mnemonic sequence of words from the provided secret key + fn to_mnemonic( + &self, + language: &MnemonicLanguage, + passphrase: Option, + ) -> Result, KeyManagerError> { + Ok(from_bytes(self.encipher(passphrase)?, language)?) + } +} + +#[cfg(test)] +mod test { + use crate::{ + cipher_seed::CipherSeed, + error::KeyManagerError, + mnemonic::{Mnemonic, MnemonicLanguage}, + }; + + #[test] + fn test_cipher_seed_generation_and_deciphering() { + let seed = CipherSeed::new(); + + let mut enciphered_seed = seed.encipher(Some("Passphrase".to_string())).unwrap(); + + let deciphered_seed = + CipherSeed::from_enciphered_bytes(&enciphered_seed, Some("Passphrase".to_string())).unwrap(); + assert_eq!(seed, deciphered_seed); + + match CipherSeed::from_enciphered_bytes(&enciphered_seed, Some("WrongPassphrase".to_string())) { + Err(KeyManagerError::DecryptionFailed) => (), + _ => panic!("Version should not match"), + } + + enciphered_seed[0] = 1; + + match CipherSeed::from_enciphered_bytes(&enciphered_seed, Some("Passphrase".to_string())) { + Err(KeyManagerError::VersionMismatch) => (), + _ => panic!("Version should not match"), + } + + enciphered_seed[0] = 0; + // Prevent the 1 our 256 chances that it was already a zero + if enciphered_seed[1] == 0 { + enciphered_seed[1] = 1; + } else { + enciphered_seed[1] = 0; + } + match CipherSeed::from_enciphered_bytes(&enciphered_seed, Some("Passphrase".to_string())) { + Err(KeyManagerError::CrcError) => (), + _ => panic!("Crc should not match"), + } + } + + #[test] + fn test_cipher_seed_to_mnemonic_and_from_mnemonic() { + // Valid Mnemonic sequence + let seed = CipherSeed::new(); + match seed.to_mnemonic(&MnemonicLanguage::Japanese, None) { + Ok(mnemonic_seq) => { + match CipherSeed::from_mnemonic(&mnemonic_seq, None) { + Ok(mnemonic_seed) => assert_eq!(seed, mnemonic_seed), + Err(e) => panic!("Couldn't create CipherSeed from Mnemonic: {}", e), + } + // Language known + match CipherSeed::from_mnemonic_with_language(&mnemonic_seq, &MnemonicLanguage::Japanese, None) { + Ok(mnemonic_seed) => assert_eq!(seed, mnemonic_seed), + Err(_e) => panic!("Couldn't create CipherSeed from Mnemonic with Language"), + } + }, + Err(_e) => panic!("Couldn't convert CipherSeed to Mnemonic"), + } + // Invalid Mnemonic sequence + let mnemonic_seq = vec![ + "stay", "what", "minor", "stay", "olive", "clip", "buyer", "know", "report", "obey", "pen", "door", "type", + "cover", "vote", "federal", "husband", "cave", "alone", "dynamic", "reopen", "visa", "young", "gas", + ] + .iter() + .map(|x| x.to_string()) + .collect::>(); + // Language not known + match CipherSeed::from_mnemonic(&mnemonic_seq, None) { + Ok(_k) => panic!(), + Err(_e) => {}, + } + // Language known + match CipherSeed::from_mnemonic_with_language(&mnemonic_seq, &MnemonicLanguage::Japanese, None) { + Ok(_k) => panic!(), + Err(_e) => {}, + } + } + + #[test] + fn cipher_seed_to_and_from_mnemonic_with_passphrase() { + let seed = CipherSeed::new(); + match seed.to_mnemonic(&MnemonicLanguage::Spanish, Some("Passphrase".to_string())) { + Ok(mnemonic_seq) => match CipherSeed::from_mnemonic(&mnemonic_seq, Some("Passphrase".to_string())) { + Ok(mnemonic_seed) => assert_eq!(seed, mnemonic_seed), + Err(e) => panic!("Couldn't create CipherSeed from Mnemonic: {}", e), + }, + Err(_e) => panic!("Couldn't convert CipherSeed to Mnemonic"), + } + + match seed.to_mnemonic(&MnemonicLanguage::Spanish, Some("Passphrase".to_string())) { + Ok(mnemonic_seq) => { + if CipherSeed::from_mnemonic(&mnemonic_seq, Some("WrongPassphrase".to_string())).is_ok() { + panic!("Should not be able to derive seed with wrong passphrase"); + } + }, + Err(_e) => panic!("Couldn't convert CipherSeed to Mnemonic"), + } + } +} diff --git a/base_layer/key_manager/src/error.rs b/base_layer/key_manager/src/error.rs new file mode 100644 index 0000000000..14b79ea058 --- /dev/null +++ b/base_layer/key_manager/src/error.rs @@ -0,0 +1,62 @@ +// Copyright 2021. The Tari Project +// +// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +// following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +// disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote +// products derived from this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +use argon2::password_hash::Error as PasswordHashError; +use tari_crypto::tari_utilities::ByteArrayError; +use thiserror::Error; + +#[derive(Debug, Error, PartialEq)] +pub enum KeyManagerError { + #[error("Could not convert into byte array: `{0}`")] + ByteArrayError(#[from] ByteArrayError), + #[error("Mnemonic Error: `{0}`")] + MnemonicError(#[from] MnemonicError), + #[error("Error with password hashing: `{0}`")] + PasswordHashError(#[from] PasswordHashError), + #[error("Cryptographic operation error: `{0}`")] + CryptographicError(String), + #[error("Cannot parse CipherSeed from the provided vector, it is of the incorrect length")] + InvalidData, + #[error("CipherSeed CRC32 validation failed")] + CrcError, + #[error("Invalid CipherSeed version")] + VersionMismatch, + #[error("Decrypted data failed Version or MAC validation")] + DecryptionFailed, +} + +#[derive(Debug, Error, PartialEq)] +pub enum MnemonicError { + #[error( + "Only ChineseSimplified, ChineseTraditional, English, French, Italian, Japanese, Korean and Spanish are \ + defined natural languages" + )] + UnknownLanguage, + #[error("Only 2048 words for each language was selected to form Mnemonic word lists")] + WordNotFound, + #[error("A mnemonic word does not exist for the requested index")] + IndexOutOfBounds, + #[error("A problem encountered constructing a secret key from bytes or mnemonic sequence: `{0}`")] + ByteArrayError(#[from] ByteArrayError), + #[error("Encoding a mnemonic sequence to bytes requires exactly 24 mnemonic words")] + EncodeInvalidLength, +} diff --git a/base_layer/key_manager/src/file_backup.rs b/base_layer/key_manager/src/file_backup.rs deleted file mode 100644 index 9e92a6dd3a..0000000000 --- a/base_layer/key_manager/src/file_backup.rs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright 2019 The Tari Project -// -// Redistribution and use in source and binary forms, with or without modification, are permitted provided that the -// following conditions are met: -// -// 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following -// disclaimer. -// -// 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the -// following disclaimer in the documentation and/or other materials provided with the distribution. -// -// 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote -// products derived from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, -// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, -// WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE -// USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -use serde::de::DeserializeOwned; -use std::{fs::File, io::prelude::*}; -use thiserror::Error; - -// TODO: file should be decrypted using Salsa20 or ChaCha20 - -#[derive(Debug, Error)] -pub enum FileError { - #[error("The specified backup file could not be created")] - FileCreate, - #[error("The specified backup file could not be opened")] - FileOpen, - #[error("Could not read from backup file")] - FileRead, - #[error("Could not write to backup file")] - FileWrite, - #[error("Problem serializing struct into JSON")] - Serialize, - #[error("Problem deserializing JSON into a new struct")] - Deserialize, -} - -pub trait FileBackup { - fn from_file(filename: &str) -> Result; - fn to_file(&self, filename: &str) -> Result<(), FileError>; -} - -impl FileBackup for T -where T: serde::Serialize + DeserializeOwned -{ - /// Load struct state from backup file - fn from_file(filename: &str) -> Result { - let mut file_handle = match File::open(&filename) { - Ok(file) => file, - Err(_e) => return Err(FileError::FileOpen), - }; - let mut file_content = String::new(); - match file_handle.read_to_string(&mut file_content) { - Ok(_) => match serde_json::from_str(&file_content) { - Ok(km) => Ok(km), - Err(_) => Err(FileError::Deserialize), - }, - Err(_) => Err(FileError::FileRead), - } - } - - /// Backup struct state in file specified by filename - fn to_file(&self, filename: &str) -> Result<(), FileError> { - match File::create(filename) { - Ok(mut file_handle) => match serde_json::to_string(&self) { - Ok(json_data) => match file_handle.write_all(json_data.as_bytes()) { - Ok(_) => Ok(()), - Err(_) => Err(FileError::FileWrite), - }, - Err(_) => Err(FileError::Serialize), - }, - - Err(_) => Err(FileError::FileCreate), - } - } -} - -#[cfg(test)] -mod test { - use crate::file_backup::*; - use serde_derive::{Deserialize, Serialize}; - use std::fs::remove_file; - - #[test] - fn test_struct_to_file_and_from_file() { - #[derive(Debug, PartialEq, Serialize, Deserialize)] - pub struct R { - pub var1: String, - pub var2: Vec, - pub var3: usize, - } - let desired_struct = R { - var1: "Test".to_string(), - var2: vec![0, 1, 2], - var3: 3, - }; - // Backup struct to file - let backup_filename = "test_backup.json".to_string(); - match desired_struct.to_file(&backup_filename) { - Ok(_v) => { - // Restore struct from file - let backup_result: Result = R::from_file(&backup_filename); - match backup_result { - Ok(backup_struct) => { - // Remove temp backup file - remove_file(backup_filename).unwrap(); - assert_eq!(desired_struct, backup_struct); - }, - Err(_e) => panic!(), - }; - }, - Err(_e) => panic!(), - }; - } -} diff --git a/base_layer/key_manager/src/key_manager.rs b/base_layer/key_manager/src/key_manager.rs index 2a6cafe326..3ca14c409a 100644 --- a/base_layer/key_manager/src/key_manager.rs +++ b/base_layer/key_manager/src/key_manager.rs @@ -20,25 +20,13 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use crate::mnemonic; +use crate::cipher_seed::CipherSeed; use digest::Digest; -use rand::{CryptoRng, Rng}; -use serde::de::DeserializeOwned; -use serde_derive::{Deserialize, Serialize}; use std::marker::PhantomData; use tari_crypto::{ keys::SecretKey, tari_utilities::{byte_array::ByteArrayError, hex::Hex}, }; -use thiserror::Error; - -#[derive(Debug, Error, PartialEq)] -pub enum KeyManagerError { - #[error("Could not convert into byte array: `{0}`")] - ByteArrayError(#[from] ByteArrayError), - #[error("Could not convert provided Mnemonic into master key: `{0}`")] - MnemonicError(#[from] mnemonic::MnemonicError), -} #[derive(Clone, Debug)] pub struct DerivedKey @@ -48,77 +36,45 @@ where K: SecretKey pub key_index: u64, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq)] pub struct KeyManager { - master_key: K, + seed: CipherSeed, pub branch_seed: String, primary_key_index: u64, digest_type: PhantomData, + key_type: PhantomData, } impl KeyManager where - K: SecretKey + serde::Serialize + DeserializeOwned + mnemonic::Mnemonic, + K: SecretKey, D: Digest, { - /// Creates a new KeyManager with a new randomly selected master_key - pub fn new(rng: &mut R) -> KeyManager { + /// Creates a new KeyManager with a new randomly selected entropy + pub fn new() -> KeyManager { KeyManager { - master_key: SecretKey::random(rng), + seed: CipherSeed::new(), branch_seed: "".to_string(), primary_key_index: 0, digest_type: PhantomData, + key_type: PhantomData, } } /// Constructs a KeyManager from known parts - pub fn from(master_key: K, branch_seed: String, primary_key_index: u64) -> KeyManager { + pub fn from(seed: CipherSeed, branch_seed: String, primary_key_index: u64) -> KeyManager { KeyManager { - master_key, + seed, branch_seed, primary_key_index, digest_type: PhantomData, - } - } - - /// Constructs a KeyManager by generating a master_key=SHA256(seed_phrase) using a non-mnemonic seed phrase - pub fn from_seed_phrase( - seed_phrase: String, - branch_seed: String, - primary_key_index: u64, - ) -> Result, KeyManagerError> { - match K::from_bytes(D::digest(&seed_phrase.into_bytes()).as_slice()) { - Ok(master_key) => Ok(KeyManager { - master_key, - branch_seed, - primary_key_index, - digest_type: PhantomData, - }), - Err(e) => Err(KeyManagerError::from(e)), - } - } - - /// Creates a KeyManager from the provided sequence of mnemonic words, the language of the mnemonic sequence will be - /// auto detected - pub fn from_mnemonic( - mnemonic_seq: &[String], - branch_seed: String, - primary_key_index: u64, - ) -> Result, KeyManagerError> { - match K::from_mnemonic(mnemonic_seq) { - Ok(master_key) => Ok(KeyManager { - master_key, - branch_seed, - primary_key_index, - digest_type: PhantomData, - }), - Err(e) => Err(KeyManagerError::from(e)), + key_type: PhantomData, } } /// Derive a new private key from master key: derived_key=SHA256(master_key||branch_seed||index) pub fn derive_key(&self, key_index: u64) -> Result, ByteArrayError> { - let concatenated = format!("{}{}", self.master_key.to_hex(), key_index.to_string()); + let concatenated = format!("{}{}", self.seed.entropy.to_vec().to_hex(), key_index.to_string()); match K::from_bytes(D::digest(&concatenated.into_bytes()).as_slice()) { Ok(k) => Ok(DerivedKey { k, key_index }), Err(e) => Err(e), @@ -131,8 +87,8 @@ where self.derive_key(self.primary_key_index) } - pub fn master_key(&self) -> &K { - &self.master_key + pub fn cipher_seed(&self) -> &CipherSeed { + &self.seed } pub fn key_index(&self) -> u64 { @@ -144,67 +100,32 @@ where } } +impl Default for KeyManager +where + K: SecretKey, + D: Digest, +{ + fn default() -> Self { + Self::new() + } +} + #[cfg(test)] mod test { - use crate::{file_backup::*, key_manager::*}; - use rand::rngs::OsRng; + use crate::key_manager::*; use sha2::Sha256; - use std::fs::remove_file; use tari_crypto::ristretto::RistrettoSecretKey; #[test] fn test_new_keymanager() { - let km1 = KeyManager::::new(&mut OsRng); - let km2 = KeyManager::::new(&mut OsRng); - assert_ne!(km1.master_key, km2.master_key); - } - - #[test] - fn test_from_seed_phrase() { - let seed_phrase1 = "random seed phrase".to_string(); - let seed_phrase2 = "additional random Seed phrase".to_string(); - let branch_seed = "".to_string(); - let km1 = KeyManager::::from_seed_phrase(seed_phrase1, branch_seed.clone(), 0); - let km2 = KeyManager::::from_seed_phrase(seed_phrase2, branch_seed, 0); - if km1.is_ok() && km2.is_ok() { - assert_ne!(km1.unwrap().master_key, km2.unwrap().master_key); - } else { - panic!() - } - } - - #[test] - fn test_from_mnemonic() { - let mnemonic_seq1 = vec![ - "clever", "jaguar", "bus", "engage", "oil", "august", "media", "high", "trick", "remove", "tiny", "join", - "item", "tobacco", "orange", "pony", "tomorrow", "also", "dignity", "giraffe", "little", "board", "army", - "scale", - ] - .iter() - .map(|x| x.to_string()) - .collect::>(); - let mnemonic_seq2 = vec![ - "spatial", "travel", "remove", "few", "cinnamon", "three", "drift", "grit", "amazing", "isolate", "merge", - "tonight", "apple", "garden", "damage", "job", "equal", "ahead", "wolf", "initial", "woman", "regret", - "neither", "divorce", - ] - .iter() - .map(|x| x.to_string()) - .collect::>(); - let branch_seed = "".to_string(); - let km1 = KeyManager::::from_mnemonic(&mnemonic_seq1, branch_seed.clone(), 0); - let km2 = KeyManager::::from_mnemonic(&mnemonic_seq2, branch_seed, 0); - - if km1.is_ok() && km2.is_ok() { - assert_ne!(km1.unwrap().master_key, km2.unwrap().master_key); - } else { - panic!() - } + let km1 = KeyManager::::new(); + let km2 = KeyManager::::new(); + assert_ne!(km1.seed, km2.seed); } #[test] fn test_derive_and_next_key() { - let mut km = KeyManager::::new(&mut OsRng); + let mut km = KeyManager::::new(); let next_key1_result = km.next_key(); let next_key2_result = km.next_key(); let desired_key_index1 = 1; @@ -223,28 +144,22 @@ mod test { } #[test] - fn test_to_file_and_from_file() { - let desired_km = KeyManager::::new(&mut OsRng); - let backup_filename = "test_km_backup.json".to_string(); - // Backup KeyManager to file - match desired_km.to_file(&backup_filename) { - Ok(_v) => { - // Restore KeyManager from file - let backup_km_result: Result, FileError> = - KeyManager::from_file(&backup_filename); - match backup_km_result { - Ok(backup_km) => { - // Remove temp key_manager backup file - remove_file(backup_filename).unwrap(); - - assert_eq!(desired_km.branch_seed, backup_km.branch_seed); - assert_eq!(desired_km.master_key, backup_km.master_key); - assert_eq!(desired_km.primary_key_index, backup_km.primary_key_index); - }, - Err(_e) => panic!(), - }; - }, - Err(_e) => panic!(), - }; + fn test_derive_and_next_key_with_branch_seed() { + let mut km = KeyManager::::from(CipherSeed::new(), "Test".to_string(), 0); + let next_key1_result = km.next_key(); + let next_key2_result = km.next_key(); + let desired_key_index1 = 1; + let desired_key_index2 = 2; + let derived_key1_result = km.derive_key(desired_key_index1); + let derived_key2_result = km.derive_key(desired_key_index2); + let next_key1 = next_key1_result.unwrap(); + let next_key2 = next_key2_result.unwrap(); + let derived_key1 = derived_key1_result.unwrap(); + let derived_key2 = derived_key2_result.unwrap(); + assert_ne!(next_key1.k, next_key2.k); + assert_eq!(next_key1.k, derived_key1.k); + assert_eq!(next_key2.k, derived_key2.k); + assert_eq!(next_key1.key_index, desired_key_index1); + assert_eq!(next_key2.key_index, desired_key_index2); } } diff --git a/base_layer/key_manager/src/lib.rs b/base_layer/key_manager/src/lib.rs index 99570b6c15..f2387e7b4b 100644 --- a/base_layer/key_manager/src/lib.rs +++ b/base_layer/key_manager/src/lib.rs @@ -5,8 +5,9 @@ #![deny(unused_must_use)] #![deny(unreachable_patterns)] #![deny(unknown_lints)] +pub mod cipher_seed; pub mod diacritics; -pub mod file_backup; +pub mod error; pub mod key_manager; pub mod mnemonic; pub mod mnemonic_wordlists; diff --git a/base_layer/key_manager/src/mnemonic.rs b/base_layer/key_manager/src/mnemonic.rs index 05ad54273a..14a1994fb9 100644 --- a/base_layer/key_manager/src/mnemonic.rs +++ b/base_layer/key_manager/src/mnemonic.rs @@ -20,35 +20,18 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use crate::{diacritics::*, mnemonic_wordlists::*}; -use std::slice::Iter; -use tari_crypto::{ - keys::SecretKey, - tari_utilities::{bit::*, byte_array::ByteArrayError}, +use crate::{ + diacritics::*, + error::{KeyManagerError, MnemonicError}, + mnemonic_wordlists::*, }; -use thiserror::Error; +use std::slice::Iter; +use tari_crypto::tari_utilities::bit::*; /// The Mnemonic system simplifies the encoding and decoding of a secret key into and from a Mnemonic word sequence /// It can autodetect the language of the Mnemonic word sequence // TODO: Develop a language autodetection mechanism to distinguish between ChineseTraditional and ChineseSimplified -#[derive(Debug, Error, PartialEq)] -pub enum MnemonicError { - #[error( - "Only ChineseSimplified, ChineseTraditional, English, French, Italian, Japanese, Korean and Spanish are \ - defined natural languages" - )] - UnknownLanguage, - #[error("Only 2048 words for each language was selected to form Mnemonic word lists")] - WordNotFound, - #[error("A mnemonic word does not exist for the requested index")] - IndexOutOfBounds, - #[error("A problem encountered constructing a secret key from bytes or mnemonic sequence: `{0}`")] - ByteArrayError(#[from] ByteArrayError), - #[error("Encoding a mnemonic sequence to bytes requires exactly 24 mnemonic words")] - EncodeInvalidLength, -} - #[derive(Clone, Debug, PartialEq)] pub enum MnemonicLanguage { ChineseSimplified, @@ -161,11 +144,6 @@ pub fn from_bytes(bytes: Vec, language: &MnemonicLanguage) -> Result(k: &K, language: &MnemonicLanguage) -> Result, MnemonicError> { - from_bytes(k.to_vec(), language) -} - /// Generates a vector of bytes that represent the provided mnemonic sequence of words, the language of the mnemonic /// sequence is autodetected pub fn to_bytes(mnemonic_seq: &[String]) -> Result, MnemonicError> { @@ -186,63 +164,28 @@ pub fn to_bytes_with_language(mnemonic_seq: &[String], language: &MnemonicLangua Err(err) => return Err(err), } } - // Discard unused bytes - let mut bytes = bits_to_bytes(&bits); - for _i in 32..bytes.len() { - bytes.pop(); - } - if bytes.len() == 32 { + let bytes = bits_to_bytes(&bits); + + if bytes.len() == 33 { Ok(bytes) } else { Err(MnemonicError::EncodeInvalidLength) } } -/// Generates a SecretKey that represents the provided mnemonic sequence of words. -/// The language of the mnemonic sequence is autodetected. -pub fn to_secretkey(mnemonic_seq: &[String]) -> Result { - let bytes = to_bytes(mnemonic_seq)?; - match K::from_bytes(&bytes) { - Ok(k) => Ok(k), - Err(e) => Err(MnemonicError::from(e)), - } -} - -/// Generates a SecretKey that represent the provided mnemonic sequence of words using the specified language -pub fn to_secretkey_with_language( - mnemonic_seq: &[String], - language: &MnemonicLanguage, -) -> Result { - let bytes = to_bytes_with_language(mnemonic_seq, language)?; - match K::from_bytes(&bytes) { - Ok(k) => Ok(k), - Err(e) => Err(MnemonicError::from(e)), - } -} - pub trait Mnemonic { - fn from_mnemonic(mnemonic_seq: &[String]) -> Result; - fn from_mnemonic_with_language(mnemonic_seq: &[String], language: &MnemonicLanguage) -> Result; - fn to_mnemonic(&self, language: &MnemonicLanguage) -> Result, MnemonicError>; -} - -impl Mnemonic for T { - /// Generates a SecretKey that represent the provided mnemonic sequence of words, the language of the mnemonic - /// sequence is autodetected - fn from_mnemonic(mnemonic_seq: &[String]) -> Result { - to_secretkey(mnemonic_seq) - } - - /// Generates a SecretKey that represent the provided mnemonic sequence of words using the specified language - fn from_mnemonic_with_language(mnemonic_seq: &[String], language: &MnemonicLanguage) -> Result { - to_secretkey_with_language(mnemonic_seq, language) - } - - /// Generates a mnemonic sequence of words from the provided secret key - fn to_mnemonic(&self, language: &MnemonicLanguage) -> Result, MnemonicError> { - from_secret_key(self, language) - } + fn from_mnemonic(mnemonic_seq: &[String], passphrase: Option) -> Result; + fn from_mnemonic_with_language( + mnemonic_seq: &[String], + language: &MnemonicLanguage, + passphrase: Option, + ) -> Result; + fn to_mnemonic( + &self, + language: &MnemonicLanguage, + passphrase: Option, + ) -> Result, KeyManagerError>; } #[cfg(test)] @@ -268,12 +211,6 @@ mod test { } } - #[test] - fn test_to_secret_key_no_words() { - let err = to_secretkey::(&[]).unwrap_err(); - assert!(matches!(err, MnemonicError::EncodeInvalidLength)); - } - #[test] fn test_language_detection() { // Test valid Mnemonic words @@ -421,43 +358,4 @@ mod test { Err(_e) => panic!(), } } - - #[test] - fn test_secretkey_to_mnemonic_and_from_mnemonic() { - // Valid Mnemonic sequence - let desired_k = RistrettoSecretKey::random(&mut OsRng); - match desired_k.to_mnemonic(&MnemonicLanguage::Japanese) { - Ok(mnemonic_seq) => { - match RistrettoSecretKey::from_mnemonic(&mnemonic_seq) { - Ok(mnemonic_k) => assert_eq!(desired_k, mnemonic_k), - Err(_e) => panic!(), - } - // Language known - match RistrettoSecretKey::from_mnemonic_with_language(&mnemonic_seq, &MnemonicLanguage::Japanese) { - Ok(mnemonic_k) => assert_eq!(desired_k, mnemonic_k), - Err(_e) => panic!(), - } - }, - Err(_e) => panic!(), - } - - // Invalid Mnemonic sequence - let mnemonic_seq = vec![ - "clever", "jaguar", "bus", "engage", "oil", "august", "media", "high", "trick", "remove", "tiny", "join", - "item", "tobacco", "orange", "pny", "tomorrow", "also", "dignity", "giraffe", "little", "board", "army", - ] - .iter() - .map(|x| x.to_string()) - .collect::>(); - // Language not known - match RistrettoSecretKey::from_mnemonic(&mnemonic_seq) { - Ok(_k) => panic!(), - Err(_e) => {}, - } - // Language known - match RistrettoSecretKey::from_mnemonic_with_language(&mnemonic_seq, &MnemonicLanguage::Japanese) { - Ok(_k) => panic!(), - Err(_e) => {}, - } - } } diff --git a/base_layer/wallet/Cargo.toml b/base_layer/wallet/Cargo.toml index 0113dfb986..1be418e923 100644 --- a/base_layer/wallet/Cargo.toml +++ b/base_layer/wallet/Cargo.toml @@ -20,9 +20,11 @@ tari_storage = { version = "^0.12", path = "../../infrastructure/storage" } aes-gcm = "^0.8" async-trait = "0.1.50" +argon2 = "0.2" bincode = "1.3.1" blake2 = "0.9.0" chrono = { version = "0.4.6", features = ["serde"] } +clear_on_drop = "=0.2.4" crossbeam-channel = "0.3.8" diesel = { version = "1.4.7", features = ["sqlite", "serde_json", "chrono"] } diesel_migrations = "1.4.0" diff --git a/base_layer/wallet/migrations/2021-10-27-103505_update_key_manager_state/down.sql b/base_layer/wallet/migrations/2021-10-27-103505_update_key_manager_state/down.sql new file mode 100644 index 0000000000..291a97c5ce --- /dev/null +++ b/base_layer/wallet/migrations/2021-10-27-103505_update_key_manager_state/down.sql @@ -0,0 +1 @@ +-- This file should undo anything in `up.sql` \ No newline at end of file diff --git a/base_layer/wallet/migrations/2021-10-27-103505_update_key_manager_state/up.sql b/base_layer/wallet/migrations/2021-10-27-103505_update_key_manager_state/up.sql new file mode 100644 index 0000000000..6cb8af3c9b --- /dev/null +++ b/base_layer/wallet/migrations/2021-10-27-103505_update_key_manager_state/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE main.key_manager_states + RENAME COLUMN master_key TO seed; \ No newline at end of file diff --git a/base_layer/wallet/src/error.rs b/base_layer/wallet/src/error.rs index a79548ac0e..e007a95144 100644 --- a/base_layer/wallet/src/error.rs +++ b/base_layer/wallet/src/error.rs @@ -40,6 +40,7 @@ use tari_comms::{ use tari_comms_dht::store_forward::StoreAndForwardError; use tari_core::transactions::transaction::TransactionError; use tari_crypto::tari_utilities::{hex::HexError, ByteArrayError}; +use tari_key_manager::error::KeyManagerError; use tari_p2p::{initialization::CommsInitializationError, services::liveness::error::LivenessError}; use tari_service_framework::ServiceInitializationError; use thiserror::Error; @@ -84,6 +85,8 @@ pub enum WalletError { ByteArrayError(#[from] tari_crypto::tari_utilities::ByteArrayError), #[error("Utxo Scanner Error: {0}")] UtxoScannerError(#[from] UtxoScannerError), + #[error("Key manager error: `{0}`")] + KeyManagerError(#[from] KeyManagerError), } pub const LOG_TARGET: &str = "tari::application"; @@ -132,6 +135,8 @@ pub enum WalletStorageError { HexError(#[from] HexError), #[error("Invalid Encryption Cipher was provided to database")] InvalidEncryptionCipher, + #[error("Invalid passphrase was provided")] + InvalidPassphrase, #[error("Missing Nonce in encrypted data")] MissingNonce, #[error("Aead error: `{0}`")] @@ -148,10 +153,10 @@ pub enum WalletStorageError { IoError(#[from] std::io::Error), #[error("No password provided for encrypted wallet")] NoPasswordError, - #[error("Incorrect password provided for encrypted wallet")] - IncorrectPassword, #[error("Deprecated operation error")] DeprecatedOperation, + #[error("Key Manager Error: `{0}`")] + KeyManagerError(#[from] KeyManagerError), } impl From for ExitCodes { @@ -159,7 +164,7 @@ impl From for ExitCodes { use WalletStorageError::*; match err { NoPasswordError => ExitCodes::NoPassword, - IncorrectPassword => ExitCodes::IncorrectPassword, + InvalidPassphrase => ExitCodes::IncorrectPassword, e => ExitCodes::WalletError(e.to_string()), } } diff --git a/base_layer/wallet/src/output_manager_service/error.rs b/base_layer/wallet/src/output_manager_service/error.rs index 8d6d8a20ac..f82092d9c7 100644 --- a/base_layer/wallet/src/output_manager_service/error.rs +++ b/base_layer/wallet/src/output_manager_service/error.rs @@ -31,7 +31,7 @@ use tari_core::transactions::{ CoinbaseBuildError, }; use tari_crypto::{script::ScriptError, tari_utilities::ByteArrayError}; -use tari_key_manager::{key_manager::KeyManagerError, mnemonic::MnemonicError}; +use tari_key_manager::error::{KeyManagerError, MnemonicError}; use tari_service_framework::reply_channel::TransportChannelError; use thiserror::Error; use time::OutOfRangeError; @@ -109,7 +109,7 @@ pub enum OutputManagerError { #[error("Tari script error : {0}")] ScriptError(#[from] ScriptError), #[error("Master secret key does not match persisted key manager state")] - MasterSecretKeyMismatch, + MasterSeedMismatch, #[error("Private Key is not found in the current Key Chain")] KeyNotFoundInKeyChain, #[error("Connectivity error: {source}")] @@ -167,6 +167,8 @@ pub enum OutputManagerStorageError { AeadError(String), #[error("Tari script error : {0}")] ScriptError(#[from] ScriptError), + #[error("Key Manager Error: `{0}`")] + KeyManagerError(#[from] KeyManagerError), } impl From for ExitCodes { diff --git a/base_layer/wallet/src/output_manager_service/master_key_manager.rs b/base_layer/wallet/src/output_manager_service/master_key_manager.rs index 38e5ed66bf..919581c010 100644 --- a/base_layer/wallet/src/output_manager_service/master_key_manager.rs +++ b/base_layer/wallet/src/output_manager_service/master_key_manager.rs @@ -34,8 +34,9 @@ use tari_common_types::types::{PrivateKey, PublicKey}; use tari_core::transactions::transaction_protocol::RewindData; use tari_crypto::{keys::PublicKey as PublicKeyTrait, range_proof::REWIND_USER_MESSAGE_LENGTH}; use tari_key_manager::{ + cipher_seed::CipherSeed, key_manager::KeyManager, - mnemonic::{from_secret_key, MnemonicLanguage}, + mnemonic::{Mnemonic, MnemonicLanguage}, }; const LOG_TARGET: &str = "wallet::output_manager_service::master_key_manager"; @@ -59,15 +60,12 @@ pub(crate) struct MasterKeyManager { impl MasterKeyManager where TBackend: OutputManagerBackend + 'static { - pub async fn new( - master_secret_key: PrivateKey, - db: OutputManagerDatabase, - ) -> Result { + pub async fn new(master_seed: CipherSeed, db: OutputManagerDatabase) -> Result { // Check to see if there is any persisted state. If there is confirm that the provided master secret key matches let key_manager_state = match db.get_key_manager_state().await? { None => { let starting_state = KeyManagerState { - master_key: master_secret_key, + seed: master_seed, branch_seed: "".to_string(), primary_key_index: 0, }; @@ -75,46 +73,46 @@ where TBackend: OutputManagerBackend + 'static starting_state }, Some(km) => { - if km.master_key != master_secret_key { - return Err(OutputManagerError::MasterSecretKeyMismatch); + if km.seed != master_seed { + return Err(OutputManagerError::MasterSeedMismatch); } km }, }; let utxo_key_manager = KeyManager::::from( - key_manager_state.master_key.clone(), + key_manager_state.seed.clone(), key_manager_state.branch_seed, key_manager_state.primary_key_index, ); let utxo_script_key_manager = KeyManager::::from( - key_manager_state.master_key.clone(), + key_manager_state.seed.clone(), KEY_MANAGER_SCRIPT_BRANCH_KEY.to_string(), key_manager_state.primary_key_index, ); let coinbase_key_manager = KeyManager::::from( - key_manager_state.master_key.clone(), + key_manager_state.seed.clone(), KEY_MANAGER_COINBASE_BRANCH_KEY.to_string(), 0, ); let coinbase_script_key_manager = KeyManager::::from( - key_manager_state.master_key.clone(), + key_manager_state.seed.clone(), KEY_MANAGER_COINBASE_SCRIPT_BRANCH_KEY.to_string(), 0, ); let rewind_key_manager = KeyManager::::from( - key_manager_state.master_key.clone(), + key_manager_state.seed.clone(), KEY_MANAGER_RECOVERY_VIEWONLY_BRANCH_KEY.to_string(), 0, ); let rewind_key = rewind_key_manager.derive_key(0)?.k; let rewind_blinding_key_manager = KeyManager::::from( - key_manager_state.master_key, + key_manager_state.seed, KEY_MANAGER_RECOVERY_BLINDING_BRANCH_KEY.to_string(), 0, ); @@ -173,10 +171,9 @@ where TBackend: OutputManagerBackend + 'static /// Return the Seed words for the current Master Key set in the Key Manager pub async fn get_seed_words(&self, language: &MnemonicLanguage) -> Result, OutputManagerError> { - Ok(from_secret_key( - self.utxo_key_manager.lock().await.master_key(), - language, - )?) + let km = self.utxo_key_manager.lock().await; + let seed_words = (*km).cipher_seed().to_mnemonic(language, None)?; + Ok(seed_words) } /// Return the public rewind keys diff --git a/base_layer/wallet/src/output_manager_service/mod.rs b/base_layer/wallet/src/output_manager_service/mod.rs index 27430cfaf1..d4d4a80727 100644 --- a/base_layer/wallet/src/output_manager_service/mod.rs +++ b/base_layer/wallet/src/output_manager_service/mod.rs @@ -33,8 +33,8 @@ use crate::{ use futures::future; use log::*; pub(crate) use master_key_manager::MasterKeyManager; -use tari_comms::types::CommsSecretKey; use tari_core::{consensus::NetworkConsensus, transactions::CryptoFactories}; +use tari_key_manager::cipher_seed::CipherSeed; use tari_service_framework::{ async_trait, reply_channel, @@ -63,7 +63,7 @@ where T: OutputManagerBackend backend: Option, factories: CryptoFactories, network: NetworkConsensus, - master_secret_key: CommsSecretKey, + master_seed: CipherSeed, } impl OutputManagerServiceInitializer @@ -74,14 +74,14 @@ where T: OutputManagerBackend + 'static backend: T, factories: CryptoFactories, network: NetworkConsensus, - master_secret_key: CommsSecretKey, + master_seed: CipherSeed, ) -> Self { Self { config, backend: Some(backend), factories, network, - master_secret_key, + master_seed, } } } @@ -111,7 +111,7 @@ where T: OutputManagerBackend + 'static let factories = self.factories.clone(); let config = self.config.clone(); let constants = self.network.create_consensus_constants().pop().unwrap(); - let master_secret_key = self.master_secret_key.clone(); + let master_seed = self.master_seed.clone(); context.spawn_when_ready(move |handles| async move { let base_node_service_handle = handles.expect_handle::(); let connectivity = handles.expect_handle::(); @@ -126,7 +126,7 @@ where T: OutputManagerBackend + 'static handles.get_shutdown_signal(), base_node_service_handle, connectivity, - master_secret_key, + master_seed, ) .await .expect("Could not initialize Output Manager Service") diff --git a/base_layer/wallet/src/output_manager_service/service.rs b/base_layer/wallet/src/output_manager_service/service.rs index cb9881b8f7..26f83a0a98 100644 --- a/base_layer/wallet/src/output_manager_service/service.rs +++ b/base_layer/wallet/src/output_manager_service/service.rs @@ -48,7 +48,7 @@ use tari_common_types::{ transaction::TxId, types::{PrivateKey, PublicKey}, }; -use tari_comms::types::{CommsPublicKey, CommsSecretKey}; +use tari_comms::types::CommsPublicKey; use tari_core::{ consensus::{ConsensusConstants, ConsensusEncodingSized, ConsensusEncodingWrapper}, transactions::{ @@ -69,6 +69,7 @@ use tari_crypto::{ script::TariScript, tari_utilities::{hex::Hex, ByteArray}, }; +use tari_key_manager::cipher_seed::CipherSeed; use tari_service_framework::reply_channel; use tari_shutdown::ShutdownSignal; @@ -105,13 +106,13 @@ where shutdown_signal: ShutdownSignal, base_node_service: BaseNodeServiceHandle, connectivity: TWalletConnectivity, - master_secret_key: CommsSecretKey, + master_seed: CipherSeed, ) -> Result { // Clear any encumberances for transactions that were being negotiated but did not complete to become official // Pending Transactions. db.clear_short_term_encumberances().await?; - let master_key_manager = MasterKeyManager::new(master_secret_key, db.clone()).await?; + let master_key_manager = MasterKeyManager::new(master_seed, db.clone()).await?; let resources = OutputManagerResources { config, diff --git a/base_layer/wallet/src/output_manager_service/storage/database.rs b/base_layer/wallet/src/output_manager_service/storage/database.rs index 97d61ad374..127472d2d9 100644 --- a/base_layer/wallet/src/output_manager_service/storage/database.rs +++ b/base_layer/wallet/src/output_manager_service/storage/database.rs @@ -33,9 +33,10 @@ use std::{ }; use tari_common_types::{ transaction::TxId, - types::{BlindingFactor, Commitment, HashOutput, PrivateKey}, + types::{BlindingFactor, Commitment, HashOutput}, }; use tari_core::transactions::transaction::TransactionOutput; +use tari_key_manager::cipher_seed::CipherSeed; const LOG_TARGET: &str = "wallet::output_manager_service::database"; @@ -132,7 +133,7 @@ pub trait OutputManagerBackend: Send + Sync + Clone { /// Holds the state of the KeyManager being used by the Output Manager Service #[derive(Clone, Debug, PartialEq)] pub struct KeyManagerState { - pub master_key: PrivateKey, + pub seed: CipherSeed, pub branch_seed: String, pub primary_key_index: u64, } diff --git a/base_layer/wallet/src/output_manager_service/storage/sqlite_db.rs b/base_layer/wallet/src/output_manager_service/storage/sqlite_db.rs index 1de4dfcf02..afd94398ce 100644 --- a/base_layer/wallet/src/output_manager_service/storage/sqlite_db.rs +++ b/base_layer/wallet/src/output_manager_service/storage/sqlite_db.rs @@ -36,7 +36,7 @@ use crate::{ encryption::{decrypt_bytes_integral_nonce, encrypt_bytes_integral_nonce, Encryptable}, }, }; -use aes_gcm::{aead::Error as AeadError, Aes256Gcm, Error}; +use aes_gcm::Aes256Gcm; use chrono::{NaiveDateTime, Utc}; use diesel::{prelude::*, result::Error as DieselError, sql_query, SqliteConnection}; use log::*; @@ -65,6 +65,7 @@ use tari_crypto::{ ByteArray, }, }; +use tari_key_manager::cipher_seed::CipherSeed; use tokio::time::Instant; const LOG_TARGET: &str = "wallet::output_manager_service::database::sqlite_db"; @@ -936,10 +937,10 @@ impl OutputManagerBackend for OutputManagerSqliteDatabase { let mut key_manager_state = KeyManagerStateSql::get_state(&conn)?; - let _ = PrivateKey::from_vec(&key_manager_state.master_key).map_err(|_| { + let _ = CipherSeed::from_enciphered_bytes(&key_manager_state.seed, None).map_err(|_| { error!( target: LOG_TARGET, - "Could not create PrivateKey from stored bytes, They might already be encrypted" + "Could not create Cipher Seed from stored bytes, They might already be encrypted" ); OutputManagerStorageError::AlreadyEncrypted })?; @@ -1191,13 +1192,13 @@ impl NewOutputSql { } impl Encryptable for NewOutputSql { - fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), AeadError> { + fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { self.spending_key = encrypt_bytes_integral_nonce(cipher, self.spending_key.clone())?; self.script_private_key = encrypt_bytes_integral_nonce(cipher, self.script_private_key.clone())?; Ok(()) } - fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), AeadError> { + fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { self.spending_key = decrypt_bytes_integral_nonce(cipher, self.spending_key.clone())?; self.script_private_key = decrypt_bytes_integral_nonce(cipher, self.script_private_key.clone())?; Ok(()) @@ -1616,13 +1617,13 @@ impl TryFrom for DbUnblindedOutput { } impl Encryptable for OutputSql { - fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), AeadError> { + fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { self.spending_key = encrypt_bytes_integral_nonce(cipher, self.spending_key.clone())?; self.script_private_key = encrypt_bytes_integral_nonce(cipher, self.script_private_key.clone())?; Ok(()) } - fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), AeadError> { + fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { self.spending_key = decrypt_bytes_integral_nonce(cipher, self.spending_key.clone())?; self.script_private_key = decrypt_bytes_integral_nonce(cipher, self.script_private_key.clone())?; Ok(()) @@ -1701,7 +1702,7 @@ impl From for UpdateOutputSql { #[table_name = "key_manager_states"] struct KeyManagerStateSql { id: i32, - master_key: Vec, + seed: Vec, branch_seed: String, primary_key_index: i64, timestamp: NaiveDateTime, @@ -1710,7 +1711,7 @@ struct KeyManagerStateSql { #[derive(Clone, Debug, Insertable)] #[table_name = "key_manager_states"] struct NewKeyManagerStateSql { - master_key: Vec, + seed: Vec, branch_seed: String, primary_key_index: i64, timestamp: NaiveDateTime, @@ -1719,7 +1720,10 @@ struct NewKeyManagerStateSql { impl From for NewKeyManagerStateSql { fn from(km: KeyManagerState) -> Self { Self { - master_key: km.master_key.to_vec(), + seed: km + .seed + .encipher(None) + .expect("The only way for enciphering to fail is that the Crypto libraries are broken"), branch_seed: km.branch_seed, primary_key_index: km.primary_key_index as i64, timestamp: Utc::now().naive_utc(), @@ -1731,7 +1735,7 @@ impl TryFrom for KeyManagerState { fn try_from(km: KeyManagerStateSql) -> Result { Ok(Self { - master_key: PrivateKey::from_vec(&km.master_key).map_err(|_| OutputManagerStorageError::ConversionError)?, + seed: CipherSeed::from_enciphered_bytes(&km.seed, None)?, branch_seed: km.branch_seed, primary_key_index: km.primary_key_index as u64, }) @@ -1758,7 +1762,7 @@ impl KeyManagerStateSql { match KeyManagerStateSql::get_state(conn) { Ok(km) => { let update = KeyManagerStateUpdateSql { - master_key: Some(self.master_key.clone()), + seed: Some(self.seed.clone()), branch_seed: Some(self.branch_seed.clone()), primary_key_index: Some(self.primary_key_index), }; @@ -1770,7 +1774,7 @@ impl KeyManagerStateSql { }, Err(_) => { let inserter = NewKeyManagerStateSql { - master_key: self.master_key.clone(), + seed: self.seed.clone(), branch_seed: self.branch_seed.clone(), primary_key_index: self.primary_key_index, timestamp: self.timestamp, @@ -1786,7 +1790,7 @@ impl KeyManagerStateSql { Ok(km) => { let current_index = km.primary_key_index + 1; let update = KeyManagerStateUpdateSql { - master_key: None, + seed: None, branch_seed: None, primary_key_index: Some(current_index), }; @@ -1804,7 +1808,7 @@ impl KeyManagerStateSql { match KeyManagerStateSql::get_state(conn) { Ok(km) => { let update = KeyManagerStateUpdateSql { - master_key: None, + seed: None, branch_seed: None, primary_key_index: Some(index as i64), }; @@ -1822,42 +1826,42 @@ impl KeyManagerStateSql { #[derive(AsChangeset)] #[table_name = "key_manager_states"] struct KeyManagerStateUpdateSql { - master_key: Option>, + seed: Option>, branch_seed: Option, primary_key_index: Option, } impl Encryptable for KeyManagerStateSql { - fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), Error> { - let encrypted_master_key = encrypt_bytes_integral_nonce(cipher, self.master_key.clone())?; + fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { + let encrypted_seed = encrypt_bytes_integral_nonce(cipher, self.seed.clone())?; let encrypted_branch_seed = encrypt_bytes_integral_nonce(cipher, self.branch_seed.clone().into_bytes())?; - self.master_key = encrypted_master_key; + self.seed = encrypted_seed; self.branch_seed = encrypted_branch_seed.to_hex(); Ok(()) } - fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), Error> { - let decrypted_master_key = decrypt_bytes_integral_nonce(cipher, self.master_key.clone())?; + fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { + let decrypted_seed = decrypt_bytes_integral_nonce(cipher, self.seed.clone())?; let decrypted_branch_seed = - decrypt_bytes_integral_nonce(cipher, from_hex(self.branch_seed.as_str()).map_err(|_| Error)?)?; - self.master_key = decrypted_master_key; + decrypt_bytes_integral_nonce(cipher, from_hex(self.branch_seed.as_str()).map_err(|e| e.to_string())?)?; + self.seed = decrypted_seed; self.branch_seed = from_utf8(decrypted_branch_seed.as_slice()) - .map_err(|_| Error)? + .map_err(|e| e.to_string())? .to_string(); Ok(()) } } impl Encryptable for NewKeyManagerStateSql { - fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), Error> { - let encrypted_master_key = encrypt_bytes_integral_nonce(cipher, self.master_key.clone())?; + fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { + let encrypted_seed = encrypt_bytes_integral_nonce(cipher, self.seed.clone())?; let encrypted_branch_seed = encrypt_bytes_integral_nonce(cipher, self.branch_seed.clone().as_bytes().to_vec())?; - self.master_key = encrypted_master_key; + self.seed = encrypted_seed; self.branch_seed = encrypted_branch_seed.to_hex(); Ok(()) } - fn decrypt(&mut self, _cipher: &Aes256Gcm) -> Result<(), Error> { + fn decrypt(&mut self, _cipher: &Aes256Gcm) -> Result<(), String> { unimplemented!("Not supported") // let decrypted_master_key = decrypt_bytes_integral_nonce(&cipher, self.master_key.clone())?; // let decrypted_branch_seed = @@ -2005,12 +2009,12 @@ impl From for KnownOneSidedPaymentScriptSql { } impl Encryptable for KnownOneSidedPaymentScriptSql { - fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), AeadError> { + fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { self.private_key = encrypt_bytes_integral_nonce(cipher, self.private_key.clone())?; Ok(()) } - fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), AeadError> { + fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { self.private_key = decrypt_bytes_integral_nonce(cipher, self.private_key.clone())?; Ok(()) } @@ -2026,16 +2030,17 @@ mod test { }; use diesel::{Connection, SqliteConnection}; use rand::{rngs::OsRng, RngCore}; - use tari_crypto::{keys::SecretKey, script}; + use tari_crypto::script; use tempfile::tempdir; - use tari_common_types::types::{CommitmentFactory, PrivateKey}; + use tari_common_types::types::CommitmentFactory; use tari_core::transactions::{ tari_amount::MicroTari, test_helpers::{create_unblinded_output, TestParams as TestParamsHelpers}, transaction::{OutputFeatures, TransactionInput, UnblindedOutput}, CryptoFactories, }; + use tari_key_manager::cipher_seed::CipherSeed; use tari_test_utils::random; use crate::{ @@ -2178,7 +2183,7 @@ mod test { assert!(KeyManagerStateSql::get_state(&conn).is_err()); let state1 = KeyManagerState { - master_key: PrivateKey::random(&mut OsRng), + seed: CipherSeed::new(), branch_seed: random::string(8), primary_key_index: 0, }; @@ -2271,7 +2276,7 @@ mod test { let cipher = Aes256Gcm::new(key); let starting_state = KeyManagerState { - master_key: PrivateKey::random(&mut OsRng), + seed: CipherSeed::new(), branch_seed: "boop boop".to_string(), primary_key_index: 1, }; @@ -2295,7 +2300,7 @@ mod test { db_state.decrypt(&cipher).unwrap(); let decrypted_data = KeyManagerState::try_from(db_state).unwrap(); - assert_eq!(decrypted_data.master_key, starting_state.master_key); + assert_eq!(decrypted_data.seed, starting_state.seed); assert_eq!(decrypted_data.branch_seed, starting_state.branch_seed); assert_eq!(decrypted_data.primary_key_index, 2); } @@ -2314,7 +2319,7 @@ mod test { let factories = CryptoFactories::default(); let starting_state = KeyManagerState { - master_key: PrivateKey::random(&mut OsRng), + seed: CipherSeed::new(), branch_seed: "boop boop".to_string(), primary_key_index: 1, }; diff --git a/base_layer/wallet/src/schema.rs b/base_layer/wallet/src/schema.rs index 8562c14884..269b965f5d 100644 --- a/base_layer/wallet/src/schema.rs +++ b/base_layer/wallet/src/schema.rs @@ -53,7 +53,7 @@ table! { table! { key_manager_states (id) { id -> Integer, - master_key -> Binary, + seed -> Binary, branch_seed -> Text, primary_key_index -> BigInt, timestamp -> Timestamp, diff --git a/base_layer/wallet/src/storage/database.rs b/base_layer/wallet/src/storage/database.rs index d23d806298..b645df3685 100644 --- a/base_layer/wallet/src/storage/database.rs +++ b/base_layer/wallet/src/storage/database.rs @@ -28,12 +28,8 @@ use std::{ sync::Arc, }; use tari_common_types::chain_metadata::ChainMetadata; -use tari_comms::{ - multiaddr::Multiaddr, - peer_manager::PeerFeatures, - tor::TorIdentity, - types::{CommsPublicKey, CommsSecretKey}, -}; +use tari_comms::{multiaddr::Multiaddr, peer_manager::PeerFeatures, tor::TorIdentity}; +use tari_key_manager::cipher_seed::CipherSeed; const LOG_TARGET: &str = "wallet::database"; @@ -44,7 +40,7 @@ pub trait WalletBackend: Send + Sync + Clone { /// Modify the state the of the backend with a write operation fn write(&self, op: WriteOperation) -> Result, WalletStorageError>; /// Apply encryption to the backend. - fn apply_encryption(&self, cipher: Aes256Gcm) -> Result<(), WalletStorageError>; + fn apply_encryption(&self, passphrase: String) -> Result; /// Remove encryption from the backend. fn remove_encryption(&self) -> Result<(), WalletStorageError>; } @@ -56,8 +52,9 @@ pub enum DbKey { TorId, BaseNodeChainMetadata, ClientKey(String), - MasterSecretKey, - MasterPublicKey, + MasterSeed, + PassphraseHash, + EncryptionSalt, } pub enum DbValue { @@ -67,8 +64,9 @@ pub enum DbValue { ClientValue(String), ValueCleared, BaseNodeChainMetadata(ChainMetadata), - MasterSecretKey(CommsSecretKey), - MasterPublicKey(CommsPublicKey), + MasterSeed(CipherSeed), + PassphraseHash(String), + EncryptionSalt(String), } #[derive(Clone)] @@ -76,7 +74,7 @@ pub enum DbKeyValuePair { ClientKeyValue(String, String), TorId(TorIdentity), BaseNodeChainMetadata(ChainMetadata), - MasterSecretKey(CommsSecretKey), + MasterSeed(CipherSeed), CommsAddress(Multiaddr), CommsFeatures(PeerFeatures), } @@ -98,34 +96,32 @@ where T: WalletBackend + 'static Self { db: Arc::new(db) } } - pub async fn get_master_secret_key(&self) -> Result, WalletStorageError> { + pub async fn get_master_seed(&self) -> Result, WalletStorageError> { let db_clone = self.db.clone(); - let c = tokio::task::spawn_blocking(move || match db_clone.fetch(&DbKey::MasterSecretKey) { + let c = tokio::task::spawn_blocking(move || match db_clone.fetch(&DbKey::MasterSeed) { Ok(None) => Ok(None), - Ok(Some(DbValue::MasterSecretKey(k))) => Ok(Some(k)), - Ok(Some(other)) => unexpected_result(DbKey::MasterSecretKey, other), - Err(e) => log_error(DbKey::MasterSecretKey, e), + Ok(Some(DbValue::MasterSeed(k))) => Ok(Some(k)), + Ok(Some(other)) => unexpected_result(DbKey::MasterSeed, other), + Err(e) => log_error(DbKey::MasterSeed, e), }) .await .map_err(|err| WalletStorageError::BlockingTaskSpawnError(err.to_string()))??; Ok(c) } - pub async fn set_master_secret_key(&self, key: CommsSecretKey) -> Result<(), WalletStorageError> { + pub async fn set_master_seed(&self, seed: CipherSeed) -> Result<(), WalletStorageError> { let db_clone = self.db.clone(); - tokio::task::spawn_blocking(move || { - db_clone.write(WriteOperation::Insert(DbKeyValuePair::MasterSecretKey(key))) - }) - .await - .map_err(|err| WalletStorageError::BlockingTaskSpawnError(err.to_string()))??; + tokio::task::spawn_blocking(move || db_clone.write(WriteOperation::Insert(DbKeyValuePair::MasterSeed(seed)))) + .await + .map_err(|err| WalletStorageError::BlockingTaskSpawnError(err.to_string()))??; Ok(()) } - pub async fn clear_master_secret_key(&self) -> Result<(), WalletStorageError> { + pub async fn clear_master_seed(&self) -> Result<(), WalletStorageError> { let db_clone = self.db.clone(); - tokio::task::spawn_blocking(move || db_clone.write(WriteOperation::Remove(DbKey::MasterSecretKey))) + tokio::task::spawn_blocking(move || db_clone.write(WriteOperation::Remove(DbKey::MasterSeed))) .await .map_err(|err| WalletStorageError::BlockingTaskSpawnError(err.to_string()))??; Ok(()) @@ -229,9 +225,9 @@ where T: WalletBackend + 'static Ok(()) } - pub async fn apply_encryption(&self, cipher: Aes256Gcm) -> Result<(), WalletStorageError> { + pub async fn apply_encryption(&self, passphrase: String) -> Result { let db_clone = self.db.clone(); - tokio::task::spawn_blocking(move || db_clone.apply_encryption(cipher)) + tokio::task::spawn_blocking(move || db_clone.apply_encryption(passphrase)) .await .map_err(|err| WalletStorageError::BlockingTaskSpawnError(err.to_string())) .and_then(|inner_result| inner_result) @@ -315,13 +311,14 @@ where T: WalletBackend + 'static impl Display for DbKey { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { match self { - DbKey::MasterSecretKey => f.write_str(&"MasterSecretKey".to_string()), - DbKey::MasterPublicKey => f.write_str(&"MasterPublicKey".to_string()), + DbKey::MasterSeed => f.write_str(&"MasterSeed".to_string()), DbKey::CommsAddress => f.write_str(&"CommsAddress".to_string()), DbKey::CommsFeatures => f.write_str(&"Node features".to_string()), DbKey::TorId => f.write_str(&"TorId".to_string()), DbKey::ClientKey(k) => f.write_str(&format!("ClientKey: {:?}", k)), DbKey::BaseNodeChainMetadata => f.write_str(&"Last seen Chain metadata from base node".to_string()), + DbKey::PassphraseHash => f.write_str(&"PassphraseHash".to_string()), + DbKey::EncryptionSalt => f.write_str(&"EncryptionSalt".to_string()), } } } @@ -329,14 +326,15 @@ impl Display for DbKey { impl Display for DbValue { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { match self { - DbValue::MasterSecretKey(k) => f.write_str(&format!("MasterSecretKey: {:?}", k)), - DbValue::MasterPublicKey(k) => f.write_str(&format!("MasterPublicKey: {:?}", k)), + DbValue::MasterSeed(k) => f.write_str(&format!("MasterSeed: {:?}", k)), DbValue::ClientValue(v) => f.write_str(&format!("ClientValue: {:?}", v)), DbValue::ValueCleared => f.write_str(&"ValueCleared".to_string()), DbValue::CommsFeatures(_) => f.write_str(&"Node features".to_string()), DbValue::CommsAddress(_) => f.write_str(&"Comms Address".to_string()), DbValue::TorId(v) => f.write_str(&format!("Tor ID: {}", v)), DbValue::BaseNodeChainMetadata(v) => f.write_str(&format!("Last seen Chain metadata from base node:{}", v)), + DbValue::PassphraseHash(h) => f.write_str(&format!("PassphraseHash: {}", h)), + DbValue::EncryptionSalt(s) => f.write_str(&format!("EncryptionSalt: {}", s)), } } } @@ -364,9 +362,7 @@ mod test { sqlite_db::WalletSqliteDatabase, sqlite_utilities::run_migration_and_create_sqlite_connection, }; - use rand::rngs::OsRng; - use tari_comms::types::CommsSecretKey; - use tari_crypto::keys::SecretKey; + use tari_key_manager::cipher_seed::CipherSeed; use tari_test_utils::random::string; use tempfile::tempdir; use tokio::runtime::Runtime; @@ -382,13 +378,13 @@ mod test { let db = WalletDatabase::new(WalletSqliteDatabase::new(connection, None).unwrap()); // Test wallet settings - assert!(runtime.block_on(db.get_master_secret_key()).unwrap().is_none()); - let secret_key = CommsSecretKey::random(&mut OsRng); - runtime.block_on(db.set_master_secret_key(secret_key.clone())).unwrap(); - let stored_key = runtime.block_on(db.get_master_secret_key()).unwrap().unwrap(); - assert_eq!(secret_key, stored_key); - runtime.block_on(db.clear_master_secret_key()).unwrap(); - assert!(runtime.block_on(db.get_master_secret_key()).unwrap().is_none()); + assert!(runtime.block_on(db.get_master_seed()).unwrap().is_none()); + let seed = CipherSeed::new(); + runtime.block_on(db.set_master_seed(seed.clone())).unwrap(); + let stored_seed = runtime.block_on(db.get_master_seed()).unwrap().unwrap(); + assert_eq!(seed, stored_seed); + runtime.block_on(db.clear_master_seed()).unwrap(); + assert!(runtime.block_on(db.get_master_seed()).unwrap().is_none()); let client_key_values = vec![ ("key1".to_string(), "value1".to_string()), diff --git a/base_layer/wallet/src/storage/sqlite_db.rs b/base_layer/wallet/src/storage/sqlite_db.rs index e9bf03c569..838552c73c 100644 --- a/base_layer/wallet/src/storage/sqlite_db.rs +++ b/base_layer/wallet/src/storage/sqlite_db.rs @@ -32,7 +32,11 @@ use crate::{ use aes_gcm::{ aead::{generic_array::GenericArray, Aead}, Aes256Gcm, - Error as AeadError, + NewAead, +}; +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, + Argon2, }; use diesel::{prelude::*, SqliteConnection}; use log::*; @@ -41,20 +45,12 @@ use std::{ sync::{Arc, RwLock}, }; use tari_common_types::chain_metadata::ChainMetadata; -use tari_comms::{ - multiaddr::Multiaddr, - peer_manager::PeerFeatures, - tor::TorIdentity, - types::{CommsPublicKey, CommsSecretKey}, -}; -use tari_crypto::{ - keys::PublicKey, - tari_utilities::{ - hex::{from_hex, Hex}, - message_format::MessageFormat, - ByteArray, - }, +use tari_comms::{multiaddr::Multiaddr, peer_manager::PeerFeatures, tor::TorIdentity}; +use tari_crypto::tari_utilities::{ + hex::{from_hex, Hex}, + message_format::MessageFormat, }; +use tari_key_manager::cipher_seed::CipherSeed; use tokio::time::Instant; const LOG_TARGET: &str = "wallet::storage::sqlite_db"; @@ -66,8 +62,11 @@ pub struct WalletSqliteDatabase { cipher: Arc>>, } impl WalletSqliteDatabase { - pub fn new(database_connection: WalletDbConnection, cipher: Option) -> Result { - check_db_encryption_status(&database_connection, cipher.clone())?; + pub fn new( + database_connection: WalletDbConnection, + passphrase: Option, + ) -> Result { + let cipher = check_db_encryption_status(&database_connection, passphrase)?; Ok(Self { database_connection, @@ -75,45 +74,38 @@ impl WalletSqliteDatabase { }) } - fn set_master_secret_key( - &self, - secret_key: &CommsSecretKey, - conn: &SqliteConnection, - ) -> Result<(), WalletStorageError> { + fn set_master_seed(&self, seed: &CipherSeed, conn: &SqliteConnection) -> Result<(), WalletStorageError> { let cipher = acquire_read_lock!(self.cipher); match cipher.as_ref() { None => { - WalletSettingSql::new(DbKey::MasterSecretKey.to_string(), secret_key.to_hex()).set(conn)?; - let public_key = CommsPublicKey::from_secret_key(secret_key); - WalletSettingSql::new(DbKey::MasterPublicKey.to_string(), public_key.to_hex()).set(conn)?; + let seed_bytes = seed.encipher(None)?; + WalletSettingSql::new(DbKey::MasterSeed.to_string(), seed_bytes.to_hex()).set(conn)?; }, Some(cipher) => { - let public_key = CommsPublicKey::from_secret_key(secret_key); - WalletSettingSql::new(DbKey::MasterPublicKey.to_string(), public_key.to_hex()).set(conn)?; - let ciphertext_integral_nonce = encrypt_bytes_integral_nonce(cipher, secret_key.to_vec()) - .map_err(|e| WalletStorageError::AeadError(format!("Encryption Error:{}", e.to_string())))?; - WalletSettingSql::new(DbKey::MasterSecretKey.to_string(), ciphertext_integral_nonce.to_hex()) - .set(conn)?; + let seed_bytes = seed.encipher(None)?; + let ciphertext_integral_nonce = encrypt_bytes_integral_nonce(cipher, seed_bytes) + .map_err(|e| WalletStorageError::AeadError(format!("Encryption Error:{}", e)))?; + WalletSettingSql::new(DbKey::MasterSeed.to_string(), ciphertext_integral_nonce.to_hex()).set(conn)?; }, } Ok(()) } - fn get_master_secret_key(&self, conn: &SqliteConnection) -> Result, WalletStorageError> { + fn get_master_seed(&self, conn: &SqliteConnection) -> Result, WalletStorageError> { let cipher = acquire_read_lock!(self.cipher); - if let Some(key_str) = WalletSettingSql::get(DbKey::MasterSecretKey.to_string(), conn)? { - let secret_key = match cipher.as_ref() { - None => CommsSecretKey::from_hex(key_str.as_str())?, + if let Some(seed_str) = WalletSettingSql::get(DbKey::MasterSeed.to_string(), conn)? { + let seed = match cipher.as_ref() { + None => CipherSeed::from_enciphered_bytes(&from_hex(seed_str.as_str())?, None)?, Some(cipher) => { - let decrypted_key_bytes = decrypt_bytes_integral_nonce(cipher, from_hex(key_str.as_str())?) - .map_err(|e| WalletStorageError::AeadError(format!("Decryption Error:{}", e.to_string())))?; - CommsSecretKey::from_bytes(decrypted_key_bytes.as_slice())? + let decrypted_key_bytes = decrypt_bytes_integral_nonce(cipher, from_hex(seed_str.as_str())?) + .map_err(|e| WalletStorageError::AeadError(format!("Decryption Error:{}", e)))?; + CipherSeed::from_enciphered_bytes(&decrypted_key_bytes, None)? }, }; - Ok(Some(secret_key)) + Ok(Some(seed)) } else { Ok(None) } @@ -123,7 +115,7 @@ impl WalletSqliteDatabase { let cipher = acquire_read_lock!(self.cipher); if let Some(cipher) = cipher.as_ref() { o.decrypt(cipher) - .map_err(|e| WalletStorageError::AeadError(format!("Decryption Error:{}", e.to_string())))?; + .map_err(|e| WalletStorageError::AeadError(format!("Decryption Error:{}", e)))?; } Ok(()) } @@ -132,7 +124,7 @@ impl WalletSqliteDatabase { let cipher = acquire_read_lock!(self.cipher); if let Some(cipher) = cipher.as_ref() { o.encrypt(cipher) - .map_err(|e| WalletStorageError::AeadError(format!("Encryption Error:{}", e.to_string())))?; + .map_err(|e| WalletStorageError::AeadError(format!("Encryption Error:{}", e)))?; } Ok(()) } @@ -170,7 +162,7 @@ impl WalletSqliteDatabase { Some(cipher) => { let bytes = bincode::serialize(&tor).map_err(|e| WalletStorageError::ConversionError(e.to_string()))?; let ciphertext_integral_nonce = encrypt_bytes_integral_nonce(cipher, bytes) - .map_err(|e| WalletStorageError::AeadError(format!("Encryption Error:{}", e.to_string())))?; + .map_err(|e| WalletStorageError::AeadError(format!("Encryption Error:{}", e)))?; WalletSettingSql::new(DbKey::TorId.to_string(), ciphertext_integral_nonce.to_hex()).set(conn)?; }, } @@ -187,7 +179,7 @@ impl WalletSqliteDatabase { }, Some(cipher) => { let decrypted_key_bytes = decrypt_bytes_integral_nonce(cipher, from_hex(&key_str)?) - .map_err(|e| WalletStorageError::AeadError(format!("Decryption Error:{}", e.to_string())))?; + .map_err(|e| WalletStorageError::AeadError(format!("Decryption Error:{}", e)))?; bincode::deserialize(&decrypted_key_bytes) .map_err(|e| WalletStorageError::ConversionError(e.to_string()))? }, @@ -220,9 +212,9 @@ impl WalletSqliteDatabase { let acquire_lock = start.elapsed(); let kvp_text; match kvp { - DbKeyValuePair::MasterSecretKey(sk) => { - kvp_text = "MasterSecretKey"; - self.set_master_secret_key(&sk, &(*conn))?; + DbKeyValuePair::MasterSeed(seed) => { + kvp_text = "MasterSeed"; + self.set_master_seed(&seed, &(*conn))?; }, DbKeyValuePair::TorId(node_id) => { kvp_text = "TorId"; @@ -280,10 +272,9 @@ impl WalletSqliteDatabase { let conn = self.database_connection.acquire_lock(); let acquire_lock = start.elapsed(); match k { - DbKey::MasterSecretKey => { - let _ = WalletSettingSql::clear(DbKey::MasterSecretKey.to_string(), &conn)?; + DbKey::MasterSeed => { + let _ = WalletSettingSql::clear(DbKey::MasterSeed.to_string(), &conn)?; }, - DbKey::MasterPublicKey => return Err(WalletStorageError::OperationNotSupported), DbKey::ClientKey(ref k) => { if ClientKeyValueSql::clear(k, &conn)? { return Ok(Some(DbValue::ValueCleared)); @@ -301,6 +292,12 @@ impl WalletSqliteDatabase { DbKey::TorId => { let _ = WalletSettingSql::clear(DbKey::TorId.to_string(), &conn)?; }, + DbKey::PassphraseHash => { + return Err(WalletStorageError::OperationNotSupported); + }, + DbKey::EncryptionSalt => { + return Err(WalletStorageError::OperationNotSupported); + }, }; trace!( target: LOG_TARGET, @@ -312,6 +309,11 @@ impl WalletSqliteDatabase { ); Ok(None) } + + pub fn cipher(&self) -> Option { + let cipher = acquire_read_lock!(self.cipher); + (*cipher).clone() + } } impl WalletBackend for WalletSqliteDatabase { @@ -321,14 +323,7 @@ impl WalletBackend for WalletSqliteDatabase { let acquire_lock = start.elapsed(); let result = match key { - DbKey::MasterSecretKey => self.get_master_secret_key(&conn)?.map(DbValue::MasterSecretKey), - DbKey::MasterPublicKey => { - if let Some(key_str) = WalletSettingSql::get(key.to_string(), &conn)? { - Some(DbValue::MasterPublicKey(CommsPublicKey::from_hex(key_str.as_str())?)) - } else { - None - } - }, + DbKey::MasterSeed => self.get_master_seed(&conn)?.map(DbValue::MasterSeed), DbKey::ClientKey(k) => match ClientKeyValueSql::get(k, &conn)? { None => None, Some(mut v) => { @@ -340,6 +335,8 @@ impl WalletBackend for WalletSqliteDatabase { DbKey::TorId => self.get_tor_id(&conn)?, DbKey::CommsFeatures => self.get_comms_features(&conn)?.map(DbValue::CommsFeatures), DbKey::BaseNodeChainMetadata => self.get_chain_metadata(&conn)?.map(DbValue::BaseNodeChainMetadata), + DbKey::PassphraseHash => WalletSettingSql::get(key.to_string(), &conn)?.map(DbValue::PassphraseHash), + DbKey::EncryptionSalt => WalletSettingSql::get(key.to_string(), &conn)?.map(DbValue::EncryptionSalt), }; trace!( target: LOG_TARGET, @@ -360,7 +357,7 @@ impl WalletBackend for WalletSqliteDatabase { } } - fn apply_encryption(&self, cipher: Aes256Gcm) -> Result<(), WalletStorageError> { + fn apply_encryption(&self, passphrase: String) -> Result { let mut current_cipher = acquire_write_lock!(self.cipher); if current_cipher.is_some() { return Err(WalletStorageError::AlreadyEncrypted); @@ -369,21 +366,50 @@ impl WalletBackend for WalletSqliteDatabase { let start = Instant::now(); let conn = self.database_connection.acquire_lock(); let acquire_lock = start.elapsed(); - let secret_key_str = match WalletSettingSql::get(DbKey::MasterSecretKey.to_string(), &conn)? { - None => return Err(WalletStorageError::ValueNotFound(DbKey::MasterSecretKey)), + + // Check if there is an existing passphrase applied + let db_passphrase_hash = WalletSettingSql::get(DbKey::PassphraseHash.to_string(), &conn)?; + let db_encryption_salt = WalletSettingSql::get(DbKey::EncryptionSalt.to_string(), &conn)?; + if db_encryption_salt.is_some() || db_passphrase_hash.is_some() { + return Err(WalletStorageError::AlreadyEncrypted); + } + + let argon2 = Argon2::default(); + let passphrase_salt = SaltString::generate(&mut OsRng); + + let passphrase_hash = argon2 + .hash_password_simple(passphrase.as_bytes(), &passphrase_salt) + .map_err(|e| WalletStorageError::AeadError(e.to_string()))? + .to_string(); + let encryption_salt = SaltString::generate(&mut OsRng); + + let derived_encryption_key = argon2 + .hash_password_simple(passphrase.as_bytes(), encryption_salt.as_str()) + .map_err(|e| WalletStorageError::AeadError(e.to_string()))? + .hash + .ok_or_else(|| WalletStorageError::AeadError("Problem generating encryption key hash".to_string()))?; + let key = GenericArray::from_slice(derived_encryption_key.as_bytes()); + let cipher = Aes256Gcm::new(key); + + WalletSettingSql::new(DbKey::PassphraseHash.to_string(), passphrase_hash).set(&conn)?; + WalletSettingSql::new(DbKey::EncryptionSalt.to_string(), encryption_salt.as_str().to_string()).set(&conn)?; + + let master_seed_str = match WalletSettingSql::get(DbKey::MasterSeed.to_string(), &conn)? { + None => return Err(WalletStorageError::ValueNotFound(DbKey::MasterSeed)), Some(sk) => sk, }; - // If this fails then the database is already encrypted. - let secret_key = CommsSecretKey::from_hex(&secret_key_str).map_err(|_| WalletStorageError::AlreadyEncrypted)?; - let ciphertext_integral_nonce = encrypt_bytes_integral_nonce(&cipher, secret_key.to_vec()) - .map_err(|e| WalletStorageError::AeadError(format!("Encryption Error:{}", e.to_string())))?; - WalletSettingSql::new(DbKey::MasterSecretKey.to_string(), ciphertext_integral_nonce.to_hex()).set(&conn)?; + let master_seed_bytes = from_hex(master_seed_str.as_str())?; + // Sanity check that the decrypted bytes are a valid CipherSeed + let _master_seed = CipherSeed::from_enciphered_bytes(&master_seed_bytes, None)?; + let ciphertext_integral_nonce = encrypt_bytes_integral_nonce(&cipher, master_seed_bytes) + .map_err(|e| WalletStorageError::AeadError(format!("Encryption Error:{}", e)))?; + WalletSettingSql::new(DbKey::MasterSeed.to_string(), ciphertext_integral_nonce.to_hex()).set(&conn)?; // Encrypt all the client values let mut client_key_values = ClientKeyValueSql::index(&conn)?; for ckv in client_key_values.iter_mut() { ckv.encrypt(&cipher) - .map_err(|e| WalletStorageError::AeadError(format!("Encryption Error:{}", e.to_string())))?; + .map_err(|e| WalletStorageError::AeadError(format!("Encryption Error:{}", e)))?; ckv.set(&conn)?; } @@ -393,11 +419,11 @@ impl WalletBackend for WalletSqliteDatabase { let tor = TorIdentity::from_json(&v).map_err(|e| WalletStorageError::ConversionError(e.to_string()))?; let bytes = bincode::serialize(&tor).map_err(|e| WalletStorageError::ConversionError(e.to_string()))?; let ciphertext_integral_nonce = encrypt_bytes_integral_nonce(&cipher, bytes) - .map_err(|e| WalletStorageError::AeadError(format!("Encryption Error:{}", e.to_string())))?; + .map_err(|e| WalletStorageError::AeadError(format!("Encryption Error:{}", e)))?; WalletSettingSql::new(DbKey::TorId.to_string(), ciphertext_integral_nonce.to_hex()).set(&conn)?; } - (*current_cipher) = Some(cipher); + (*current_cipher) = Some(cipher.clone()); trace!( target: LOG_TARGET, "sqlite profile - apply_encryption: lock {} + db_op {} = {} ms", @@ -406,7 +432,7 @@ impl WalletBackend for WalletSqliteDatabase { start.elapsed().as_millis() ); - Ok(()) + Ok(cipher) } fn remove_encryption(&self) -> Result<(), WalletStorageError> { @@ -419,21 +445,25 @@ impl WalletBackend for WalletSqliteDatabase { let start = Instant::now(); let conn = self.database_connection.acquire_lock(); let acquire_lock = start.elapsed(); - let secret_key_str = match WalletSettingSql::get(DbKey::MasterSecretKey.to_string(), &conn)? { - None => return Err(WalletStorageError::ValueNotFound(DbKey::MasterSecretKey)), + let master_seed_str = match WalletSettingSql::get(DbKey::MasterSeed.to_string(), &conn)? { + None => return Err(WalletStorageError::ValueNotFound(DbKey::MasterSeed)), Some(sk) => sk, }; - let secret_key_bytes = decrypt_bytes_integral_nonce(&cipher, from_hex(secret_key_str.as_str())?) - .map_err(|e| WalletStorageError::AeadError(format!("Decryption Error:{}", e.to_string())))?; - let decrypted_key = CommsSecretKey::from_bytes(secret_key_bytes.as_slice())?; - WalletSettingSql::new(DbKey::MasterSecretKey.to_string(), decrypted_key.to_hex()).set(&conn)?; + let master_seed_bytes = decrypt_bytes_integral_nonce(&cipher, from_hex(master_seed_str.as_str())?) + .map_err(|e| WalletStorageError::AeadError(format!("Decryption Error:{}", e)))?; + // Sanity check that the decrypted bytes are a valid CipherSeed + let _master_seed = CipherSeed::from_enciphered_bytes(&master_seed_bytes, None)?; + WalletSettingSql::new(DbKey::MasterSeed.to_string(), master_seed_bytes.to_hex()).set(&conn)?; + + let _ = WalletSettingSql::clear(DbKey::PassphraseHash.to_string(), &conn)?; + let _ = WalletSettingSql::clear(DbKey::EncryptionSalt.to_string(), &conn)?; // Decrypt all the client values let mut client_key_values = ClientKeyValueSql::index(&conn)?; for ckv in client_key_values.iter_mut() { ckv.decrypt(&cipher) - .map_err(|e| WalletStorageError::AeadError(format!("Decryption Error:{}", e.to_string())))?; + .map_err(|e| WalletStorageError::AeadError(format!("Decryption Error:{}", e)))?; ckv.set(&conn)?; } @@ -441,7 +471,7 @@ impl WalletBackend for WalletSqliteDatabase { let key_str = WalletSettingSql::get(DbKey::TorId.to_string(), &conn)?; if let Some(v) = key_str { let decrypted_key_bytes = decrypt_bytes_integral_nonce(&cipher, from_hex(v.as_str())?) - .map_err(|e| WalletStorageError::AeadError(format!("Decryption Error:{}", e.to_string())))?; + .map_err(|e| WalletStorageError::AeadError(format!("Decryption Error:{}", e)))?; let tor_id: TorIdentity = bincode::deserialize(&decrypted_key_bytes) .map_err(|e| WalletStorageError::ConversionError(e.to_string()))?; let tor_string = tor_id @@ -471,15 +501,46 @@ impl WalletBackend for WalletSqliteDatabase { /// Master Public Key that is stored in the db fn check_db_encryption_status( database_connection: &WalletDbConnection, - cipher: Option, -) -> Result<(), WalletStorageError> { + passphrase: Option, +) -> Result, WalletStorageError> { let start = Instant::now(); let conn = database_connection.acquire_lock(); let acquire_lock = start.elapsed(); - let secret_key = WalletSettingSql::get(DbKey::MasterSecretKey.to_string(), &conn)?; - let db_public_key = WalletSettingSql::get(DbKey::MasterPublicKey.to_string(), &conn)?; - if cipher.is_some() && secret_key.is_none() { + let db_passphrase_hash = WalletSettingSql::get(DbKey::PassphraseHash.to_string(), &conn)?; + let db_encryption_salt = WalletSettingSql::get(DbKey::EncryptionSalt.to_string(), &conn)?; + + let cipher = match (passphrase, db_passphrase_hash, db_encryption_salt) { + (Some(_), None, _) => { + error!( + target: LOG_TARGET, + "Passphrase is provided but database is not encrypted" + ); + return Err(WalletStorageError::InvalidPassphrase); + }, + (Some(passphrase), Some(db_passphrase_hash), Some(encryption_salt)) => { + let argon2 = Argon2::default(); + let stored_hash = + PasswordHash::new(&db_passphrase_hash).map_err(|e| WalletStorageError::AeadError(e.to_string()))?; + if let Err(e) = argon2.verify_password(passphrase.as_bytes(), &stored_hash) { + error!(target: LOG_TARGET, "Incorrect passphrase ({})", e); + return Err(WalletStorageError::InvalidPassphrase); + } + + let derived_encryption_key = argon2 + .hash_password_simple(passphrase.as_bytes(), encryption_salt.as_str()) + .map_err(|e| WalletStorageError::AeadError(e.to_string()))? + .hash + .ok_or_else(|| WalletStorageError::AeadError("Problem generating encryption key hash".to_string()))?; + let key = GenericArray::from_slice(derived_encryption_key.as_bytes()); + Some(Aes256Gcm::new(key)) + }, + _ => None, + }; + + let secret_seed = WalletSettingSql::get(DbKey::MasterSeed.to_string(), &conn)?; + + if cipher.is_some() && secret_seed.is_none() { error!( target: LOG_TARGET, "Cipher is provided but there is no Master Secret Key in DB to decrypt" @@ -487,9 +548,9 @@ fn check_db_encryption_status( return Err(WalletStorageError::InvalidEncryptionCipher); } - if let Some(sk) = secret_key { - let master_secret_key = match CommsSecretKey::from_hex(sk.as_str()) { - Ok(sk) => { + if let Some(sk) = secret_seed { + match CipherSeed::from_enciphered_bytes(&from_hex(sk.as_str())?, None) { + Ok(_sk) => { // This means the key was unencrypted if cipher.is_some() { error!( @@ -498,7 +559,6 @@ fn check_db_encryption_status( ); return Err(WalletStorageError::InvalidEncryptionCipher); } - sk }, Err(_) => { // This means the secret key was encrypted. Try decrypt @@ -512,16 +572,16 @@ fn check_db_encryption_status( let nonce = GenericArray::from_slice(sk_bytes.as_slice()); let decrypted_key = cipher_inner.decrypt(nonce, data.as_ref()).map_err(|e| { - error!(target: LOG_TARGET, "Incorrect password ({})", e); - WalletStorageError::IncorrectPassword + error!(target: LOG_TARGET, "Incorrect passphrase ({})", e); + WalletStorageError::InvalidPassphrase })?; - CommsSecretKey::from_bytes(decrypted_key.as_slice()).map_err(|_| { + let _ = CipherSeed::from_enciphered_bytes(&decrypted_key, None).map_err(|_| { error!( target: LOG_TARGET, - "Decrypted Master Secret Key cannot be parsed into a RistrettoSecretKey" + "Decrypted Master Secret Key cannot be parsed into a Cipher Seed" ); WalletStorageError::InvalidEncryptionCipher - })? + })?; } else { error!( target: LOG_TARGET, @@ -531,36 +591,6 @@ fn check_db_encryption_status( } }, }; - - if let Some(pk_hex) = db_public_key { - let db_master_public_key = CommsPublicKey::from_hex(pk_hex.as_str())?; - let public_key = CommsPublicKey::from_secret_key(&master_secret_key); - if public_key != db_master_public_key { - if cipher.is_some() { - error!( - target: LOG_TARGET, - "Cipher is provided but does not decrypt the stored Master Secret Key that produces the \ - stored Comms Public Key." - ); - return Err(WalletStorageError::InvalidEncryptionCipher); - } else { - // If the db is not encrypted then update the stored public key to keep it in sync. - WalletSettingSql::new(DbKey::MasterPublicKey.to_string(), public_key.to_hex()).set(&conn)?; - } - } - } else { - if cipher.is_some() { - // This means the database was not in the correct state for a Cipher to be provided. - error!( - target: LOG_TARGET, - "Cipher is provided but Master Public Key is not present in the database" - ); - return Err(WalletStorageError::InvalidEncryptionCipher); - } - // Due to migration the associated public key is not stored and should be - let public_key_hex = CommsPublicKey::from_secret_key(&master_secret_key).to_hex(); - WalletSettingSql::new(DbKey::MasterPublicKey.to_string(), public_key_hex).set(&conn)?; - } } trace!( target: LOG_TARGET, @@ -570,7 +600,7 @@ fn check_db_encryption_status( start.elapsed().as_millis() ); - Ok(()) + Ok(cipher) } /// A Sql version of the wallet setting key-value table @@ -658,18 +688,18 @@ impl ClientKeyValueSql { impl Encryptable for ClientKeyValueSql { #[allow(unused_assignments)] - fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), AeadError> { + fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { let encrypted_value = encrypt_bytes_integral_nonce(cipher, self.value.as_bytes().to_vec())?; self.value = encrypted_value.to_hex(); Ok(()) } #[allow(unused_assignments)] - fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), AeadError> { + fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { let decrypted_value = - decrypt_bytes_integral_nonce(cipher, from_hex(self.value.as_str()).map_err(|_| aes_gcm::Error)?)?; + decrypt_bytes_integral_nonce(cipher, from_hex(self.value.as_str()).map_err(|e| e.to_string())?)?; self.value = from_utf8(decrypted_value.as_slice()) - .map_err(|_| AeadError)? + .map_err(|e| e.to_string())? .to_string(); Ok(()) } @@ -682,16 +712,8 @@ mod test { sqlite_db::{ClientKeyValueSql, WalletSettingSql, WalletSqliteDatabase}, sqlite_utilities::run_migration_and_create_sqlite_connection, }; - use aes_gcm::{ - aead::{generic_array::GenericArray, Aead, NewAead}, - Aes256Gcm, - }; - use rand::{rngs::OsRng, RngCore}; - use tari_comms::types::{CommsPublicKey, CommsSecretKey}; - use tari_crypto::{ - keys::{PublicKey, SecretKey}, - tari_utilities::{hex::Hex, ByteArray}, - }; + use tari_crypto::tari_utilities::hex::Hex; + use tari_key_manager::cipher_seed::CipherSeed; use tari_test_utils::random::string; use tempfile::tempdir; @@ -701,105 +723,67 @@ mod test { let tempdir = tempdir().unwrap(); let db_folder = tempdir.path().to_str().unwrap().to_string(); let connection = run_migration_and_create_sqlite_connection(&format!("{}{}", db_folder, db_name)).unwrap(); - let secret_key1 = CommsSecretKey::random(&mut OsRng); - let public_key1 = CommsPublicKey::from_secret_key(&secret_key1); + let secret_seed1 = CipherSeed::new(); + { let conn = connection.acquire_lock(); - WalletSettingSql::new(DbKey::MasterSecretKey.to_string(), secret_key1.to_hex()) - .set(&conn) - .unwrap(); + WalletSettingSql::new( + DbKey::MasterSeed.to_string(), + secret_seed1.encipher(None).unwrap().to_hex(), + ) + .set(&conn) + .unwrap(); } let db = WalletSqliteDatabase::new(connection.clone(), None).unwrap(); - if let DbValue::MasterSecretKey(sk) = db.fetch(&DbKey::MasterSecretKey).unwrap().unwrap() { - assert_eq!(sk, secret_key1); + if let DbValue::MasterSeed(sk) = db.fetch(&DbKey::MasterSeed).unwrap().unwrap() { + assert_eq!(sk, secret_seed1); } else { panic!("Should be a Master Secret Key"); }; - if let DbValue::MasterPublicKey(pk) = db.fetch(&DbKey::MasterPublicKey).unwrap().unwrap() { - assert_eq!(pk, public_key1); - } else { - panic!("Should be a Master Public Key"); - }; - let secret_key2 = CommsSecretKey::random(&mut OsRng); - let public_key2 = CommsPublicKey::from_secret_key(&secret_key2); + let secret_seed2 = CipherSeed::new(); + { let conn = connection.acquire_lock(); - db.set_master_secret_key(&secret_key2, &conn).unwrap(); + db.set_master_seed(&secret_seed2, &conn).unwrap(); } - if let DbValue::MasterPublicKey(pk) = db.fetch(&DbKey::MasterPublicKey).unwrap().unwrap() { - assert_eq!(pk, public_key2); + + if let DbValue::MasterSeed(sk) = db.fetch(&DbKey::MasterSeed).unwrap().unwrap() { + assert_eq!(sk, secret_seed2); } else { - panic!("Should be a Master Public Key"); + panic!("Should be a Master Secret Key"); }; } #[test] - pub fn test_encrypted_secret_public_key_validation_during_startup() { + pub fn test_encrypted_seed_validation_during_startup() { let db_name = format!("{}.sqlite3", string(8).as_str()); let db_tempdir = tempdir().unwrap(); let db_folder = db_tempdir.path().to_str().unwrap().to_string(); let connection = run_migration_and_create_sqlite_connection(&format!("{}{}", db_folder, db_name)).unwrap(); - let key = GenericArray::from_slice(b"an example very very secret key."); - let cipher = Aes256Gcm::new(key); + let passphrase = "an example very very secret key.".to_string(); - assert!(WalletSqliteDatabase::new(connection.clone(), Some(cipher.clone())).is_err()); + assert!(WalletSqliteDatabase::new(connection.clone(), Some(passphrase.clone())).is_err()); - let secret_key = CommsSecretKey::random(&mut OsRng); + let seed = CipherSeed::new(); { let conn = connection.acquire_lock(); - WalletSettingSql::new(DbKey::MasterSecretKey.to_string(), secret_key.to_hex()) + WalletSettingSql::new(DbKey::MasterSeed.to_string(), seed.encipher(None).unwrap().to_hex()) .set(&conn) .unwrap(); } - assert!(WalletSqliteDatabase::new(connection.clone(), Some(cipher.clone())).is_err()); - - // encrypt the private key - let secret_key_bytes = secret_key.to_vec(); - let mut nonce = [0u8; 12]; - OsRng.fill_bytes(&mut nonce); - let nonce_ga = GenericArray::from_slice(&nonce); - let mut ciphertext = cipher - .encrypt(nonce_ga, secret_key_bytes.as_ref()) - .expect("encryption failure!"); - - let mut ciphertext_integral_nonce = nonce.to_vec(); - - ciphertext_integral_nonce.append(&mut ciphertext); + assert!(WalletSqliteDatabase::new(connection.clone(), Some(passphrase.clone())).is_err()); { - let conn = connection.acquire_lock(); - WalletSettingSql::new(DbKey::MasterSecretKey.to_string(), ciphertext_integral_nonce.to_hex()) - .set(&conn) - .unwrap(); + let db = WalletSqliteDatabase::new(connection.clone(), None).unwrap(); + db.apply_encryption(passphrase.clone()).unwrap(); } assert!(WalletSqliteDatabase::new(connection.clone(), None).is_err()); - // No public key to compare against - assert!(WalletSqliteDatabase::new(connection.clone(), Some(cipher.clone())).is_err()); - - // insert incorrect public key - let incorrect_public_key = CommsPublicKey::from_secret_key(&CommsSecretKey::random(&mut OsRng)); - { - let conn = connection.acquire_lock(); - WalletSettingSql::new(DbKey::MasterPublicKey.to_string(), incorrect_public_key.to_hex()) - .set(&conn) - .unwrap(); - } - assert!(WalletSqliteDatabase::new(connection.clone(), Some(cipher.clone())).is_err()); - - // insert correct public key - let public_key = CommsPublicKey::from_secret_key(&secret_key); - { - let conn = connection.acquire_lock(); - WalletSettingSql::new(DbKey::MasterPublicKey.to_string(), public_key.to_hex()) - .set(&conn) - .unwrap(); - } - assert!(WalletSqliteDatabase::new(connection, Some(cipher)).is_ok()); + assert!(WalletSqliteDatabase::new(connection, Some(passphrase)).is_ok()); } #[test] @@ -810,7 +794,7 @@ mod test { let db_path = format!("{}/{}", db_folder, db_name); let connection = run_migration_and_create_sqlite_connection(&db_path).unwrap(); - let secret_key = CommsSecretKey::random(&mut OsRng); + let seed = CipherSeed::new(); let key_values = vec![ ClientKeyValueSql::new("key1".to_string(), "value1".to_string()), ClientKeyValueSql::new("key2".to_string(), "value2".to_string()), @@ -819,25 +803,24 @@ mod test { let db = WalletSqliteDatabase::new(connection.clone(), None).unwrap(); { let conn = connection.acquire_lock(); - db.set_master_secret_key(&secret_key, &conn).unwrap(); + db.set_master_seed(&seed, &conn).unwrap(); for kv in key_values.iter() { kv.set(&conn).unwrap(); } } - let read_secret_key1 = match db.fetch(&DbKey::MasterSecretKey).unwrap().unwrap() { - DbValue::MasterSecretKey(sk) => sk, + let read_seed1 = match db.fetch(&DbKey::MasterSeed).unwrap().unwrap() { + DbValue::MasterSeed(sk) => sk, _ => { panic!("Should be able to read Key"); }, }; - assert_eq!(secret_key, read_secret_key1); + assert_eq!(seed, read_seed1); - let key = GenericArray::from_slice(b"an example very very secret key."); - let cipher = Aes256Gcm::new(key); - db.apply_encryption(cipher).unwrap(); - let read_secret_key2 = match db.fetch(&DbKey::MasterSecretKey).unwrap().unwrap() { - DbValue::MasterSecretKey(sk) => sk, + let passphrase = "an example very very secret key.".to_string(); + db.apply_encryption(passphrase).unwrap(); + let read_seed2 = match db.fetch(&DbKey::MasterSeed).unwrap().unwrap() { + DbValue::MasterSeed(sk) => sk, _ => { panic!("Should be able to read Key"); }, @@ -854,36 +837,28 @@ mod test { } } - assert_eq!(secret_key, read_secret_key2); + assert_eq!(seed, read_seed2); { let conn = connection.acquire_lock(); - let secret_key_str = WalletSettingSql::get(DbKey::MasterSecretKey.to_string(), &conn) + let secret_key_str = WalletSettingSql::get(DbKey::MasterSeed.to_string(), &conn) .unwrap() .unwrap(); assert!(secret_key_str.len() > 64); - db.set_master_secret_key(&secret_key, &conn).unwrap(); - let secret_key_str = WalletSettingSql::get(DbKey::MasterSecretKey.to_string(), &conn) + db.set_master_seed(&seed, &conn).unwrap(); + let secret_key_str = WalletSettingSql::get(DbKey::MasterSeed.to_string(), &conn) .unwrap() .unwrap(); assert!(secret_key_str.len() > 64); } db.remove_encryption().unwrap(); - let read_secret_key3 = match db.fetch(&DbKey::MasterSecretKey).unwrap().unwrap() { - DbValue::MasterSecretKey(sk) => sk, + let read_seed3 = match db.fetch(&DbKey::MasterSeed).unwrap().unwrap() { + DbValue::MasterSeed(sk) => sk, _ => { panic!("Should be able to read Key"); }, }; - assert_eq!(secret_key, read_secret_key3); - - { - let conn = connection.acquire_lock(); - let secret_key_str = WalletSettingSql::get(DbKey::MasterSecretKey.to_string(), &conn) - .unwrap() - .unwrap(); - assert_eq!(secret_key_str.len(), 64); - } + assert_eq!(seed, read_seed3); for kv in key_values.iter() { match db.fetch(&DbKey::ClientKey(kv.key.clone())).unwrap().unwrap() { diff --git a/base_layer/wallet/src/storage/sqlite_utilities.rs b/base_layer/wallet/src/storage/sqlite_utilities.rs index 35d8f095d8..ad6ba82a65 100644 --- a/base_layer/wallet/src/storage/sqlite_utilities.rs +++ b/base_layer/wallet/src/storage/sqlite_utilities.rs @@ -27,12 +27,7 @@ use crate::{ storage::{database::WalletDatabase, sqlite_db::WalletSqliteDatabase}, transaction_service::storage::sqlite_db::TransactionServiceSqliteDatabase, }; -use aes_gcm::{ - aead::{generic_array::GenericArray, NewAead}, - Aes256Gcm, -}; use diesel::{Connection, SqliteConnection}; -use digest::Digest; use fs2::FileExt; use log::*; use std::{ @@ -40,7 +35,6 @@ use std::{ path::{Path, PathBuf}, sync::{Arc, Mutex, MutexGuard}, }; -use tari_crypto::common::Blake256; const LOG_TARGET: &str = "wallet::storage:sqlite_utilities"; @@ -100,7 +94,7 @@ pub async fn partial_wallet_backup>(current_db: P, backup_path: P // open a connection and clear the Master Secret Key let connection = run_migration_and_create_sqlite_connection(backup_path)?; let db = WalletDatabase::new(WalletSqliteDatabase::new(connection, None)?); - db.clear_master_secret_key().await?; + db.clear_master_seed().await?; Ok(()) } @@ -149,12 +143,6 @@ pub fn initialize_sqlite_database_backends( ), WalletStorageError, > { - let cipher = passphrase.map(|passphrase_str| { - let passphrase_hash = Blake256::new().chain(passphrase_str.as_bytes()).finalize(); - let key = GenericArray::from_slice(passphrase_hash.as_slice()); - Aes256Gcm::new(key) - }); - let connection = run_migration_and_create_sqlite_connection(&db_path).map_err(|e| { error!( target: LOG_TARGET, @@ -163,9 +151,9 @@ pub fn initialize_sqlite_database_backends( e })?; - let wallet_backend = WalletSqliteDatabase::new(connection.clone(), cipher.clone())?; - let transaction_backend = TransactionServiceSqliteDatabase::new(connection.clone(), cipher.clone()); - let output_manager_backend = OutputManagerSqliteDatabase::new(connection.clone(), cipher); + let wallet_backend = WalletSqliteDatabase::new(connection.clone(), passphrase)?; + let transaction_backend = TransactionServiceSqliteDatabase::new(connection.clone(), wallet_backend.cipher()); + let output_manager_backend = OutputManagerSqliteDatabase::new(connection.clone(), wallet_backend.cipher()); let contacts_backend = ContactsServiceSqliteDatabase::new(connection); Ok(( diff --git a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs index cdce88597d..25701f08a6 100644 --- a/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs +++ b/base_layer/wallet/src/transaction_service/storage/sqlite_db.rs @@ -35,7 +35,7 @@ use crate::{ encryption::{decrypt_bytes_integral_nonce, encrypt_bytes_integral_nonce, Encryptable}, }, }; -use aes_gcm::{self, aead::Error as AeadError, Aes256Gcm}; +use aes_gcm::{self, Aes256Gcm}; use chrono::{NaiveDateTime, Utc}; use diesel::{prelude::*, result::Error as DieselError, SqliteConnection}; use log::*; @@ -1291,19 +1291,19 @@ impl InboundTransactionSql { } impl Encryptable for InboundTransactionSql { - fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), AeadError> { + fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { let encrypted_protocol = encrypt_bytes_integral_nonce(cipher, self.receiver_protocol.as_bytes().to_vec())?; self.receiver_protocol = encrypted_protocol.to_hex(); Ok(()) } - fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), AeadError> { + fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { let decrypted_protocol = decrypt_bytes_integral_nonce( cipher, - from_hex(self.receiver_protocol.as_str()).map_err(|_| aes_gcm::Error)?, + from_hex(self.receiver_protocol.as_str()).map_err(|e| e.to_string())?, )?; self.receiver_protocol = from_utf8(decrypted_protocol.as_slice()) - .map_err(|_| aes_gcm::Error)? + .map_err(|e| e.to_string())? .to_string(); Ok(()) } @@ -1461,19 +1461,19 @@ impl OutboundTransactionSql { } impl Encryptable for OutboundTransactionSql { - fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), AeadError> { + fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { let encrypted_protocol = encrypt_bytes_integral_nonce(cipher, self.sender_protocol.as_bytes().to_vec())?; self.sender_protocol = encrypted_protocol.to_hex(); Ok(()) } - fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), AeadError> { + fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { let decrypted_protocol = decrypt_bytes_integral_nonce( cipher, - from_hex(self.sender_protocol.as_str()).map_err(|_| aes_gcm::Error)?, + from_hex(self.sender_protocol.as_str()).map_err(|e| e.to_string())?, )?; self.sender_protocol = from_utf8(decrypted_protocol.as_slice()) - .map_err(|_| aes_gcm::Error)? + .map_err(|e| e.to_string())? .to_string(); Ok(()) } @@ -1728,19 +1728,19 @@ impl CompletedTransactionSql { } impl Encryptable for CompletedTransactionSql { - fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), AeadError> { + fn encrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { let encrypted_protocol = encrypt_bytes_integral_nonce(cipher, self.transaction_protocol.as_bytes().to_vec())?; self.transaction_protocol = encrypted_protocol.to_hex(); Ok(()) } - fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), AeadError> { + fn decrypt(&mut self, cipher: &Aes256Gcm) -> Result<(), String> { let decrypted_protocol = decrypt_bytes_integral_nonce( cipher, - from_hex(self.transaction_protocol.as_str()).map_err(|_| aes_gcm::Error)?, + from_hex(self.transaction_protocol.as_str()).map_err(|e| e.to_string())?, )?; self.transaction_protocol = from_utf8(decrypted_protocol.as_slice()) - .map_err(|_| aes_gcm::Error)? + .map_err(|e| e.to_string())? .to_string(); Ok(()) } diff --git a/base_layer/wallet/src/util/encryption.rs b/base_layer/wallet/src/util/encryption.rs index 9a062fff2a..f86dcec309 100644 --- a/base_layer/wallet/src/util/encryption.rs +++ b/base_layer/wallet/src/util/encryption.rs @@ -30,24 +30,26 @@ pub const AES_NONCE_BYTES: usize = 12; pub const AES_KEY_BYTES: usize = 32; pub trait Encryptable { - fn encrypt(&mut self, cipher: &C) -> Result<(), AeadError>; - fn decrypt(&mut self, cipher: &C) -> Result<(), AeadError>; + fn encrypt(&mut self, cipher: &C) -> Result<(), String>; + fn decrypt(&mut self, cipher: &C) -> Result<(), String>; } -pub fn decrypt_bytes_integral_nonce(cipher: &Aes256Gcm, ciphertext: Vec) -> Result, AeadError> { +pub fn decrypt_bytes_integral_nonce(cipher: &Aes256Gcm, ciphertext: Vec) -> Result, String> { if ciphertext.len() < AES_NONCE_BYTES { - return Err(AeadError); + return Err(AeadError.to_string()); } let (nonce, cipher_text) = ciphertext.split_at(AES_NONCE_BYTES); let nonce = GenericArray::from_slice(nonce); - cipher.decrypt(nonce, cipher_text.as_ref()) + cipher.decrypt(nonce, cipher_text.as_ref()).map_err(|e| e.to_string()) } -pub fn encrypt_bytes_integral_nonce(cipher: &Aes256Gcm, plaintext: Vec) -> Result, AeadError> { +pub fn encrypt_bytes_integral_nonce(cipher: &Aes256Gcm, plaintext: Vec) -> Result, String> { let mut nonce = [0u8; AES_NONCE_BYTES]; OsRng.fill_bytes(&mut nonce); let nonce_ga = GenericArray::from_slice(&nonce); - let mut ciphertext = cipher.encrypt(nonce_ga, plaintext.as_ref())?; + let mut ciphertext = cipher + .encrypt(nonce_ga, plaintext.as_ref()) + .map_err(|e| e.to_string())?; let mut ciphertext_integral_nonce = nonce.to_vec(); ciphertext_integral_nonce.append(&mut ciphertext); Ok(ciphertext_integral_nonce) @@ -60,6 +62,7 @@ mod test { aead::{generic_array::GenericArray, NewAead}, Aes256Gcm, }; + #[test] fn test_encrypt_decrypt() { let plaintext = b"The quick brown fox was annoying".to_vec(); diff --git a/base_layer/wallet/src/wallet.rs b/base_layer/wallet/src/wallet.rs index f90a83515c..0f3ee66fda 100644 --- a/base_layer/wallet/src/wallet.rs +++ b/base_layer/wallet/src/wallet.rs @@ -20,19 +20,12 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::{marker::PhantomData, sync::Arc}; - -use aes_gcm::{ - aead::{generic_array::GenericArray, NewAead}, - Aes256Gcm, -}; use digest::Digest; use log::*; -use rand::rngs::OsRng; +use std::{marker::PhantomData, sync::Arc}; use tari_common::configuration::bootstrap::ApplicationType; use tari_crypto::{ common::Blake256, - keys::SecretKey, ristretto::{RistrettoPublicKey, RistrettoSchnorr, RistrettoSecretKey}, script, script::{ExecutionStack, TariScript}, @@ -90,6 +83,7 @@ use crate::{ utxo_scanner_service::{handle::UtxoScannerHandle, UtxoScannerServiceInitializer}, }; use tari_common_types::transaction::TxId; +use tari_key_manager::cipher_seed::CipherSeed; const LOG_TARGET: &str = "wallet"; @@ -129,11 +123,10 @@ where output_manager_backend: V, contacts_backend: W, shutdown_signal: ShutdownSignal, - recovery_master_key: Option, + recovery_seed: Option, ) -> Result { - let master_secret_key = - read_or_create_master_secret_key(recovery_master_key, &mut wallet_database.clone()).await?; - let comms_secret_key = derive_comms_secret_key(&master_secret_key)?; + let master_seed = read_or_create_master_seed(recovery_seed, &mut wallet_database.clone()).await?; + let comms_secret_key = derive_comms_secret_key(&master_seed)?; let node_identity = Arc::new(NodeIdentity::new( comms_secret_key, @@ -177,7 +170,7 @@ where output_manager_backend, factories.clone(), config.network, - master_secret_key, + master_seed, )) .add_initializer(TransactionServiceInitializer::new( config.transaction_service_config.unwrap_or_default(), @@ -484,11 +477,7 @@ where /// in which case this will fail. pub async fn apply_encryption(&mut self, passphrase: String) -> Result<(), WalletError> { debug!(target: LOG_TARGET, "Applying wallet encryption."); - let passphrase_hash = Blake256::new().chain(passphrase.as_bytes()).finalize(); - let key = GenericArray::from_slice(passphrase_hash.as_slice()); - let cipher = Aes256Gcm::new(key); - - self.db.apply_encryption(cipher.clone()).await?; + let cipher = self.db.apply_encryption(passphrase).await?; self.output_manager_service.apply_encryption(cipher.clone()).await?; self.transaction_service.apply_encryption(cipher).await?; Ok(()) @@ -511,30 +500,29 @@ where } } -async fn read_or_create_master_secret_key( - recovery_master_key: Option, +async fn read_or_create_master_seed( + recovery_seed: Option, db: &mut WalletDatabase, -) -> Result { - let db_master_secret_key = db.get_master_secret_key().await?; +) -> Result { + let db_master_seed = db.get_master_seed().await?; - let master_secret_key = match recovery_master_key { - None => match db_master_secret_key { + let master_seed = match recovery_seed { + None => match db_master_seed { None => { - let secret_key = CommsSecretKey::random(&mut OsRng); - db.set_master_secret_key(secret_key.clone()).await?; - secret_key + let seed = CipherSeed::new(); + db.set_master_seed(seed.clone()).await?; + seed }, - Some(secret_key) => secret_key, + Some(seed) => seed, }, - Some(recovery_key) => { - if db_master_secret_key.is_none() { - db.set_master_secret_key(recovery_key.clone()).await?; - recovery_key + Some(recovery_seed) => { + if db_master_seed.is_none() { + db.set_master_seed(recovery_seed.clone()).await?; + recovery_seed } else { error!( target: LOG_TARGET, - "Attempted recovery would overwrite the existing wallet database master secret key, causing a \ - `MasterSecretKeyMismatch` error." + "Attempted recovery would overwrite the existing wallet database master seed" ); let msg = "Wallet already exists! Move the existing wallet database file.".to_string(); return Err(WalletError::WalletRecoveryError(msg)); @@ -542,12 +530,12 @@ async fn read_or_create_master_secret_key( }, }; - Ok(master_secret_key) + Ok(master_seed) } -fn derive_comms_secret_key(master_secret_key: &CommsSecretKey) -> Result { +fn derive_comms_secret_key(master_seed: &CipherSeed) -> Result { let comms_key_manager = KeyManager::::from( - master_secret_key.clone(), + master_seed.clone(), KEY_MANAGER_COMMS_SECRET_KEY_BRANCH_KEY.to_string(), 0, ); diff --git a/base_layer/wallet/tests/output_manager_service/service.rs b/base_layer/wallet/tests/output_manager_service/service.rs index d727bb5e1c..30ef5c1920 100644 --- a/base_layer/wallet/tests/output_manager_service/service.rs +++ b/base_layer/wallet/tests/output_manager_service/service.rs @@ -34,7 +34,6 @@ use tari_comms::{ peer_manager::{NodeIdentity, PeerFeatures}, protocol::rpc::{mock::MockRpcServer, NamedProtocolService}, test_utils::node_identity::build_node_identity, - types::CommsSecretKey, }; use tari_core::{ base_node::rpc::BaseNodeWalletRpcServer, @@ -59,6 +58,7 @@ use tari_crypto::{ script, script::TariScript, }; +use tari_key_manager::{cipher_seed::CipherSeed, mnemonic::Mnemonic}; use tari_p2p::Network; use tari_service_framework::reply_channel; use tari_shutdown::Shutdown; @@ -149,6 +149,38 @@ async fn setup_output_manager_service( wallet_connectivity_mock.set_base_node_wallet_rpc_client(connect_rpc_client(&mut connection).await); } + + let cipher_seed = CipherSeed::from_mnemonic( + &[ + "parade".to_string(), + "genius".to_string(), + "cradle".to_string(), + "milk".to_string(), + "perfect".to_string(), + "ride".to_string(), + "online".to_string(), + "world".to_string(), + "lady".to_string(), + "apple".to_string(), + "rent".to_string(), + "business".to_string(), + "oppose".to_string(), + "force".to_string(), + "tumble".to_string(), + "escape".to_string(), + "tongue".to_string(), + "camera".to_string(), + "ceiling".to_string(), + "edge".to_string(), + "shine".to_string(), + "gauge".to_string(), + "fossil".to_string(), + "orphan".to_string(), + ], + None, + ) + .unwrap(); + let output_manager_service = OutputManagerService::new( OutputManagerServiceConfig { base_node_query_timeout: Duration::from_secs(10), @@ -164,7 +196,7 @@ async fn setup_output_manager_service( shutdown.to_signal(), basenode_service_handle, wallet_connectivity_mock.clone(), - CommsSecretKey::default(), + cipher_seed, ) .await .unwrap(); @@ -230,7 +262,7 @@ pub async fn setup_oms_with_bn_state( shutdown.to_signal(), base_node_service_handle.clone(), connectivity, - CommsSecretKey::default(), + CipherSeed::new(), ) .await .unwrap(); @@ -1645,7 +1677,7 @@ async fn test_oms_key_manager_discrepancy() { let (connection, _tempdir) = get_temp_sqlite_database_connection(); let db = OutputManagerDatabase::new(OutputManagerSqliteDatabase::new(connection, None)); - let master_key1 = CommsSecretKey::random(&mut OsRng); + let master_seed1 = CipherSeed::new(); let output_manager_service = OutputManagerService::new( OutputManagerServiceConfig::default(), @@ -1657,7 +1689,7 @@ async fn test_oms_key_manager_discrepancy() { shutdown.to_signal(), basenode_service_handle.clone(), wallet_connectivity.clone(), - master_key1.clone(), + master_seed1.clone(), ) .await .unwrap(); @@ -1675,14 +1707,14 @@ async fn test_oms_key_manager_discrepancy() { shutdown.to_signal(), basenode_service_handle.clone(), wallet_connectivity.clone(), - master_key1, + master_seed1, ) .await .expect("Should be able to make a new OMS with same master key"); drop(output_manager_service2); let (_oms_request_sender3, oms_request_receiver3) = reply_channel::unbounded(); - let master_key2 = CommsSecretKey::random(&mut OsRng); + let master_seed2 = CipherSeed::new(); let output_manager_service3 = OutputManagerService::new( OutputManagerServiceConfig::default(), oms_request_receiver3, @@ -1693,12 +1725,12 @@ async fn test_oms_key_manager_discrepancy() { shutdown.to_signal(), basenode_service_handle, wallet_connectivity, - master_key2, + master_seed2, ) .await; assert!(matches!( output_manager_service3, - Err(OutputManagerError::MasterSecretKeyMismatch) + Err(OutputManagerError::MasterSeedMismatch) )); } diff --git a/base_layer/wallet/tests/output_manager_service/storage.rs b/base_layer/wallet/tests/output_manager_service/storage.rs index 2d1a8e1f12..09ba45d115 100644 --- a/base_layer/wallet/tests/output_manager_service/storage.rs +++ b/base_layer/wallet/tests/output_manager_service/storage.rs @@ -25,9 +25,8 @@ use aes_gcm::{ Aes256Gcm, }; use rand::{rngs::OsRng, RngCore}; -use tari_common_types::types::PrivateKey; use tari_core::transactions::{tari_amount::MicroTari, CryptoFactories}; -use tari_crypto::keys::SecretKey; +use tari_key_manager::cipher_seed::CipherSeed; use tari_wallet::output_manager_service::{ error::OutputManagerStorageError, service::Balance, @@ -351,7 +350,7 @@ pub fn test_key_manager_crud() { assert!(runtime.block_on(db.increment_key_index()).is_err()); let state1 = KeyManagerState { - master_key: PrivateKey::random(&mut OsRng), + seed: CipherSeed::new(), branch_seed: "blah".to_string(), primary_key_index: 0, }; diff --git a/base_layer/wallet/tests/transaction_service/service.rs b/base_layer/wallet/tests/transaction_service/service.rs index 6fd4652832..d6134ccbc6 100644 --- a/base_layer/wallet/tests/transaction_service/service.rs +++ b/base_layer/wallet/tests/transaction_service/service.rs @@ -50,7 +50,6 @@ use tari_comms::{ peer_manager::{NodeIdentity, PeerFeatures}, protocol::rpc::{mock::MockRpcServer, NamedProtocolService}, test_utils::node_identity::build_node_identity, - types::CommsSecretKey, CommsNode, PeerConnection, }; @@ -96,6 +95,7 @@ use tari_crypto::{ script, script::{ExecutionStack, TariScript}, }; +use tari_key_manager::cipher_seed::CipherSeed; use tari_p2p::{comms_connector::pubsub_connector, domain_message::DomainMessage, Network}; use tari_service_framework::{reply_channel, RegisterHandle, StackBuilder}; use tari_shutdown::{Shutdown, ShutdownSignal}; @@ -205,7 +205,7 @@ pub fn setup_transaction_service>( oms_backend, factories.clone(), Network::Weatherwax.into(), - CommsSecretKey::default(), + CipherSeed::new(), )) .add_initializer(TransactionServiceInitializer::new( TransactionServiceConfig { @@ -339,7 +339,7 @@ pub fn setup_transaction_service_no_comms( shutdown.to_signal(), basenode_service_handle.clone(), wallet_connectivity.clone(), - CommsSecretKey::default(), + CipherSeed::new(), )) .unwrap(); diff --git a/base_layer/wallet/tests/wallet/mod.rs b/base_layer/wallet/tests/wallet/mod.rs index 49004f603a..291c193409 100644 --- a/base_layer/wallet/tests/wallet/mod.rs +++ b/base_layer/wallet/tests/wallet/mod.rs @@ -20,16 +20,9 @@ // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -use std::{panic, path::Path, sync::Arc, time::Duration}; - -use aes_gcm::{ - aead::{generic_array::GenericArray, NewAead}, - Aes256Gcm, -}; -use digest::Digest; use rand::rngs::OsRng; +use std::{panic, path::Path, sync::Arc, time::Duration}; use tari_crypto::{ - common::Blake256, inputs, keys::{PublicKey as PublicKeyTrait, SecretKey}, script, @@ -44,7 +37,7 @@ use tari_common_types::{ use tari_comms::{ multiaddr::Multiaddr, peer_manager::{NodeId, NodeIdentity, Peer, PeerFeatures, PeerFlags}, - types::{CommsPublicKey, CommsSecretKey}, + types::CommsPublicKey, }; use tari_comms_dht::{store_forward::SafConfig, DhtConfig}; use tari_core::transactions::{ @@ -53,6 +46,7 @@ use tari_core::transactions::{ transaction::OutputFeatures, CryptoFactories, }; +use tari_key_manager::cipher_seed::CipherSeed; use tari_p2p::{initialization::P2pConfig, transport::TransportType, Network, DEFAULT_DNS_NAME_SERVER}; use tari_shutdown::{Shutdown, ShutdownSignal}; use tari_test_utils::random; @@ -101,7 +95,7 @@ async fn create_wallet( factories: CryptoFactories, shutdown_signal: ShutdownSignal, passphrase: Option, - recovery_master_key: Option, + recovery_seed: Option, ) -> Result { const NETWORK: Network = Network::Weatherwax; let node_identity = NodeIdentity::random(&mut OsRng, get_next_memory_address(), PeerFeatures::COMMUNICATION_NODE); @@ -173,7 +167,7 @@ async fn create_wallet( output_manager_backend, contacts_backend, shutdown_signal, - recovery_master_key, + recovery_seed, ) .await } @@ -324,25 +318,16 @@ async fn test_wallet() { panic!("Should not be able to instantiate encrypted wallet without cipher"); } - let passphrase_hash = Blake256::new() - .chain("wrong passphrase".to_string().as_bytes()) - .finalize(); - let key = GenericArray::from_slice(passphrase_hash.as_slice()); - let cipher = Aes256Gcm::new(key); - let result = WalletSqliteDatabase::new(connection.clone(), Some(cipher)); + let result = WalletSqliteDatabase::new(connection.clone(), Some("wrong passphrase".to_string())); if let Err(err) = result { - assert!(matches!(err, WalletStorageError::IncorrectPassword)); + assert!(matches!(err, WalletStorageError::InvalidPassphrase)); } else { panic!("Should not be able to instantiate encrypted wallet without cipher"); } - let passphrase_hash = Blake256::new() - .chain("It's turtles all the way down".to_string().as_bytes()) - .finalize(); - let key = GenericArray::from_slice(passphrase_hash.as_slice()); - let cipher = Aes256Gcm::new(key); - let db = WalletSqliteDatabase::new(connection, Some(cipher)).expect("Should be able to instantiate db with cipher"); + let db = WalletSqliteDatabase::new(connection, Some("It's turtles all the way down".to_string())) + .expect("Should be able to instantiate db with cipher"); drop(db); let mut shutdown_a = Shutdown::new(); @@ -389,11 +374,9 @@ async fn test_wallet() { .join("alice_db_backup") .with_extension("sqlite3"); - alice_wallet - .db - .set_master_secret_key(alice_identity.secret_key().clone()) - .await - .unwrap(); + let alice_seed = CipherSeed::new(); + + alice_wallet.db.set_master_seed(alice_seed).await.unwrap(); shutdown_a.trigger(); alice_wallet.wait_until_shutdown().await; @@ -405,16 +388,16 @@ async fn test_wallet() { let connection = run_migration_and_create_sqlite_connection(¤t_wallet_path).expect("Could not open Sqlite db"); let wallet_db = WalletDatabase::new(WalletSqliteDatabase::new(connection.clone(), None).unwrap()); - let master_private_key = wallet_db.get_master_secret_key().await.unwrap(); - assert!(master_private_key.is_some()); + let master_seed = wallet_db.get_master_seed().await.unwrap(); + assert!(master_seed.is_some()); // Checking that the backup has had its Comms Private Key is cleared. let connection = run_migration_and_create_sqlite_connection(&backup_wallet_path).expect( "Could not open Sqlite db", ); let backup_wallet_db = WalletDatabase::new(WalletSqliteDatabase::new(connection.clone(), None).unwrap()); - let master_secret_key = backup_wallet_db.get_master_secret_key().await.unwrap(); - assert!(master_secret_key.is_none()); + let master_seed = backup_wallet_db.get_master_seed().await.unwrap(); + assert!(master_seed.is_none()); shutdown_b.trigger(); @@ -428,14 +411,14 @@ async fn test_do_not_overwrite_master_key() { // create a wallet and shut it down let mut shutdown = Shutdown::new(); - let (recovery_master_key, _) = PublicKey::random_keypair(&mut OsRng); + let recovery_seed = CipherSeed::new(); let wallet = create_wallet( dir.path(), "wallet_db", factories.clone(), shutdown.to_signal(), None, - Some(recovery_master_key), + Some(recovery_seed), ) .await .unwrap(); @@ -444,14 +427,14 @@ async fn test_do_not_overwrite_master_key() { // try to use a new master key to create a wallet using the existing wallet database let shutdown = Shutdown::new(); - let (recovery_master_key, _) = PublicKey::random_keypair(&mut OsRng); + let recovery_seed = CipherSeed::new(); match create_wallet( dir.path(), "wallet_db", factories.clone(), shutdown.to_signal(), None, - Some(recovery_master_key.clone()), + Some(recovery_seed.clone()), ) .await { @@ -467,7 +450,7 @@ async fn test_do_not_overwrite_master_key() { factories.clone(), shutdown.to_signal(), None, - Some(recovery_master_key), + Some(recovery_seed), ) .await .unwrap(); diff --git a/base_layer/wallet_ffi/src/error.rs b/base_layer/wallet_ffi/src/error.rs index 8c86a2b355..13ed469f11 100644 --- a/base_layer/wallet_ffi/src/error.rs +++ b/base_layer/wallet_ffi/src/error.rs @@ -26,7 +26,7 @@ use tari_crypto::{ signatures::SchnorrSignatureError, tari_utilities::{hex::HexError, ByteArrayError}, }; -use tari_key_manager::mnemonic::MnemonicError; +use tari_key_manager::error::MnemonicError; use tari_wallet::{ contacts_service::error::{ContactsServiceError, ContactsServiceStorageError}, error::{WalletError, WalletStorageError}, @@ -270,7 +270,7 @@ impl From for LibWalletError { code: 427, message: format!("{:?}", w), }, - WalletError::WalletStorageError(WalletStorageError::IncorrectPassword) => Self { + WalletError::WalletStorageError(WalletStorageError::InvalidPassphrase) => Self { code: 428, message: format!("{:?}", w), }, diff --git a/base_layer/wallet_ffi/src/lib.rs b/base_layer/wallet_ffi/src/lib.rs index f18156ff00..b8e87fbef2 100644 --- a/base_layer/wallet_ffi/src/lib.rs +++ b/base_layer/wallet_ffi/src/lib.rs @@ -159,6 +159,7 @@ use tari_comms::{ }; use tari_comms_dht::{store_forward::SafConfig, DbConnectionUrl, DhtConfig}; use tari_core::transactions::{tari_amount::MicroTari, transaction::OutputFeatures, CryptoFactories}; +use tari_key_manager::cipher_seed::CipherSeed; use tari_p2p::{ transport::{TorConfig, TransportType, TransportType::Tor}, Network, @@ -972,9 +973,12 @@ pub unsafe extern "C" fn seed_words_push_word( (*seed_words).0.push(word_string); if (*seed_words).0.len() >= 24 { - return if let Err(e) = TariPrivateKey::from_mnemonic(&(*seed_words).0) { - log::error!(target: LOG_TARGET, "Problem building private key from seed phrase"); - error = LibWalletError::from(e).code; + return if let Err(e) = CipherSeed::from_mnemonic(&(*seed_words).0, None) { + log::error!( + target: LOG_TARGET, + "Problem building valid private seed from seed phrase" + ); + error = LibWalletError::from(WalletError::KeyManagerError(e)).code; ptr::swap(error_out, &mut error as *mut c_int); SeedWordPushResult::InvalidSeedPhrase as u8 } else { @@ -2802,14 +2806,14 @@ pub unsafe extern "C" fn wallet_create( None }; - let recovery_master_key = if seed_words.is_null() { + let recovery_seed = if seed_words.is_null() { None } else { - match TariPrivateKey::from_mnemonic(&(*seed_words).0) { - Ok(private_key) => Some(private_key), + match CipherSeed::from_mnemonic(&(*seed_words).0, None) { + Ok(seed) => Some(seed), Err(e) => { error!(target: LOG_TARGET, "Mnemonic Error for given seed words: {:?}", e); - error = LibWalletError::from(e).code; + error = LibWalletError::from(WalletError::KeyManagerError(e)).code; ptr::swap(error_out, &mut error as *mut c_int); return ptr::null_mut(); }, @@ -2891,7 +2895,7 @@ pub unsafe extern "C" fn wallet_create( output_manager_backend, contacts_backend, shutdown.to_signal(), - recovery_master_key, + recovery_seed, )); match w { @@ -5764,9 +5768,9 @@ mod test { run_migration_and_create_sqlite_connection(&sql_database_path).expect("Could not open Sqlite db"); let wallet_backend = WalletDatabase::new(WalletSqliteDatabase::new(connection, None).unwrap()); - let stored_key = runtime.block_on(wallet_backend.get_master_secret_key()).unwrap(); + let stored_seed = runtime.block_on(wallet_backend.get_master_seed()).unwrap(); drop(wallet_backend); - assert!(stored_key.is_none(), "No key should be stored yet"); + assert!(stored_seed.is_none(), "No key should be stored yet"); let alice_wallet = wallet_create( alice_config, @@ -5799,10 +5803,7 @@ mod test { run_migration_and_create_sqlite_connection(&sql_database_path).expect("Could not open Sqlite db"); let wallet_backend = WalletDatabase::new(WalletSqliteDatabase::new(connection, None).unwrap()); - let stored_key1 = runtime - .block_on(wallet_backend.get_master_secret_key()) - .unwrap() - .unwrap(); + let stored_seed1 = runtime.block_on(wallet_backend.get_master_seed()).unwrap().unwrap(); drop(wallet_backend); @@ -5839,12 +5840,9 @@ mod test { run_migration_and_create_sqlite_connection(&sql_database_path).expect("Could not open Sqlite db"); let wallet_backend = WalletDatabase::new(WalletSqliteDatabase::new(connection, None).unwrap()); - let stored_key2 = runtime - .block_on(wallet_backend.get_master_secret_key()) - .unwrap() - .unwrap(); + let stored_seed2 = runtime.block_on(wallet_backend.get_master_seed()).unwrap().unwrap(); - assert_eq!(stored_key1, stored_key2); + assert_eq!(stored_seed1, stored_seed2); drop(wallet_backend); @@ -5861,9 +5859,9 @@ mod test { run_migration_and_create_sqlite_connection(&sql_database_path).expect("Could not open Sqlite db"); let wallet_backend = WalletDatabase::new(WalletSqliteDatabase::new(connection, None).unwrap()); - let stored_key = runtime.block_on(wallet_backend.get_master_secret_key()).unwrap(); + let stored_seed = runtime.block_on(wallet_backend.get_master_seed()).unwrap(); - assert!(stored_key.is_none(), "key should be cleared"); + assert!(stored_seed.is_none(), "key should be cleared"); drop(wallet_backend); string_destroy(alice_network_str as *mut c_char); @@ -6228,9 +6226,9 @@ mod test { let recovery_in_progress_ptr = &mut recovery_in_progress as *mut bool; let mnemonic = vec![ - "clever", "jaguar", "bus", "engage", "oil", "august", "media", "high", "trick", "remove", "tiny", - "join", "item", "tobacco", "orange", "pony", "tomorrow", "also", "dignity", "giraffe", "little", - "board", "army", "scale", + "parade", "genius", "cradle", "milk", "perfect", "ride", "online", "world", "lady", "apple", "rent", + "business", "oppose", "force", "tumble", "escape", "tongue", "camera", "ceiling", "edge", "shine", + "gauge", "fossil", "orphan", ]; let seed_words = seed_words_create();