Wallet Seed Encryption + BIP39 (#2004)

* add encryption/decryption of wallet seed

* rustfmt

* update passphrase to encrypt/decrypt wallet

* rustfmt

* add conversion for old wallet seeds

* add ability to restore old seed

* rustfmt

* add bip39 recovery phrase support, conversion from existing seed formats

* rustfmt

* error recovery

* rustfmt

* add password prompts where required

* rustfmt

* add note specifying wallet conversion

* update documentation

* doc update
This commit is contained in:
Yeastplume
2018-11-24 10:03:09 +00:00
committed by GitHub
parent 80a09c3fec
commit 5992542152
19 changed files with 465 additions and 102 deletions
Generated
+13
View File
@@ -687,6 +687,7 @@ dependencies = [
"humansize 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"reqwest 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)",
"rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
"tar 0.4.20 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -954,6 +955,7 @@ dependencies = [
"log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
"prettytable-rs 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)",
"ring 0.13.3 (registry+https://github.com/rust-lang/crates.io-index)",
"serde 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_derive 1.0.80 (registry+https://github.com/rust-lang/crates.io-index)",
"serde_json 1.0.33 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -1973,6 +1975,16 @@ dependencies = [
"digest 0.7.6 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rpassword"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
"winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "rustc-demangle"
version = "0.1.9"
@@ -3009,6 +3021,7 @@ dependencies = [
"checksum reqwest 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ab52e462d1e15891441aeefadff68bdea005174328ce3da0a314f2ad313ec837"
"checksum ring 0.13.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7ed733c36010c3d4d4718588f16a6c06a670b01c0047029ae81c3ca0acd81ff5"
"checksum ripemd160 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "482aa56cc68aaeccdaaff1cc5a72c247da8bbad3beb174ca5741f274c22883fb"
"checksum rpassword 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d127299b02abda51634f14025aec43ae87a7aa7a95202b6a868ec852607d1451"
"checksum rustc-demangle 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "bcfe5b13211b4d78e5c2cadfebd7769197d95c639c35a50057eb4c05de811395"
"checksum rustc-serialize 0.3.24 (registry+https://github.com/rust-lang/crates.io-index)" = "dcf128d1287d2ea9d80910b5f1120d0b8eede3fbf1abe91c40d39ea7d51e6fda"
"checksum rustc_version 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a"
+1
View File
@@ -29,6 +29,7 @@ serde = "1"
serde_json = "1"
log = "0.4"
term = "0.5"
rpassword = "2.0.0"
grin_api = { path = "./api", version = "0.4.1" }
grin_config = { path = "./config", version = "0.4.1" }
+61 -21
View File
@@ -18,8 +18,9 @@ When creating a new wallet, the file structure should be:
* `grin-wallet.toml` contains configuration information for the wallet. You can modify values within
to change ports, the address of your grin node, or logging values.
* `wallet_data/wallet.seed` is your master seed file. You must back this file up somewhere in order to
be able to recover or restore your wallet (along with its password, if given).
* `wallet_data/wallet.seed` is your master seed file. Its contents are encrypted with your password (required).
You should back this file up somewhere in order to be able to recover or restore your wallet. Your seed file
can also be recovered using a seed phrase if you lose this file or forget your password.
### Data Directory
@@ -59,12 +60,12 @@ and the results verified against the latest chain information.
##### Password
All keys generated by your wallet are combinations of the master seed + a password. If no password is provided, it's assumed this
password is blank. If you do provide a password, all operations will use the seed+password and you will need the password to view or
spend any generated outputs. The password is specified with `-p`
Your wallet.seed file, which contains your wallet's unique master seed, is encrypted with your password. Your password is specified
at wallet creation time, and must be provided for any wallet operation. You will be prompted for your password when required, but
you can also specify it on the command line by providing the `-p`argument.
```sh
[host]$ grin wallet -p mypassword info
[host]$ grin wallet -p mypass info
```
## Basic Wallet Commands
@@ -74,22 +75,62 @@ spend any generated outputs. The password is specified with `-p`
### init
Before using a wallet a new `grin-wallet.toml` configuration file, seed file `wallet.seed` and storage database need
Before using a wallet a new `grin-wallet.toml` configuration file, master seed contained in `wallet.seed` and storage database need
to be generated via the init command as follows:
By default this will place your wallet files into `~/.grin`. It is VERY IMPORTANT that you back up the `~/.grin/wallet_data/wallet.seed`
file somewhere safe and private, and ensure you somehow remember the password used to generate the wallet.
```sh
[host]$ grin wallet init
```
Alternatively, if you'd like to run a wallet in a directory other than the default, you can run:
You will be prompted to enter a password for the new wallet. By default, your wallet files will be placed into `~/.grin`. Alternatively,
if you'd like to run a wallet in a directory other than the default, you can run:
```sh
[host]$ grin wallet [-p password] init -h
[host]$ grin wallet -p mypass init -h
```
This will create a `grin-wallet.toml` file in the current directory configured to use the data files in the current directory,
as well as all needed data files. When running any `grin wallet` command, grin will check the current directory to see if
a `grin-wallet.toml` file exists. If not it will use the default in `~/.grin`
The init command will also print a 24 (or 12) word recovery phrase, which you should write down and store in a non-digital format. This
phrase can be used to re-create your master seed file if it gets lost or corrupted, or you forget the wallet password. If you'd prefer
to use a 12-word recovery phrase, you can also pass in the `--short_wordlist` or `-s` parameter.
It is also highly recommended that you back up the `~/.grin/wallet_data/wallet.seed` file somewhere safe and private,
and ensure you somehow remember the password used to encrypt the wallet seed file.
### recover
The `recover` command is used to regenerate your wallet seed file from your recovery phrase. Note that this operation only
restores your seed file, not the outputs stored in your wallet. If, for instance, you forget your wallet password, you can
delete the `wallet_data/wallet.seed` file from your wallet data directory, run `grin wallet recover`, and (provided you used
the correct recovery phrase,) your wallet contents should again be usable.
To recover your wallet seed, delete (or backup) the wallet's `wallet_data/wallet.seed` file, then run:
```sh
[host]$ grin wallet recover -p "[12 or 24 word passphrase separated by spaces"
e.g:
[host]$ grin wallet recover -p "shiver alarm excuse turtle absorb surface lunch virtual want remind hard slow vacuum park silver asthma engage library battle jelly buffalo female inquiry wire"
```
If you're restoring a wallet from scratch, you'll then need to use the `grin wallet restore` command to scan the chain
for your outputs and restore them. See the `grin wallet restore` command below for details of the entire process.
You can also view your recovery phrase with your password by running the recover command without any arguments, e.g:
```sh
[host]$ grin wallet recover
Password:
Your recovery phrase is:
shiver alarm excuse turtle absorb surface lunch virtual want remind hard slow vacuum park silver asthma engage library battle jelly buffalo female inquiry wire
Please back-up these words in a non-digital format.
```
### account
To create a new account, use the 'grin wallet account' command with the argument '-c', e.g.:
@@ -296,7 +337,7 @@ Wallet Outputs - Block Height: 49
#### cancel
Everything before Step 6 in the send phase above happens completely locally in the wallets' data storage and separately from the chain.
Everything before Step 6 in the send phase above happens completely locally in the wallets' data storage and separately from the chain.
Since it's very easy for a sender, (through error or malice,) to fail to post a transaction to the chain, it's very possible for the contents
of a wallet to become locked, with all outputs unable to be selected because the wallet is waiting for a transaction that will never hit
the chain to complete. For example, in the output from `grin wallet txs -i 6` above, the transaction is showing as `confirmed == false`
@@ -342,7 +383,7 @@ This will create a file called tx_3.json containing your raw transaction data. N
##### restore
If for some reason the wallet cancel commands above don't work, or you need to restore from a backed up `wallet.seed` file and password, you can perform a full wallet restore.
If for some reason the wallet cancel commands above don't work, you need to restore from a backed up `wallet.seed` file and password, or have recovered the wallet seed from a recovery phrase, you can perform a full wallet restore.
To do this, generate an empty wallet somewhere with:
@@ -357,18 +398,17 @@ Delete the newly generated wallet data directory and seed file:
[host@new_wallet_dir]# rm wallet_data/wallet.seed
```
Then copy your backed up `wallet.seed` file into the new `wallet_data` directory, ensuring it's called `wallet.seed`
If you need to recover your wallet seed from a recovery phrase, use the `grin wallet recover -p "[recovery phrase]" command
as outlined above. Otherwise, if you're restoring from a backed-up seed file, simply copy your backed up `wallet.seed` file
into the new `wallet_data` directory, ensuring it's called `wallet.seed`
```sh
[host@new_wallet_dir]# cp OLD_WALLET.seed wallet_data/wallet.seed
```
Then ensure that you're running a grin node, and makes sure nothing is attempting to mine into your wallet. Then, in the
wallet directory:
Ensure the Grin node with which your wallet is talking is running, and make sure nothing is attempting to mine into your wallet.
Then, in the wallet directory:
```sh
grin wallet restore
```
Note this operation can potentially take a long time. Once it's done, your wallet outputs should be restored, and you can
transact with your restored wallet as before the backup.
transact with your restored wallet as before the backup. Your transaction log history is not restored, and will simply
contain incoming transactions for each output found.
+13 -6
View File
@@ -43,6 +43,16 @@ impl Keychain for ExtKeychain {
Ok(keychain)
}
fn from_mnemonic(word_list: &str, extension_word: &str) -> Result<Self, Error> {
let secp = secp::Secp256k1::with_caps(secp::ContextFlag::Commit);
let master = ExtendedPrivKey::from_mnemonic(&secp, word_list, extension_word)?;
let keychain = ExtKeychain {
secp: secp,
master: master,
};
Ok(keychain)
}
/// For testing - probably not a good idea to use outside of tests.
fn from_random_seed() -> Result<ExtKeychain, Error> {
let seed: String = thread_rng().sample_iter(&Alphanumeric).take(16).collect();
@@ -85,8 +95,7 @@ impl Keychain for ExtKeychain {
} else {
None
}
})
.collect();
}).collect();
let mut neg_keys: Vec<SecretKey> = blind_sum
.negative_key_ids
@@ -98,8 +107,7 @@ impl Keychain for ExtKeychain {
} else {
None
}
})
.collect();
}).collect();
pos_keys.extend(
&blind_sum
@@ -220,8 +228,7 @@ mod test {
&BlindSum::new()
.add_blinding_factor(BlindingFactor::from_secret_key(skey1))
.add_blinding_factor(BlindingFactor::from_secret_key(skey2))
)
.unwrap(),
).unwrap(),
BlindingFactor::from_secret_key(skey3),
);
}
+1
View File
@@ -432,6 +432,7 @@ impl ExtKeychainPath {
pub trait Keychain: Sync + Send + Clone {
fn from_seed(seed: &[u8]) -> Result<Self, Error>;
fn from_mnemonic(word_list: &str, extension_word: &str) -> Result<Self, Error>;
fn from_random_seed() -> Result<Self, Error>;
fn root_key_id() -> Identifier;
fn derive_key_id(depth: u8, d1: u32, d2: u32, d3: u32, d4: u32) -> Identifier;
+6 -6
View File
@@ -263,7 +263,7 @@ impl LocalServerContainer {
self.wallet_config.data_file_dir = self.working_dir.clone();
let _ = fs::create_dir_all(self.wallet_config.clone().data_file_dir);
let r = wallet::WalletSeed::init_file(&self.wallet_config);
let r = wallet::WalletSeed::init_file(&self.wallet_config, 32, "");
let client_n = HTTPNodeClient::new(&self.wallet_config.check_node_api_http_addr, None);
@@ -295,9 +295,9 @@ impl LocalServerContainer {
pub fn get_wallet_seed(config: &WalletConfig) -> wallet::WalletSeed {
let _ = fs::create_dir_all(config.clone().data_file_dir);
wallet::WalletSeed::init_file(config).unwrap();
wallet::WalletSeed::init_file(config, 32, "").unwrap();
let wallet_seed =
wallet::WalletSeed::from_file(config).expect("Failed to read wallet seed file.");
wallet::WalletSeed::from_file(config, "").expect("Failed to read wallet seed file.");
wallet_seed
}
@@ -306,7 +306,7 @@ impl LocalServerContainer {
wallet_seed: &wallet::WalletSeed,
) -> wallet::WalletInfo {
let keychain: keychain::ExtKeychain = wallet_seed
.derive_keychain("")
.derive_keychain()
.expect("Failed to derive keychain from seed file and passphrase.");
let client_n = HTTPNodeClient::new(&config.check_node_api_http_addr, None);
let mut wallet = LMDBBackend::new(config.clone(), "", client_n)
@@ -329,10 +329,10 @@ impl LocalServerContainer {
.expect("Could not parse amount as a number with optional decimal point.");
let wallet_seed =
wallet::WalletSeed::from_file(config).expect("Failed to read wallet seed file.");
wallet::WalletSeed::from_file(config, "").expect("Failed to read wallet seed file.");
let keychain: keychain::ExtKeychain = wallet_seed
.derive_keychain("")
.derive_keychain()
.expect("Failed to derive keychain from seed file and passphrase.");
let client_n = HTTPNodeClient::new(&config.check_node_api_http_addr, None);
+1 -1
View File
@@ -883,7 +883,7 @@ pub fn create_wallet(
) -> Arc<Mutex<WalletInst<HTTPNodeClient, keychain::ExtKeychain>>> {
let mut wallet_config = WalletConfig::default();
wallet_config.data_file_dir = String::from(dir);
let _ = wallet::WalletSeed::init_file(&wallet_config);
let _ = wallet::WalletSeed::init_file(&wallet_config, 32, "");
let mut wallet: LMDBBackend<HTTPNodeClient, keychain::ExtKeychain> =
LMDBBackend::new(wallet_config.clone(), "", client_n).unwrap_or_else(|e| {
panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config)
+122 -20
View File
@@ -13,6 +13,7 @@
// limitations under the License.
use clap::ArgMatches;
use rpassword;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::thread;
@@ -31,9 +32,10 @@ use keychain;
use servers::start_webwallet_server;
use util::file::get_first_line;
pub fn _init_wallet_seed(wallet_config: WalletConfig) {
if let Err(_) = WalletSeed::from_file(&wallet_config) {
WalletSeed::init_file(&wallet_config).expect("Failed to create wallet seed file.");
pub fn _init_wallet_seed(wallet_config: WalletConfig, password: &str) {
if let Err(_) = WalletSeed::from_file(&wallet_config, password) {
WalletSeed::init_file(&wallet_config, 32, password)
.expect("Failed to create wallet seed file.");
};
}
@@ -48,6 +50,33 @@ pub fn seed_exists(wallet_config: WalletConfig) -> bool {
}
}
pub fn prompt_password(args: &ArgMatches) -> String {
match args.value_of("pass") {
None => {
println!("Temporary note:");
println!(
"If this is your first time running your wallet since BIP32 (word lists) \
were implemented, your seed will be converted to \
the new format. Please ensure the provided password is correct."
);
println!("If this goes wrong, your old 'wallet.seed' file has been saved as 'wallet.seed.bak' \
Rename this file to back to `wallet.seed` and try again");
rpassword::prompt_password_stdout("Password: ").unwrap()
}
Some(p) => p.to_owned(),
}
}
pub fn prompt_password_confirm() -> String {
let first = rpassword::prompt_password_stdout("Password: ").unwrap();
let second = rpassword::prompt_password_stdout("Confirm Password: ").unwrap();
if first != second {
println!("Passwords do not match");
std::process::exit(0);
}
first
}
pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i32 {
// just get defaults from the global config
let mut wallet_config = config.members.unwrap().wallet;
@@ -74,15 +103,22 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
}
let node_api_secret = get_first_line(wallet_config.node_api_secret_path.clone());
// Derive the keychain based on seed from seed file and specified passphrase.
// Decrypt the seed from the seed file and derive the keychain.
// Generate the initial wallet seed if we are running "wallet init".
if let ("init", Some(_)) = wallet_args.subcommand() {
WalletSeed::init_file(&wallet_config).expect("Failed to init wallet seed file.");
if let ("init", Some(r)) = wallet_args.subcommand() {
let list_length = match r.is_present("short_wordlist") {
false => 32,
true => 16,
};
println!("Please enter a password for your new wallet");
let passphrase = prompt_password_confirm();
WalletSeed::init_file(&wallet_config, list_length, &passphrase)
.expect("Failed to init wallet seed file.");
info!("Wallet seed file created");
let client_n =
HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, node_api_secret);
let _: LMDBBackend<HTTPNodeClient, keychain::ExtKeychain> =
LMDBBackend::new(wallet_config.clone(), "", client_n).unwrap_or_else(|e| {
LMDBBackend::new(wallet_config.clone(), &passphrase, client_n).unwrap_or_else(|e| {
panic!(
"Error creating DB for wallet: {} Config: {:?}",
e, wallet_config
@@ -95,13 +131,45 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
return 0;
}
let passphrase = match wallet_args.value_of("pass") {
None => {
error!("Failed to read passphrase.");
return 1;
// Recover a seed from a recovery phrase
if let ("recover", Some(r)) = wallet_args.subcommand() {
if !r.is_present("recovery_phrase") {
// only needed to display phrase
let passphrase = prompt_password(wallet_args);
let seed = match WalletSeed::from_file(&wallet_config, &passphrase) {
Ok(s) => s,
Err(e) => {
println!("Can't open wallet seed file (check password): {}", e);
std::process::exit(0);
}
};
let _ = seed.show_recovery_phrase();
std::process::exit(0);
}
Some(p) => p,
};
let word_list = match r.value_of("recovery_phrase") {
Some(w) => w,
None => {
println!("Recovery word phrase must be provided (in quotes)");
std::process::exit(0);
}
};
// check word list is okay before asking for password
if WalletSeed::from_mnemonic(word_list).is_err() {
println!("Recovery word phrase is invalid");
std::process::exit(0);
}
println!("Please provide a new password for the recovered wallet");
let passphrase = prompt_password_confirm();
let res = WalletSeed::recover_from_phrase(&wallet_config, word_list, &passphrase);
if let Err(e) = res {
thread::sleep(Duration::from_millis(200));
error!("Error recovering seed with list '{}' - {}", word_list, e);
return 0;
}
thread::sleep(Duration::from_millis(200));
return 0;
}
let account = match wallet_args.value_of("account") {
None => {
@@ -111,6 +179,9 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
Some(p) => p,
};
// all further commands always need a password
let passphrase = prompt_password(wallet_args);
// Handle listener startup commands
{
let api_secret = get_first_line(wallet_config.api_secret_path.clone());
@@ -146,10 +217,14 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
.listen(
params,
wallet_config.clone(),
passphrase,
&passphrase,
account,
node_api_secret.clone(),
).unwrap_or_else(|e| {
if e.kind() == ErrorKind::WalletSeedDecryption {
println!("Error decrypting wallet seed (check provided password)");
std::process::exit(0);
}
panic!(
"Error creating wallet listener: {:?} Config: {:?}",
e, wallet_config
@@ -159,10 +234,19 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
("owner_api", Some(_api_args)) => {
let wallet = instantiate_wallet(
wallet_config.clone(),
passphrase,
&passphrase,
account,
node_api_secret.clone(),
);
).unwrap_or_else(|e| {
if e.kind() == grin_wallet::ErrorKind::Encryption {
println!("Error decrypting wallet seed (check provided password)");
std::process::exit(0);
}
panic!(
"Error creating wallet listener: {:?} Config: {:?}",
e, wallet_config
);
});
// TLS is disabled because we bind to localhost
controller::owner_listener(wallet.clone(), "127.0.0.1:13420", api_secret, None)
.unwrap_or_else(|e| {
@@ -175,10 +259,19 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
("web", Some(_api_args)) => {
let wallet = instantiate_wallet(
wallet_config.clone(),
passphrase,
&passphrase,
account,
node_api_secret.clone(),
);
).unwrap_or_else(|e| {
if e.kind() == grin_wallet::ErrorKind::Encryption {
println!("Error decrypting wallet seed (check provided password)");
std::process::exit(0);
}
panic!(
"Error creating wallet listener: {:?} Config: {:?}",
e, wallet_config
);
});
// start owner listener and run static file server
start_webwallet_server();
controller::owner_listener(wallet.clone(), "127.0.0.1:13420", api_secret, tls_conf)
@@ -195,10 +288,19 @@ pub fn wallet_command(wallet_args: &ArgMatches, config: GlobalWalletConfig) -> i
let wallet = instantiate_wallet(
wallet_config.clone(),
passphrase,
&passphrase,
account,
node_api_secret.clone(),
);
).unwrap_or_else(|e| {
if e.kind() == grin_wallet::ErrorKind::Encryption {
println!("Error decrypting wallet seed (check provided password)");
std::process::exit(0);
}
panic!(
"Error instantiating wallet: {:?} Config: {:?}",
e, wallet_config
);
});
let res = controller::owner_single_use(wallet.clone(), |api| {
match wallet_args.subcommand() {
+19 -6
View File
@@ -25,6 +25,7 @@ extern crate serde;
extern crate serde_json;
#[macro_use]
extern crate log;
extern crate rpassword;
extern crate term;
extern crate grin_api as api;
@@ -153,9 +154,8 @@ fn real_main() -> i32 {
.arg(Arg::with_name("pass")
.short("p")
.long("pass")
.help("Wallet passphrase used to generate the private key seed")
.takes_value(true)
.default_value(""))
.help("Wallet passphrase used to encrypt wallet seed")
.takes_value(true))
.arg(Arg::with_name("account")
.short("a")
.long("account")
@@ -343,10 +343,23 @@ fn real_main() -> i32 {
.short("h")
.long("here")
.help("Create wallet files in the current directory instead of the default ~/.grin directory")
.takes_value(false))
.arg(Arg::with_name("short_wordlist")
.help("Generate a 12 word recovery phrase/seed instead of default 24.")
.short("s")
.long("short_wordlist")
.takes_value(false)))
.subcommand(SubCommand::with_name("recover")
.about("recover (create a new wallet.seed file) from a recovery phrase")
.arg(Arg::with_name("recovery_phrase")
.help("12 or 24 word recovery phrase (encased in quotes).")
.short("p")
.long("recovery_phrase")
.takes_value(true)))
.subcommand(SubCommand::with_name("restore")
.about("Attempt to restore wallet contents from the chain using seed and password. \
.about("Attempt to restore wallet contents from the chain using seed. \
NOTE: Backup wallet.* and run `wallet listen` before running restore.")))
.get_matches();
@@ -381,9 +394,9 @@ fn real_main() -> i32 {
panic!("Error loading wallet configuration: {}", e);
});
if !cmd::seed_exists(w.members.as_ref().unwrap().wallet.clone()) {
if let ("init", Some(_)) = wallet_args.subcommand() {
if "init" == wallet_args.subcommand().0 || "recover" == wallet_args.subcommand().0 {
} else {
println!("Wallet seed file doesn't exist. Run `grin wallet -p [password] init` first");
println!("Wallet seed file doesn't exist. Run `grin wallet init` first");
exit(1);
}
}
+1
View File
@@ -26,6 +26,7 @@ term = "0.5"
tokio = "0.1.7"
tokio-core = "0.1"
tokio-retry = "0.1"
ring = "0.13"
uuid = { version = "0.6", features = ["serde", "v4"] }
url = "1.7.0"
chrono = { version = "0.4.4", features = ["serde"] }
+2 -1
View File
@@ -71,7 +71,8 @@ impl WalletCommAdapter for HTTPWalletCommAdapter {
node_api_secret: Option<String>,
) -> Result<(), Error> {
let wallet =
instantiate_wallet(config.clone(), passphrase, account, node_api_secret.clone());
instantiate_wallet(config.clone(), passphrase, account, node_api_secret.clone())
.context(ErrorKind::WalletSeedDecryption)?;
let listen_addr = params.get("api_listen_addr").unwrap();
let tls_conf = match params.get("certificate") {
Some(s) => Some(api::TLSConfig::new(
+8
View File
@@ -87,6 +87,14 @@ pub enum ErrorKind {
#[fail(display = "Wallet seed doesn't exist error")]
WalletSeedDoesntExist,
/// Enc/Decryption Error
#[fail(display = "Enc/Decryption error")]
Encryption,
/// BIP 39 word list
#[fail(display = "BIP39 Mnemonic (word list) Error")]
Mnemonic,
/// Other
#[fail(display = "Generic error: {}", _0)]
GenericError(String),
+8 -15
View File
@@ -35,6 +35,7 @@ extern crate failure;
extern crate failure_derive;
extern crate futures;
extern crate hyper;
extern crate ring;
extern crate tokio;
extern crate tokio_core;
extern crate tokio_retry;
@@ -62,7 +63,7 @@ pub use error::{Error, ErrorKind};
pub use libwallet::types::{BlockFees, CbData, NodeClient, WalletBackend, WalletInfo, WalletInst};
pub use lmdb_wallet::{wallet_db_exists, LMDBBackend};
pub use node_clients::{create_coinbase, HTTPNodeClient};
pub use types::{WalletConfig, WalletSeed, SEED_FILE};
pub use types::{EncryptedWalletSeed, WalletConfig, WalletSeed, SEED_FILE};
use std::sync::Arc;
use util::Mutex;
@@ -73,20 +74,12 @@ pub fn instantiate_wallet(
passphrase: &str,
account: &str,
node_api_secret: Option<String>,
) -> Arc<Mutex<WalletInst<HTTPNodeClient, keychain::ExtKeychain>>> {
) -> Result<Arc<Mutex<WalletInst<HTTPNodeClient, keychain::ExtKeychain>>>, Error> {
// First test decryption, so we can abort early if we have the wrong password
let _ = WalletSeed::from_file(&wallet_config, passphrase)?;
let client_n = HTTPNodeClient::new(&wallet_config.check_node_api_http_addr, node_api_secret);
let mut db_wallet = LMDBBackend::new(wallet_config.clone(), passphrase, client_n)
.unwrap_or_else(|e| {
panic!(
"Error creating DB wallet: {} Config: {:?}",
e, wallet_config
);
});
db_wallet
.set_parent_key_id_by_name(account)
.unwrap_or_else(|e| {
panic!("Error starting wallet: {}", e);
});
let mut db_wallet = LMDBBackend::new(wallet_config.clone(), passphrase, client_n)?;
db_wallet.set_parent_key_id_by_name(account)?;
info!("Using LMDB Backend for wallet");
Arc::new(Mutex::new(db_wallet))
Ok(Arc::new(Mutex::new(db_wallet)))
}
+4
View File
@@ -148,6 +148,10 @@ pub enum ErrorKind {
#[fail(display = "Wallet seed doesn't exist error")]
WalletSeedDoesntExist,
/// Wallet seed doesn't exist
#[fail(display = "Wallet seed decryption error")]
WalletSeedDecryption,
/// Transaction doesn't exist
#[fail(display = "Transaction {} doesn't exist", _0)]
TransactionDoesntExist(String),
+2 -4
View File
@@ -121,12 +121,10 @@ where
{
/// Initialise with whatever stored credentials we have
fn open_with_credentials(&mut self) -> Result<(), Error> {
let wallet_seed = WalletSeed::from_file(&self.config)
let wallet_seed = WalletSeed::from_file(&self.config, &self.passphrase)
.context(ErrorKind::CallbackImpl("Error opening wallet"))?;
let keychain = wallet_seed.derive_keychain(&self.passphrase);
let keychain = wallet_seed.derive_keychain();
self.keychain = Some(keychain.context(ErrorKind::CallbackImpl("Error deriving keychain"))?);
// Just blow up password for now after it's been used
self.passphrase = String::from("");
Ok(())
}
+181 -20
View File
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use std::cmp::min;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
@@ -20,11 +19,15 @@ use std::path::MAIN_SEPARATOR;
use blake2;
use rand::{thread_rng, Rng};
use serde_json;
use ring::aead;
use ring::{digest, pbkdf2};
use core::global::ChainTypes;
use error::{Error, ErrorKind};
use failure::ResultExt;
use keychain::Keychain;
use keychain::{mnemonic, Keychain};
use util;
pub const SEED_FILE: &'static str = "wallet.seed";
@@ -79,16 +82,20 @@ impl WalletConfig {
}
}
#[derive(Clone, PartialEq)]
pub struct WalletSeed([u8; 32]);
#[derive(Clone, Debug, PartialEq)]
pub struct WalletSeed(Vec<u8>);
impl WalletSeed {
pub fn from_bytes(bytes: &[u8]) -> WalletSeed {
let mut seed = [0; 32];
for i in 0..min(32, bytes.len()) {
seed[i] = bytes[i];
WalletSeed(bytes.to_vec())
}
pub fn from_mnemonic(word_list: &str) -> Result<WalletSeed, Error> {
let res = mnemonic::to_entropy(word_list);
match res {
Ok(s) => Ok(WalletSeed::from_bytes(&s)),
Err(_) => Err(ErrorKind::Mnemonic.into()),
}
WalletSeed(seed)
}
pub fn from_hex(hex: &str) -> Result<WalletSeed, Error> {
@@ -101,18 +108,72 @@ impl WalletSeed {
util::to_hex(self.0.to_vec())
}
pub fn derive_keychain<K: Keychain>(&self, password: &str) -> Result<K, Error> {
let seed = blake2::blake2b::blake2b(64, &password.as_bytes(), &self.0);
let result = K::from_seed(seed.as_bytes())?;
pub fn to_mnemonic(&self) -> Result<String, Error> {
let result = mnemonic::from_entropy(&self.0);
match result {
Ok(r) => Ok(r),
Err(_) => Err(ErrorKind::Mnemonic.into()),
}
}
pub fn derive_keychain_old(old_wallet_seed: [u8; 32], password: &str) -> Vec<u8> {
let seed = blake2::blake2b::blake2b(64, password.as_bytes(), &old_wallet_seed);
seed.as_bytes().to_vec()
}
pub fn derive_keychain<K: Keychain>(&self) -> Result<K, Error> {
let result = K::from_seed(&self.0)?;
Ok(result)
}
pub fn init_new() -> WalletSeed {
let seed: [u8; 32] = thread_rng().gen();
pub fn init_new(seed_length: usize) -> WalletSeed {
let mut seed: Vec<u8> = vec![];
let mut rng = thread_rng();
for _ in 0..seed_length {
seed.push(rng.gen());
}
WalletSeed(seed)
}
pub fn init_file(wallet_config: &WalletConfig) -> Result<WalletSeed, Error> {
pub fn recover_from_phrase(
wallet_config: &WalletConfig,
word_list: &str,
password: &str,
) -> Result<(), Error> {
let seed_file_path = &format!(
"{}{}{}",
wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE,
);
if Path::new(seed_file_path).exists() {
error!(
"wallet seed file {} exists. \
Please backup and delete this file before attempting recovery.",
seed_file_path
);
return Err(ErrorKind::WalletSeedExists)?;
}
let seed = WalletSeed::from_mnemonic(word_list)?;
let enc_seed = EncryptedWalletSeed::from_seed(&seed, password)?;
let enc_seed_json = serde_json::to_string_pretty(&enc_seed).context(ErrorKind::Format)?;
let mut file = File::create(seed_file_path).context(ErrorKind::IO)?;
file.write_all(&enc_seed_json.as_bytes())
.context(ErrorKind::IO)?;
warn!("Seed created from word list");
Ok(())
}
pub fn show_recovery_phrase(&self) -> Result<(), Error> {
println!("Your recovery phrase is:");
println!("{}", self.to_mnemonic()?);
println!("Please back-up these words in a non-digital format.");
Ok(())
}
pub fn init_file(
wallet_config: &WalletConfig,
seed_length: usize,
password: &str,
) -> Result<WalletSeed, Error> {
// create directory if it doesn't exist
fs::create_dir_all(&wallet_config.data_file_dir).context(ErrorKind::IO)?;
@@ -121,20 +182,24 @@ impl WalletSeed {
wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE,
);
debug!("Generating wallet seed file at: {}", seed_file_path);
warn!("Generating wallet seed file at: {}", seed_file_path);
if Path::new(seed_file_path).exists() {
Err(ErrorKind::WalletSeedExists)?
} else {
let seed = WalletSeed::init_new();
let seed = WalletSeed::init_new(seed_length);
let enc_seed = EncryptedWalletSeed::from_seed(&seed, password)?;
let enc_seed_json =
serde_json::to_string_pretty(&enc_seed).context(ErrorKind::Format)?;
let mut file = File::create(seed_file_path).context(ErrorKind::IO)?;
file.write_all(&seed.to_hex().as_bytes())
file.write_all(&enc_seed_json.as_bytes())
.context(ErrorKind::IO)?;
seed.show_recovery_phrase()?;
Ok(seed)
}
}
pub fn from_file(wallet_config: &WalletConfig) -> Result<WalletSeed, Error> {
pub fn from_file(wallet_config: &WalletConfig, password: &str) -> Result<WalletSeed, Error> {
// create directory if it doesn't exist
fs::create_dir_all(&wallet_config.data_file_dir).context(ErrorKind::IO)?;
@@ -143,13 +208,44 @@ impl WalletSeed {
wallet_config.data_file_dir, MAIN_SEPARATOR, SEED_FILE,
);
debug!("Using wallet seed file at: {}", seed_file_path,);
debug!("Using wallet seed file at: {}", seed_file_path);
if Path::new(seed_file_path).exists() {
let mut file = File::open(seed_file_path).context(ErrorKind::IO)?;
let mut buffer = String::new();
file.read_to_string(&mut buffer).context(ErrorKind::IO)?;
let wallet_seed = WalletSeed::from_hex(&buffer.trim())?;
let enc_seed: EncryptedWalletSeed =
match serde_json::from_str(&buffer).context(ErrorKind::Format) {
Ok(s) => s,
Err(_) => {
println!("Attempting to convert old wallet seed file to new format");
// TODO: remove for mainnet
// try to convert from old format
let mut bak_file = File::create(format!("{}.bak", seed_file_path))
.context(ErrorKind::IO)?;
let mut file = File::create(seed_file_path).context(ErrorKind::IO)?;
let old_wallet_seed = WalletSeed::from_hex(&buffer.trim())?;
bak_file
.write_all(&old_wallet_seed.to_hex().as_bytes())
.context(ErrorKind::IO)?;
let mut c_wallet_seed = [0u8; 32];
c_wallet_seed.copy_from_slice(&old_wallet_seed.0[0..32]);
let converted_wallet_seed =
WalletSeed::derive_keychain_old(c_wallet_seed, password);
let enc_seed = EncryptedWalletSeed::from_seed(
&WalletSeed::from_bytes(&converted_wallet_seed),
password,
)?;
let enc_seed_json =
serde_json::to_string_pretty(&enc_seed).context(ErrorKind::Format)?;
file.write_all(&enc_seed_json.as_bytes())
.context(ErrorKind::IO)?;
println!("Seed file conversion done");
println!("Consider moving funds to a newly-created wallet to support recovery phrases");
enc_seed
}
};
let wallet_seed = enc_seed.decrypt(password)?;
Ok(wallet_seed)
} else {
error!(
@@ -161,3 +257,68 @@ impl WalletSeed {
}
}
}
/// Encrypted wallet seed, for storing on disk and decrypting
/// with provided password
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
pub struct EncryptedWalletSeed {
encrypted_seed: String,
/// Salt, not so useful in single case but include anyhow for situations
/// where someone wants to store many of these
pub salt: String,
/// Nonce
pub nonce: String,
}
impl EncryptedWalletSeed {
/// Create a new encrypted seed from the given seed + password
pub fn from_seed(seed: &WalletSeed, password: &str) -> Result<EncryptedWalletSeed, Error> {
let salt: [u8; 8] = thread_rng().gen();
let nonce: [u8; 12] = thread_rng().gen();
let password = password.as_bytes();
let mut key = [0; 32];
pbkdf2::derive(&digest::SHA512, 100, &salt, password, &mut key);
let content = seed.0.to_vec();
let mut enc_bytes = content.clone();
let suffix_len = aead::CHACHA20_POLY1305.tag_len();
for _ in 0..suffix_len {
enc_bytes.push(0);
}
let sealing_key =
aead::SealingKey::new(&aead::CHACHA20_POLY1305, &key).context(ErrorKind::Encryption)?;
aead::seal_in_place(&sealing_key, &nonce, &[], &mut enc_bytes, suffix_len)
.context(ErrorKind::Encryption)?;
Ok(EncryptedWalletSeed {
encrypted_seed: util::to_hex(enc_bytes.to_vec()),
salt: util::to_hex(salt.to_vec()),
nonce: util::to_hex(nonce.to_vec()),
})
}
/// Decrypt seed
pub fn decrypt(&self, password: &str) -> Result<WalletSeed, Error> {
let mut encrypted_seed = match util::from_hex(self.encrypted_seed.clone()) {
Ok(s) => s,
Err(_) => return Err(ErrorKind::Encryption)?,
};
let salt = match util::from_hex(self.salt.clone()) {
Ok(s) => s,
Err(_) => return Err(ErrorKind::Encryption)?,
};
let nonce = match util::from_hex(self.nonce.clone()) {
Ok(s) => s,
Err(_) => return Err(ErrorKind::Encryption)?,
};
let password = password.as_bytes();
let mut key = [0; 32];
pbkdf2::derive(&digest::SHA512, 100, &salt, password, &mut key);
let opening_key =
aead::OpeningKey::new(&aead::CHACHA20_POLY1305, &key).context(ErrorKind::Encryption)?;
let decrypted_data = aead::open_in_place(&opening_key, &nonce, &[], 0, &mut encrypted_seed)
.context(ErrorKind::Encryption)?;
Ok(WalletSeed::from_bytes(&decrypted_data))
}
}
+1 -1
View File
@@ -163,7 +163,7 @@ where
{
let mut wallet_config = WalletConfig::default();
wallet_config.data_file_dir = String::from(dir);
let _ = wallet::WalletSeed::init_file(&wallet_config);
let _ = wallet::WalletSeed::init_file(&wallet_config, 32, "");
let mut wallet = LMDBBackend::new(wallet_config.clone(), "", n_client)
.unwrap_or_else(|e| panic!("Error creating wallet: {:?} Config: {:?}", e, wallet_config));
wallet.open_with_credentials().unwrap_or_else(|e| {
+1 -1
View File
@@ -114,7 +114,7 @@ fn file_exchange_test_impl(test_dir: &str) -> Result<(), libwallet::Error> {
//"mining",
//"listener",
)?;
/// output tx file
// output tx file
let file_adapter = FileWalletCommAdapter::new();
file_adapter.send_tx_async(&send_file, &mut slate)?;
api.tx_lock_outputs(&slate, lock_fn)?;
+20
View File
@@ -26,6 +26,7 @@ use util::secp;
use util::secp::key::{PublicKey, SecretKey};
use wallet::libtx::{aggsig, proof};
use wallet::libwallet::types::Context;
use wallet::{EncryptedWalletSeed, WalletSeed};
use rand::thread_rng;
@@ -481,3 +482,22 @@ fn test_rewind_range_proof() {
assert_eq!(proof_info.success, false);
assert_eq!(proof_info.value, 0);
}
#[test]
fn wallet_seed_encrypt() {
let password = "passwoid";
let wallet_seed = WalletSeed::init_new(32);
let mut enc_wallet_seed = EncryptedWalletSeed::from_seed(&wallet_seed, password).unwrap();
println!("EWS: {:?}", enc_wallet_seed);
let decrypted_wallet_seed = enc_wallet_seed.decrypt(password).unwrap();
assert_eq!(wallet_seed, decrypted_wallet_seed);
// Wrong password
let decrypted_wallet_seed = enc_wallet_seed.decrypt("");
assert!(decrypted_wallet_seed.is_err());
// Wrong nonce
enc_wallet_seed.nonce = "wrongnonce".to_owned();
let decrypted_wallet_seed = enc_wallet_seed.decrypt(password);
assert!(decrypted_wallet_seed.is_err());
}