// Copyright 2021 The Grin Developers // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Default wallet lifecycle provider use crate::config::{ config, GlobalWalletConfig, GlobalWalletConfigMembers, TorConfig, WalletConfig, GRIN_WALLET_DIR, }; use crate::core::global; use crate::keychain::Keychain; use crate::libwallet::{Error, NodeClient, WalletBackend, WalletInitStatus, WalletLCProvider}; use crate::lifecycle::seed::WalletSeed; use crate::util::secp::key::SecretKey; use crate::util::ZeroingString; use crate::LMDBBackend; use grin_util::logger::LoggingConfig; use std::fs; use std::path::PathBuf; pub struct DefaultLCProvider<'a, C, K> where C: NodeClient + 'a, K: Keychain + 'a, { data_dir: String, node_client: C, backend: Option + 'a>>, } impl<'a, C, K> DefaultLCProvider<'a, C, K> where C: NodeClient + 'a, K: Keychain + 'a, { /// Create new provider pub fn new(node_client: C) -> Self { DefaultLCProvider { node_client, data_dir: "default".to_owned(), backend: None, } } } impl<'a, C, K> WalletLCProvider<'a, C, K> for DefaultLCProvider<'a, C, K> where C: NodeClient + 'a, K: Keychain + 'a, { fn set_top_level_directory(&mut self, dir: &str) -> Result<(), Error> { self.data_dir = dir.to_owned(); Ok(()) } fn get_top_level_directory(&self) -> Result { Ok(self.data_dir.to_owned()) } fn create_config( &self, chain_type: &global::ChainTypes, file_name: &str, wallet_config: Option, logging_config: Option, tor_config: Option, ) -> Result<(), Error> { let mut default_config = GlobalWalletConfig::for_chain(&chain_type); let config_file_version = match default_config.members.as_ref() { Some(m) => m.clone().config_file_version, None => None, }; let logging = match logging_config { Some(l) => Some(l), None => match default_config.members.as_ref() { Some(m) => m.clone().logging, None => None, }, }; let wallet = match wallet_config { Some(w) => w, None => match default_config.members.as_ref() { Some(m) => m.clone().wallet, None => WalletConfig::default(), }, }; let tor = match tor_config { Some(t) => Some(t), None => match default_config.members.as_ref() { Some(m) => m.clone().tor, None => Some(TorConfig::default()), }, }; default_config = GlobalWalletConfig { members: Some(GlobalWalletConfigMembers { config_file_version, wallet, tor, logging, }), ..default_config }; let mut config_file_name = PathBuf::from(self.data_dir.clone()); config_file_name.push(file_name); // create top level dir if it doesn't exist let dd = PathBuf::from(self.data_dir.clone()); if !dd.exists() { // try create fs::create_dir_all(dd)?; } let mut data_dir_name = PathBuf::from(self.data_dir.clone()); data_dir_name.push(GRIN_WALLET_DIR); if config_file_name.exists() && data_dir_name.exists() { let msg = format!( "{} already exists in the target directory ({}). Please remove it first", file_name, config_file_name.to_str().unwrap() ); return Err(Error::Lifecycle(msg)); } // just leave as is if file exists but there's no data dir if config_file_name.exists() { return Ok(()); } let mut abs_path = std::env::current_dir()?; abs_path.push(self.data_dir.clone()); default_config.update_paths(&abs_path); let res = default_config.write_to_file(config_file_name.to_str().unwrap(), false, None, None); if let Err(e) = res { let msg = format!( "Error creating config file as ({}): {}", config_file_name.to_str().unwrap(), e ); return Err(Error::Lifecycle(msg)); } info!( "File {} configured and created", config_file_name.to_str().unwrap(), ); let mut api_secret_path = PathBuf::from(self.data_dir.clone()); api_secret_path.push(PathBuf::from(config::API_SECRET_FILE_NAME)); if !api_secret_path.exists() { config::init_api_secret(&api_secret_path).unwrap(); } else { config::check_api_secret(&api_secret_path).unwrap(); } Ok(()) } fn create_wallet( &mut self, _name: Option<&str>, mnemonic: Option, mnemonic_length: usize, password: ZeroingString, test_mode: bool, ) -> Result<(), Error> { let mut data_dir_name = PathBuf::from(self.data_dir.clone()); data_dir_name.push(GRIN_WALLET_DIR); let data_dir_name = data_dir_name.to_str().unwrap(); let exists = WalletSeed::seed_file_exists(&data_dir_name); if !test_mode { if let Ok(true) = exists { let msg = format!("Wallet seed already exists at: {}", data_dir_name); return Err(Error::WalletSeedExists(msg)); } } WalletSeed::init_file( &data_dir_name, mnemonic_length, mnemonic.clone(), password, test_mode, ) .map_err(|_| { Error::Lifecycle("Error creating wallet seed (is mnemonic valid?)".to_owned()) })?; info!("Wallet seed file created"); let mut wallet: LMDBBackend<'a, C, K> = match LMDBBackend::new(&data_dir_name, self.node_client.clone()) { Err(e) => { let msg = format!("Error creating wallet: {}, Data Dir: {}", e, &data_dir_name); error!("{}", msg); return Err(Error::Lifecycle(msg).into()); } Ok(d) => d, }; // Save init status of this wallet, to determine whether it needs a full UTXO scan let mut batch = wallet.batch_no_mask()?; match mnemonic { Some(_) => batch.save_init_status(WalletInitStatus::InitNeedsScanning)?, None => batch.save_init_status(WalletInitStatus::InitNoScanning)?, }; batch.commit()?; info!("Wallet database backend created at {}", data_dir_name); Ok(()) } fn open_wallet( &mut self, _name: Option<&str>, password: ZeroingString, create_mask: bool, use_test_rng: bool, ) -> Result, Error> { let mut data_dir_name = PathBuf::from(self.data_dir.clone()); data_dir_name.push(GRIN_WALLET_DIR); let data_dir_name = data_dir_name.to_str().unwrap(); let mut wallet: LMDBBackend<'a, C, K> = match LMDBBackend::new(&data_dir_name, self.node_client.clone()) { Err(e) => { let msg = format!("Error opening wallet: {}, Data Dir: {}", e, &data_dir_name); return Err(Error::Lifecycle(msg)); } Ok(d) => d, }; let wallet_seed = WalletSeed::from_file(&data_dir_name, password).map_err(|_| { Error::Lifecycle("Error opening wallet (is password correct?)".to_owned()) })?; let keychain = wallet_seed .derive_keychain(global::is_testnet()) .map_err(|_| Error::Lifecycle("Error deriving keychain".to_owned()))?; let mask = wallet.set_keychain(Box::new(keychain), create_mask, use_test_rng)?; self.backend = Some(Box::new(wallet)); Ok(mask) } fn close_wallet(&mut self, _name: Option<&str>) -> Result<(), Error> { if let Some(b) = self.backend.as_mut() { b.close()? } self.backend = None; Ok(()) } fn wallet_exists(&self, _name: Option<&str>) -> Result { let mut data_dir_name = PathBuf::from(self.data_dir.clone()); data_dir_name.push(GRIN_WALLET_DIR); let data_dir_name = data_dir_name.to_str().unwrap(); let res = WalletSeed::seed_file_exists(&data_dir_name) .map_err(|_| Error::CallbackImpl("Error checking for wallet existence"))?; Ok(res) } fn get_mnemonic( &self, _name: Option<&str>, password: ZeroingString, ) -> Result { let mut data_dir_name = PathBuf::from(self.data_dir.clone()); data_dir_name.push(GRIN_WALLET_DIR); let data_dir_name = data_dir_name.to_str().unwrap(); let wallet_seed = WalletSeed::from_file(&data_dir_name, password) .map_err(|_| Error::Lifecycle("Error opening wallet seed file".into()))?; let res = wallet_seed .to_mnemonic() .map_err(|_| Error::Lifecycle("Error recovering wallet seed".into()))?; Ok(ZeroingString::from(res)) } fn validate_mnemonic(&self, mnemonic: ZeroingString) -> Result<(), Error> { match WalletSeed::from_mnemonic(mnemonic) { Ok(_) => Ok(()), Err(_) => Err(Error::GenericError("Validating mnemonic".into())), } } fn recover_from_mnemonic( &self, mnemonic: ZeroingString, password: ZeroingString, ) -> Result<(), Error> { let mut data_dir_name = PathBuf::from(self.data_dir.clone()); data_dir_name.push(GRIN_WALLET_DIR); let data_dir_name = data_dir_name.to_str().unwrap(); WalletSeed::recover_from_phrase(data_dir_name, mnemonic, password) .map_err(|_| Error::Lifecycle("Error recovering from mnemonic".into()))?; Ok(()) } fn change_password( &self, _name: Option<&str>, old: ZeroingString, new: ZeroingString, ) -> Result<(), Error> { let mut data_dir_name = PathBuf::from(self.data_dir.clone()); data_dir_name.push(GRIN_WALLET_DIR); let data_dir_name = data_dir_name.to_str().unwrap(); // get seed for later check let orig_wallet_seed = WalletSeed::from_file(&data_dir_name, old) .map_err(|_| Error::Lifecycle("Error opening wallet seed file".into()))?; let orig_mnemonic = orig_wallet_seed .to_mnemonic() .map_err(|_| Error::Lifecycle("Error recovering mnemonic".into()))?; // Back up existing seed, and keep track of filename as we're deleting it // once the password change is confirmed let backup_name = WalletSeed::backup_seed(data_dir_name) .map_err(|_| Error::Lifecycle("Error temporarily backing up existing seed".into()))?; // Delete seed file WalletSeed::delete_seed_file(data_dir_name).map_err(|_| { Error::Lifecycle("Unable to delete seed file for password change".into()) })?; // Init a new file let _ = WalletSeed::init_file( data_dir_name, 0, Some(ZeroingString::from(orig_mnemonic)), new.clone(), false, ); info!("Wallet seed file created"); let new_wallet_seed = WalletSeed::from_file(&data_dir_name, new) .map_err(|_| Error::Lifecycle("Error opening wallet seed file".into()))?; if orig_wallet_seed != new_wallet_seed { let msg = "New and Old wallet seeds are not equal on password change, not removing backups." .to_string(); return Err(Error::Lifecycle(msg)); } // Removin info!("Password change confirmed, removing old seed file."); fs::remove_file(backup_name).map_err(|e| Error::IO(e.to_string()))?; Ok(()) } fn delete_wallet(&self, _name: Option<&str>) -> Result<(), Error> { let data_dir_name = PathBuf::from(self.data_dir.clone()); warn!( "Removing all wallet data from: {}", data_dir_name.to_str().unwrap() ); fs::remove_dir_all(data_dir_name).map_err(|e| Error::IO(e.to_string()))?; Ok(()) } fn wallet_inst(&mut self) -> Result<&mut Box + 'a>, Error> { match self.backend.as_mut() { None => { let msg = "Wallet has not been opened".into(); Err(Error::Lifecycle(msg)) } Some(_) => Ok(&mut *self.backend.as_mut().unwrap()), } } }