diff --git a/CHANGELOG.md b/CHANGELOG.md index 2555adc79a..18465e14e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ## DFX +### feat: generate secp256k1 keys by default + +When creating a new identity with `dfx identity new`, whereas previously it would have generated an Ed25519 key, it now generates a secp256k1 key. This is to enable users to write down a BIP39-style seed phrase, to recover their key in case of emergency, which will be printed when the key is generated and can be used with a new `--seed-phrase` flag in `dfx identity import`. `dfx identity import` is however still capable of importing an Ed25519 key. + ### chore: update Candid UI canister with commit 528a4b04807904899f67b919a88597656e0cd6fa * Allow passing did files larger than 2KB. diff --git a/Cargo.lock b/Cargo.lock index 20b95278e8..fb6ba49ecf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -299,6 +299,24 @@ dependencies = [ "syn", ] +[[package]] +name = "bip32" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b30ed1d6f8437a487a266c8293aeb95b61a23261273e3e02912cdb8b68bf798b" +dependencies = [ + "bs58", + "hmac", + "k256", + "once_cell", + "pbkdf2", + "rand_core", + "ripemd", + "sha2 0.10.2", + "subtle", + "zeroize", +] + [[package]] name = "bit-set" version = "0.5.3" @@ -347,6 +365,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +dependencies = [ + "sha2 0.9.9", +] + [[package]] name = "bstr" version = "0.2.17" @@ -737,6 +764,7 @@ dependencies = [ "argon2", "atty", "base64", + "bip32", "byte-unit", "candid", "clap", @@ -763,6 +791,7 @@ dependencies = [ "ic-wasm", "indicatif", "itertools 0.10.3", + "k256", "lazy_static", "mime", "mime_guess", @@ -795,6 +824,7 @@ dependencies = [ "term", "thiserror", "time", + "tiny-bip39", "tokio", "url", "walkdir", @@ -1790,8 +1820,15 @@ dependencies = [ "ecdsa", "elliptic-curve", "sha2 0.10.2", + "sha3", ] +[[package]] +name = "keccak" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9b7d56ba4a8344d6be9729995e6b06f928af29998cdf79fe390cbf6b1fee838" + [[package]] name = "lalrpop" version = "0.19.8" @@ -2288,6 +2325,15 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9423e2b32f7a043629287a536f21951e8c6a82482d0acb1eeebfc90bc2225b22" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.3", +] + [[package]] name = "pem" version = "1.1.0" @@ -2750,6 +2796,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "ripemd" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1facec54cb5e0dc08553501fa740091086d0259ad0067e0d4103448e4cb22ed3" +dependencies = [ + "digest 0.10.3", +] + [[package]] name = "rle-decode-fast" version = "1.0.3" @@ -2767,6 +2822,12 @@ dependencies = [ "serde", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustls" version = "0.20.6" @@ -3058,6 +3119,16 @@ dependencies = [ "digest 0.10.3", ] +[[package]] +name = "sha3" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a31480366ec990f395a61b7c08122d99bd40544fdb5abcfc1b06bb29994312c" +dependencies = [ + "digest 0.10.3", + "keccak", +] + [[package]] name = "shell-words" version = "1.1.0" @@ -3240,6 +3311,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "sysinfo" version = "0.24.7" @@ -3370,6 +3453,25 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +[[package]] +name = "tiny-bip39" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62cc94d358b5a1e84a5cb9109f559aa3c4d634d2b1b4de3d0fa4adc7c78e2861" +dependencies = [ + "anyhow", + "hmac", + "once_cell", + "pbkdf2", + "rand", + "rustc-hash", + "sha2 0.10.2", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -3917,3 +4019,18 @@ name = "zeroize" version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f8f187641dad4f680d25c4bfc4225b418165984179f26ca76ec4fb6441d3a17" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/e2e/assets/ed25519/identity.pem b/e2e/assets/ed25519/identity.pem new file mode 100644 index 0000000000..dc39b5e76b --- /dev/null +++ b/e2e/assets/ed25519/identity.pem @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MFMCAQEwBQYDK2VwBCIEIODVjQrIvbt3PO3FPDKCZs2FarAbsRrLuiQZ+NBslV9U +oSMDIQDVkl2stdaeyBDvfb0t4qy9vhsv6xLl1v7p7i0NO1+9pw== +-----END PRIVATE KEY----- diff --git a/e2e/tests-dfx/ed25519.bash b/e2e/tests-dfx/ed25519.bash new file mode 100644 index 0000000000..792ffc0029 --- /dev/null +++ b/e2e/tests-dfx/ed25519.bash @@ -0,0 +1,29 @@ +#!/usr/bin/env bats + +load ../utils/_ + +setup() { + standard_setup +} + +teardown() { + dfx_stop + + standard_teardown +} + +@test "can call a canister using an ed25519 identity" { + install_asset ed25519 + assert_command dfx identity import --disable-encryption ed25519 identity.pem + dfx_new # This installs replica and other binaries + dfx identity use ed25519 + install_asset whoami + dfx_start + dfx canister create whoami + dfx build + dfx canister install whoami + assert_command dfx canister call whoami whoami + assert_eq '(principal "2nor3-keehi-duuup-d7jcn-onggn-3atzm-gejtl-5tlzn-k4g6c-nnbf7-7qe")' + assert_command dfx identity get-principal + assert_eq "2nor3-keehi-duuup-d7jcn-onggn-3atzm-gejtl-5tlzn-k4g6c-nnbf7-7qe" +} diff --git a/e2e/tests-dfx/identity_command.bash b/e2e/tests-dfx/identity_command.bash index 410b465857..ad52d02ca5 100644 --- a/e2e/tests-dfx/identity_command.bash +++ b/e2e/tests-dfx/identity_command.bash @@ -76,7 +76,7 @@ frank' assert_command dfx identity new --disable-encryption alice assert_match 'Created identity: "alice".' "$stderr" assert_command head "$DFX_CONFIG_ROOT/.config/dfx/identity/alice/identity.pem" - assert_match "BEGIN PRIVATE KEY" + assert_match "BEGIN EC PRIVATE KEY" # does not change the default identity assert_command dfx identity whoami @@ -129,7 +129,7 @@ frank' assert_command dfx identity new --disable-encryption alice assert_command head "$DFX_CONFIG_ROOT/.config/dfx/identity/alice/identity.pem" - assert_match "BEGIN PRIVATE KEY" + assert_match "BEGIN EC PRIVATE KEY" assert_command dfx identity list assert_match \ 'alice @@ -168,7 +168,7 @@ default' assert_command_fail dfx identity remove alice assert_command head "$DFX_CONFIG_ROOT/.config/dfx/identity/alice/identity.pem" - assert_match "BEGIN PRIVATE KEY" + assert_match "BEGIN EC PRIVATE KEY" assert_command dfx identity list assert_match \ 'alice @@ -211,7 +211,7 @@ default' anonymous default' assert_command head "$DFX_CONFIG_ROOT/.config/dfx/identity/alice/identity.pem" - assert_match "BEGIN PRIVATE KEY" + assert_match "BEGIN EC PRIVATE KEY" x=$(cat "$DFX_CONFIG_ROOT/.config/dfx/identity/alice/identity.pem") local key="$x" @@ -225,7 +225,7 @@ bob default' assert_command cat "$DFX_CONFIG_ROOT/.config/dfx/identity/bob/identity.pem" assert_eq "$key" "$(cat "$DFX_CONFIG_ROOT/.config/dfx/identity/bob/identity.pem")" - assert_match "BEGIN PRIVATE KEY" + assert_match "BEGIN EC PRIVATE KEY" assert_command_fail cat "$DFX_CONFIG_ROOT/.config/dfx/identity/alice/identity.pem" } @@ -236,7 +236,7 @@ default' assert_command dfx identity list assert_match 'bob' assert_command head "$DFX_CONFIG_ROOT/.config/dfx/identity/bob/identity.pem" - assert_match "BEGIN PRIVATE KEY" + assert_match "BEGIN EC PRIVATE KEY" assert_command dfx identity whoami assert_eq 'bob' @@ -262,7 +262,7 @@ default' assert_eq 'charlie' assert_command head "$DFX_CONFIG_ROOT/.config/dfx/identity/charlie/identity.pem" - assert_match "BEGIN PRIVATE KEY" + assert_match "BEGIN EC PRIVATE KEY" assert_command_fail cat "$DFX_CONFIG_ROOT/.config/dfx/identity/alice/identity.pem" } @@ -427,7 +427,7 @@ default' echo -n 1 >> bob.pem tail -n 3 alice.pem > bob.pem assert_command_fail dfx identity import --disable-encryption bob bob.pem - assert_match 'Invalid Ed25519 private key in PEM file' "$stderr" + assert_match 'Failed to validate PEM content' "$stderr" } @test "identity: can import an EC key without an EC PARAMETERS section (as quill generate makes)" { @@ -452,3 +452,21 @@ XXX assert_file_exists export.pem assert_command dfx identity import --disable-encryption bob export.pem } + +@test "identity: can import a seed phrase" { + reg="seed phrase for identity 'alice': ([a-z ]+)" + assert_command dfx identity new --disable-encryption alice + [[ $stderr =~ $reg ]] + echo "${BASH_REMATCH[1]}" >seed.txt + principal=$(dfx identity get-principal --identity alice) + assert_command dfx identity import alice2 --seed-file seed.txt --disable-encryption + assert_command dfx identity get-principal --identity alice2 + assert_eq "$principal" +} + +@test "identity: consistently imports a known seed phrase" { + echo "hollow damage this yard journey anchor tool fat action school cash ridge oval beef tribe magnet apology cabbage leisure group sign around object exact">seed.txt + assert_command dfx identity import alice --seed-file seed.txt --disable-encryption + assert_command dfx identity get-principal --identity alice + assert_eq "zs7ty-uv4vo-rvgkk-srfjo-hjaxr-w55wx-ybo5x-qx7k3-noknf-wzwe5-pqe" +} diff --git a/e2e/tests-dfx/secp256k1.bash b/e2e/tests-dfx/secp256k1.bash deleted file mode 100644 index 5e72ea1fcf..0000000000 --- a/e2e/tests-dfx/secp256k1.bash +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env bats - -load ../utils/_ - -setup() { - standard_setup -} - -teardown() { - dfx_stop - - standard_teardown -} - -@test "can call a canister using a secp256k1 identity" { - openssl ecparam -name secp256k1 -genkey -out identity.pem - assert_command dfx identity import --disable-encryption secp256k1 identity.pem - dfx_new # This installs replica and other binaries - dfx identity use secp256k1 - install_asset whoami - dfx_start - dfx canister create whoami - dfx build - dfx canister install whoami - assert_command dfx canister call whoami whoami - assert_match "$(dfx identity get-principal)" -} diff --git a/src/dfx/Cargo.toml b/src/dfx/Cargo.toml index bc14972c3c..0be009b451 100644 --- a/src/dfx/Cargo.toml +++ b/src/dfx/Cargo.toml @@ -23,6 +23,7 @@ anyhow = "1.0.56" argon2 = "0.4.0" atty = "0.2.13" base64 = "0.13.0" +bip32 = "0.4.0" byte-unit = { version = "4.0.14", features = ["serde"] } candid = { version = "0.7.15", features = [ "random" ] } clap = { version = "3.1.6", features = [ "derive" ] } @@ -45,6 +46,7 @@ ic-asset = { version = "0.20.0", path = "../canisters/frontend/ic-asset" } ic-wasm = { version = "0.1.3", default-features = false, features = ["optimize"]} indicatif = "0.16.0" itertools = "0.10.3" +k256 = { version = "0.11.4", features = ["pem"] } lazy_static = "1.4.0" mime = "0.3.16" mime_guess = "2.0.4" @@ -75,6 +77,7 @@ tempfile = "3.3.0" term = "0.7.0" thiserror = "1.0.20" time = { version = "0.3.9", features = ["serde", "macros", "serde-human-readable"] } +tiny-bip39 = "1.0.0" tokio = { version = "1.17.0", features = [ "fs" ] } url = "2.1.0" walkdir = "2.2.9" diff --git a/src/dfx/src/commands/identity/import.rs b/src/dfx/src/commands/identity/import.rs index a6666ba97c..3784ac4c0b 100644 --- a/src/dfx/src/commands/identity/import.rs +++ b/src/dfx/src/commands/identity/import.rs @@ -2,8 +2,10 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; use crate::lib::identity::identity_manager::{IdentityCreationParameters, IdentityManager}; +use anyhow::Context; use clap::Parser; use slog::info; +use std::fs; use std::path::PathBuf; /// Creates a new identity from a PEM file. @@ -13,7 +15,11 @@ pub struct ImportOpts { new_identity: String, /// The PEM file to import. - pem_file: PathBuf, + pem_file: Option, + + /// The path to a file with your seed phrase. + #[clap(long, conflicts_with("pem-file"), required_unless_present("pem-file"))] + seed_file: Option, /// DANGEROUS: By default, PEM files are encrypted with a password when writing them to disk. /// If you want the convenience of not having to type your password (but at the risk of having your PEM file compromised), you can disable the encryption. @@ -29,9 +35,18 @@ pub struct ImportOpts { pub fn exec(env: &dyn Environment, opts: ImportOpts) -> DfxResult { let log = env.get_logger(); let name = opts.new_identity.as_str(); - let params = IdentityCreationParameters::PemFile { - src_pem_file: opts.pem_file, - disable_encryption: opts.disable_encryption, + let params = if let Some(src_pem_file) = opts.pem_file { + IdentityCreationParameters::PemFile { + src_pem_file, + disable_encryption: opts.disable_encryption, + } + } else { + let mnemonic = + fs::read_to_string(opts.seed_file.unwrap()).context("Failed to read seed file")?; + IdentityCreationParameters::SeedPhrase { + mnemonic, + disable_encryption: opts.disable_encryption, + } }; IdentityManager::new(env)?.create_new_identity(name, params, opts.force)?; info!(log, r#"Imported identity: "{}"."#, name); diff --git a/src/dfx/src/lib/error/identity.rs b/src/dfx/src/lib/error/identity.rs index 64d8862edc..76d31145e2 100644 --- a/src/dfx/src/lib/error/identity.rs +++ b/src/dfx/src/lib/error/identity.rs @@ -1,6 +1,4 @@ use crate::lib::error::DfxError; - -use ring::error::Unspecified; use std::boxed::Box; use std::path::PathBuf; use thiserror::Error; @@ -16,9 +14,6 @@ pub enum IdentityError { #[error("Identity {0} does not exist at '{1}'.")] IdentityDoesNotExist(String, PathBuf), - #[error("Cannot generate key pair.")] - CannotGenerateKeyPair(Unspecified), - #[error("Cannot create identity directory at '{0}': {1:#}")] CannotCreateIdentityDirectory(PathBuf, Box), diff --git a/src/dfx/src/lib/identity/identity_manager.rs b/src/dfx/src/lib/identity/identity_manager.rs index fc2e374f0d..0faf579555 100644 --- a/src/dfx/src/lib/identity/identity_manager.rs +++ b/src/dfx/src/lib/identity/identity_manager.rs @@ -7,16 +7,20 @@ use crate::lib::identity::{ }; use anyhow::{anyhow, bail, Context}; +use bip32::XPrv; +use bip39::{Language, Mnemonic, MnemonicType, Seed}; use candid::Principal; use fn_error_context::context; -use pem::{encode, Pem}; -use ring::{rand, rand::SecureRandom, signature}; +use k256::pkcs8::LineEnding; +use k256::SecretKey; +use ring::{rand, rand::SecureRandom}; use serde::{Deserialize, Serialize}; use slog::Logger; use std::boxed::Box; use std::fs; use std::path::{Path, PathBuf}; +use super::identity_utils::validate_pem_file; use super::WALLET_CONFIG_FILENAME; const DEFAULT_IDENTITY_NAME: &str = "default"; @@ -86,6 +90,10 @@ pub enum IdentityCreationParameters { src_pem_file: PathBuf, disable_encryption: bool, }, + SeedPhrase { + mnemonic: String, + disable_encryption: bool, + }, Hardware { hsm: HardwareIdentityConfiguration, }, @@ -230,6 +238,7 @@ impl IdentityManager { let config = self.get_identity_config_or_default(name)?; let pem_path = self.get_identity_pem_path(name, &config); let pem = pem_encryption::load_pem_file(&pem_path, Some(&config))?; + validate_pem_file(&pem)?; String::from_utf8(pem).map_err(|e| anyhow!("Could not translate pem file to text: {}", e)) } @@ -468,8 +477,9 @@ To create a more secure identity, create and use an identity that is protected b " - generating new key at {}", identity_pem_path.display() ); - let key = generate_key()?; + let (key, mnemonic) = generate_key()?; pem_encryption::write_pem_file(&identity_pem_path, None, key.as_slice())?; + eprintln!("Your seed phrase: {}\nThis can be used to reconstruct your key in case of emergency, so write it down in a safe place.", mnemonic.phrase()); } } else { slog::info!( @@ -560,21 +570,18 @@ fn remove_identity_file(file: &Path) -> DfxResult { Ok(()) } -/// Generates a new Ed25519 key. -#[context("Failed to generate a fresh ed25519 key.")] -pub(super) fn generate_key() -> DfxResult> { - let rng = rand::SystemRandom::new(); - let pkcs8_bytes = signature::Ed25519KeyPair::generate_pkcs8(&rng) - .map_err(|x| DfxError::new(IdentityError::CannotGenerateKeyPair(x)))?; - - let encoded_pem = encode_pem_private_key(&(*pkcs8_bytes.as_ref())); - Ok(Vec::from(encoded_pem)) +/// Generates a new secp256k1 key. +#[context("Failed to generate a fresh secp256k1 key.")] +pub(super) fn generate_key() -> DfxResult<(Vec, Mnemonic)> { + let mnemonic = Mnemonic::new(MnemonicType::for_key_size(256)?, Language::English); + let secret = mnemonic_to_key(&mnemonic)?; + let pem = secret.to_pem(LineEnding::CRLF)?; + Ok((pem.as_bytes().to_vec(), mnemonic)) } -fn encode_pem_private_key(key: &[u8]) -> String { - let pem = Pem { - tag: "PRIVATE KEY".to_owned(), - contents: key.to_vec(), - }; - encode(&pem) +pub fn mnemonic_to_key(mnemonic: &Mnemonic) -> DfxResult { + const DEFAULT_DERIVATION_PATH: &str = "m/44'/60'/0'/0/0"; + let seed = Seed::new(mnemonic, ""); + let pk = XPrv::derive_from_path(seed.as_bytes(), &DEFAULT_DERIVATION_PATH.parse()?)?; + Ok(SecretKey::from(pk.private_key())) } diff --git a/src/dfx/src/lib/identity/identity_utils.rs b/src/dfx/src/lib/identity/identity_utils.rs index 192f443561..e4747b26df 100644 --- a/src/dfx/src/lib/identity/identity_utils.rs +++ b/src/dfx/src/lib/identity/identity_utils.rs @@ -1,12 +1,13 @@ use crate::lib::environment::Environment; use crate::lib::error::DfxResult; -use anyhow::{bail, Context}; +use anyhow::bail; +use anyhow::Context; use candid::Principal; use fn_error_context::context; use ic_agent::identity::BasicIdentity; -use openssl::ec::EcKey; -use openssl::nid::Nid; +use ic_agent::identity::PemError; +use ic_agent::identity::Secp256k1Identity; #[derive(Debug, PartialEq)] pub enum CallSender { @@ -31,20 +32,20 @@ pub async fn call_sender(_env: &dyn Environment, wallet: &Option) -> Dfx #[context("Failed to validate pem file.")] pub fn validate_pem_file(pem_content: &[u8]) -> DfxResult { - if pem_content.starts_with(b"-----BEGIN EC PARAMETERS-----") - || pem_content.starts_with(b"-----BEGIN EC PRIVATE KEY-----") - { - let private_key = - EcKey::private_key_from_pem(pem_content).context("Cannot decode PEM file content.")?; - let named_curve = private_key.group().curve_name(); - let is_secp256k1 = named_curve == Some(Nid::SECP256K1); - if !is_secp256k1 { - bail!("This functionality is currently restricted to secp256k1 private keys."); + let secp_res = + Secp256k1Identity::from_pem(pem_content).context("Failed to validate PEM content."); + if let Err(e) = secp_res { + let basic_identity_res = BasicIdentity::from_pem(pem_content); + match basic_identity_res { + Err(PemError::KeyRejected(rj)) if rj.description_() == "VersionNotSupported" => { + bail!("Ed25519 v1 keys (those generated by OpenSSL) are not supported. Try again with a v2 key"); + } + Err(_) => { + bail!(e) + } + _ => {} } - } else { - // The PEM file generated by `dfx new` don't have EC PARAMETERS header and the curve is Ed25519 - let _basic_identity = BasicIdentity::from_pem(pem_content) - .context("Invalid Ed25519 private key in PEM file")?; } + Ok(()) } diff --git a/src/dfx/src/lib/identity/mod.rs b/src/dfx/src/lib/identity/mod.rs index 599e3c86cb..2e401d8243 100644 --- a/src/dfx/src/lib/identity/mod.rs +++ b/src/dfx/src/lib/identity/mod.rs @@ -12,6 +12,7 @@ use crate::lib::root_key::fetch_root_key_if_needed; use crate::lib::waiter::waiter_with_timeout; use anyhow::{anyhow, bail, Context}; +use bip39::{Language, Mnemonic}; use candid::Principal; use fn_error_context::context; use ic_agent::identity::{AnonymousIdentity, BasicIdentity, Secp256k1Identity}; @@ -132,13 +133,14 @@ impl Identity { match parameters { IdentityCreationParameters::Pem { disable_encryption } => { identity_config.encryption = create_encryption_config(disable_encryption)?; - let pem_content = identity_manager::generate_key()?; + let (pem_content, mnemonic) = identity_manager::generate_key()?; let pem_file = manager.get_identity_pem_path(&temp_identity_name, &identity_config); pem_encryption::write_pem_file( &pem_file, Some(&identity_config), pem_content.as_slice(), )?; + eprintln!("Your seed phrase for identity '{name}': {}\nThis can be used to reconstruct your key in case of emergency, so write it down in a safe place.", mnemonic.phrase()); } IdentityCreationParameters::PemFile { src_pem_file, @@ -146,6 +148,7 @@ impl Identity { } => { identity_config.encryption = create_encryption_config(disable_encryption)?; let src_pem_content = pem_encryption::load_pem_file(&src_pem_file, None)?; + identity_utils::validate_pem_file(&src_pem_content)?; let dst_pem_file = manager.get_identity_pem_path(&temp_identity_name, &identity_config); pem_encryption::write_pem_file( @@ -158,6 +161,20 @@ impl Identity { identity_config.hsm = Some(hsm); create(&temp_identity_dir)?; } + IdentityCreationParameters::SeedPhrase { + mnemonic, + disable_encryption, + } => { + identity_config.encryption = create_encryption_config(disable_encryption)?; + let mnemonic = Mnemonic::from_phrase(&mnemonic, Language::English)?; + let key = identity_manager::mnemonic_to_key(&mnemonic)?; + let pem_file = manager.get_identity_pem_path(&temp_identity_name, &identity_config); + pem_encryption::write_pem_file( + &pem_file, + Some(&identity_config), + key.to_pem(k256::pkcs8::LineEnding::CRLF)?.as_bytes(), + )?; + } } identity_manager::write_identity_configuration( &identity_config_location, @@ -265,8 +282,9 @@ impl Identity { let pem_path = manager.load_identity_pem_path(name)?; let pem_content = pem_encryption::load_pem_file(&pem_path, Some(&config))?; - Identity::load_secp256k1_identity(manager, name, &pem_content) - .or_else(|_| Identity::load_basic_identity(manager, name, &pem_content)) + Identity::load_secp256k1_identity(manager, name, &pem_content).or_else(|e| { + Identity::load_basic_identity(manager, name, &pem_content).map_err(|_| e) + }) } } diff --git a/src/dfx/src/lib/identity/pem_encryption.rs b/src/dfx/src/lib/identity/pem_encryption.rs index 918044f3c2..e9159d43d4 100644 --- a/src/dfx/src/lib/identity/pem_encryption.rs +++ b/src/dfx/src/lib/identity/pem_encryption.rs @@ -3,7 +3,6 @@ use std::path::Path; use crate::lib::error::DfxResult; use super::identity_manager::EncryptionConfiguration; -use super::identity_utils; use super::IdentityConfiguration; use crate::lib::identity::pem_encryption::PromptMode::{DecryptingToUse, EncryptingToCreate}; @@ -21,7 +20,6 @@ pub fn load_pem_file(path: &Path, config: Option<&IdentityConfiguration>) -> Dfx let content = std::fs::read(path) .with_context(|| format!("Failed to read {}.", path.to_string_lossy()))?; let content = maybe_decrypt_pem(content.as_slice(), config)?; - identity_utils::validate_pem_file(&content)?; Ok(content) }