Skip to main content

dfx_core/identity/
identity_manager.rs

1use super::pem_utils::validate_pem_file;
2use super::{WALLET_CONFIG_FILENAME, keyring_mock};
3use crate::config::directories::get_user_dfx_config_dir;
4use crate::error::encryption::EncryptionError;
5use crate::error::encryption::EncryptionError::{NonceGenerationFailed, SaltGenerationFailed};
6use crate::error::fs::ReadDirError;
7use crate::error::identity::{
8    ConvertMnemonicToKeyError,
9    ConvertMnemonicToKeyError::DeriveExtendedKeyFromPathFailed,
10    CreateIdentityConfigError,
11    CreateIdentityConfigError::{
12        GenerateFreshEncryptionConfigurationFailed, KeyringAvailabilityCheckFailed,
13    },
14    CreateNewIdentityError,
15    CreateNewIdentityError::{
16        ConvertSecretKeyToSec1PemFailed, CreateMnemonicFromPhraseFailed,
17        CreateTemporaryIdentityDirectoryFailed, SwitchBackToIdentityFailed,
18        SwitchToAnonymousIdentityFailed,
19    },
20    ExportIdentityError,
21    ExportIdentityError::TranslatePemContentToTextFailed,
22    GenerateKeyError,
23    GenerateKeyError::GenerateFreshSecp256k1KeyFailed,
24    GetIdentityConfigOrDefaultError,
25    GetIdentityConfigOrDefaultError::LoadIdentityConfigurationFailed,
26    GetLegacyCredentialsPemPathError,
27    GetLegacyCredentialsPemPathError::GetLegacyPemPathFailed,
28    InitializeIdentityManagerError,
29    InitializeIdentityManagerError::{
30        CreateIdentityDirectoryFailed, GenerateKeyFailed, MigrateLegacyIdentityFailed,
31        WritePemToFileFailed,
32    },
33    InstantiateIdentityFromNameError,
34    InstantiateIdentityFromNameError::{GetIdentityPrincipalFailed, LoadIdentityFailed},
35    LoadIdentityError, NewIdentityManagerError,
36    NewIdentityManagerError::LoadIdentityManagerConfigurationFailed,
37    RemoveIdentityError,
38    RemoveIdentityError::{
39        DisplayLinkedWalletsFailed, DropWalletsFlagRequiredToRemoveIdentityWithWallets,
40        RemoveIdentityDirectoryFailed,
41    },
42    RenameIdentityError,
43    RenameIdentityError::{
44        GetIdentityConfigFailed, LoadPemFailed, MapWalletsToRenamedIdentityFailed, SavePemFailed,
45        SwitchDefaultIdentitySettingsFailed,
46    },
47    RequireIdentityExistsError, SaveIdentityConfigurationError,
48    SaveIdentityConfigurationError::EnsureIdentityConfigurationDirExistsFailed,
49    UseIdentityByNameError,
50    UseIdentityByNameError::WriteDefaultIdentityFailed,
51    WriteDefaultIdentityError,
52    WriteDefaultIdentityError::SaveIdentityManagerConfigurationFailed,
53};
54use crate::error::structured_file::StructuredFileError;
55use crate::foundation::get_user_home;
56use crate::fs::composite::ensure_parent_dir_exists;
57use crate::identity::identity_file_locations::{IDENTITY_PEM, IdentityFileLocations};
58use crate::identity::identity_manager::IdentityStorageModeError::UnknownStorageMode;
59use crate::identity::{
60    ANONYMOUS_IDENTITY_NAME, IDENTITY_JSON, Identity as DfxIdentity, TEMP_IDENTITY_PREFIX,
61    pem_safekeeping, pem_utils,
62};
63use crate::json::{load_json_file, save_json_file};
64use bip32::XPrv;
65use bip39::{Language, Mnemonic, MnemonicType, Seed};
66use candid::Principal;
67use k256::SecretKey;
68use k256::pkcs8::LineEnding;
69use ring::{rand, rand::SecureRandom};
70use sec1::EncodeEcPrivateKey;
71use serde::{Deserialize, Serialize};
72use slog::{Logger, debug, trace};
73use std::boxed::Box;
74use std::collections::BTreeMap;
75use std::path::{Path, PathBuf};
76use std::str::FromStr;
77use thiserror::Error;
78
79const DEFAULT_IDENTITY_NAME: &str = "default";
80
81#[derive(Clone, Debug, Default, Serialize, Deserialize)]
82struct Configuration {
83    #[serde(default = "default_identity")]
84    pub default: String,
85}
86
87fn default_identity() -> String {
88    String::from(DEFAULT_IDENTITY_NAME)
89}
90
91#[derive(Clone, Debug, Default, Serialize, Deserialize)]
92pub struct IdentityConfiguration {
93    pub hsm: Option<HardwareIdentityConfiguration>,
94
95    /// If the identity's PEM file is encrypted on disk this contains everything (except the password) to decrypt the file.
96    pub encryption: Option<EncryptionConfiguration>,
97
98    /// If the identity's PEM file is stored in the system's keyring, this field contains the identity's name WITHOUT the common prefix.
99    pub keyring_identity_suffix: Option<String>,
100}
101
102/// The information necessary to de- and encrypt (except the password) the identity's .pem file
103#[derive(Clone, Debug, Serialize, Deserialize)]
104pub struct EncryptionConfiguration {
105    /// Salt used for deriving the key from the password
106    pub pw_salt: String,
107
108    /// 96 bit Nonce used to decrypt the file
109    pub file_nonce: Vec<u8>,
110}
111
112impl EncryptionConfiguration {
113    /// Generates a random salt and nonce. Use this for every new identity
114    pub fn new() -> Result<Self, EncryptionError> {
115        let mut nonce: [u8; 12] = [0; 12];
116        let mut salt: [u8; 32] = [0; 32];
117        let sr = rand::SystemRandom::new();
118        sr.fill(&mut nonce).map_err(NonceGenerationFailed)?;
119        sr.fill(&mut salt).map_err(SaltGenerationFailed)?;
120
121        let pw_salt = hex::encode(salt);
122        let file_nonce = nonce.into();
123
124        Ok(Self {
125            pw_salt,
126            file_nonce,
127        })
128    }
129}
130
131#[derive(Clone, Debug, Default, Serialize, Deserialize)]
132pub struct HardwareIdentityConfiguration {
133    #[cfg_attr(
134        not(windows),
135        doc = r#"The file path to the opensc-pkcs11 library e.g. "/usr/local/lib/opensc-pkcs11.so""#
136    )]
137    #[cfg_attr(
138        windows,
139        doc = r#"The file path to the opensc-pkcs11 library e.g. "C:\Program Files (x86)\OpenSC Project\OpenSC\pkcs11\opensc-pkcs11.dll"#
140    )]
141    pub pkcs11_lib_path: String,
142
143    /// A sequence of pairs of hex digits
144    pub key_id: String,
145}
146
147#[derive(Clone, Debug, Serialize, Deserialize, Copy, PartialEq, Eq)]
148pub enum IdentityStorageMode {
149    Keyring,
150    PasswordProtected,
151    Plaintext,
152}
153
154#[derive(Error, Debug)]
155pub enum IdentityStorageModeError {
156    #[error("Unknown storage mode: {0}")]
157    UnknownStorageMode(String),
158}
159
160impl FromStr for IdentityStorageMode {
161    type Err = IdentityStorageModeError;
162
163    fn from_str(input: &str) -> Result<Self, Self::Err> {
164        match input {
165            "keyring" => Ok(IdentityStorageMode::Keyring),
166            "password-protected" => Ok(IdentityStorageMode::PasswordProtected),
167            "plaintext" => Ok(IdentityStorageMode::Plaintext),
168            other => Err(UnknownStorageMode(other.to_string())),
169        }
170    }
171}
172
173impl Default for IdentityStorageMode {
174    fn default() -> Self {
175        Self::Keyring
176    }
177}
178
179pub enum IdentityCreationParameters {
180    Pem {
181        mode: IdentityStorageMode,
182    },
183    PemFile {
184        src_pem_file: PathBuf,
185        mode: IdentityStorageMode,
186    },
187    SeedPhrase {
188        mnemonic: String,
189        mode: IdentityStorageMode,
190    },
191    Hardware {
192        hsm: HardwareIdentityConfiguration,
193    },
194}
195
196#[derive(Clone, Debug)]
197pub struct IdentityManager {
198    identity_json_path: PathBuf,
199    file_locations: IdentityFileLocations,
200    configuration: Configuration,
201    selected_identity: String,
202    selected_identity_principal: Option<Principal>,
203}
204
205#[derive(PartialEq)]
206pub enum InitializeIdentity {
207    Allow,
208    Disallow,
209}
210
211impl IdentityManager {
212    pub fn new(
213        logger: &Logger,
214        identity_override: Option<&str>,
215        initialize_identity: InitializeIdentity,
216    ) -> Result<Self, NewIdentityManagerError> {
217        let config_dfx_dir_path =
218            get_user_dfx_config_dir().map_err(NewIdentityManagerError::GetConfigDirectoryFailed)?;
219        let identity_root_path = config_dfx_dir_path.join("identity");
220        let identity_json_path = config_dfx_dir_path.join("identity.json");
221
222        let configuration = if identity_json_path.exists() {
223            load_configuration(&identity_json_path)
224                .map_err(LoadIdentityManagerConfigurationFailed)?
225        } else if initialize_identity == InitializeIdentity::Disallow {
226            return Err(NewIdentityManagerError::NoIdentityConfigurationFound);
227        } else {
228            initialize(logger, &identity_json_path, &identity_root_path)
229                .map_err(NewIdentityManagerError::InitializeFailed)?
230        };
231
232        let selected_identity = identity_override
233            .unwrap_or(&configuration.default)
234            .to_string();
235
236        let file_locations = IdentityFileLocations::new(identity_root_path);
237
238        let mgr = IdentityManager {
239            identity_json_path,
240            file_locations,
241            configuration,
242            selected_identity,
243            selected_identity_principal: None,
244        };
245
246        if let Some(identity) = identity_override {
247            mgr.require_identity_exists(logger, identity)
248                .map_err(NewIdentityManagerError::OverrideIdentityMustExist)?;
249        }
250
251        Ok(mgr)
252    }
253
254    pub fn get_selected_identity_principal(&self) -> Option<Principal> {
255        self.selected_identity_principal
256    }
257
258    /// Create an Identity instance for use with an Agent
259    pub fn instantiate_selected_identity(
260        &mut self,
261        log: &Logger,
262    ) -> Result<Box<DfxIdentity>, InstantiateIdentityFromNameError> {
263        let name = self.selected_identity.clone();
264        self.instantiate_identity_from_name(name.as_str(), log)
265    }
266
267    /// Provide a valid Identity name and create its Identity instance for use with an Agent
268    pub fn instantiate_identity_from_name(
269        &mut self,
270        identity_name: &str,
271        log: &Logger,
272    ) -> Result<Box<DfxIdentity>, InstantiateIdentityFromNameError> {
273        let identity = match identity_name {
274            ANONYMOUS_IDENTITY_NAME => Box::new(DfxIdentity::anonymous()),
275            identity_name => {
276                self.require_identity_exists(log, identity_name)
277                    .map_err(InstantiateIdentityFromNameError::RequireIdentityExistsFailed)?;
278                Box::new(
279                    self.load_identity(identity_name, log)
280                        .map_err(LoadIdentityFailed)?,
281                )
282            }
283        };
284        use ic_agent::identity::Identity;
285        self.selected_identity_principal =
286            Some(identity.sender().map_err(GetIdentityPrincipalFailed)?);
287        Ok(identity)
288    }
289
290    fn load_identity(&self, name: &str, log: &Logger) -> Result<DfxIdentity, LoadIdentityError> {
291        let config = self
292            .get_identity_config_or_default(name)
293            .map_err(LoadIdentityError::GetIdentityConfigOrDefaultFailed)?;
294        DfxIdentity::new(name, config, self.file_locations(), log)
295            .map_err(LoadIdentityError::NewIdentityFailed)
296    }
297
298    /// Create a new identity (name -> generated key)
299    ///
300    /// `force`: If the identity already exists, remove and re-create it.
301    pub fn create_new_identity(
302        &mut self,
303        log: &Logger,
304        name: &str,
305        parameters: IdentityCreationParameters,
306        force: bool,
307    ) -> Result<(), CreateNewIdentityError> {
308        if name == ANONYMOUS_IDENTITY_NAME {
309            return Err(CreateNewIdentityError::CannotCreateAnonymousIdentity());
310        }
311
312        trace!(log, "Creating identity '{name}'.");
313        let identity_in_use = self.get_selected_identity_name().clone();
314        // cannot delete an identity in use. Use anonymous identity temporarily if we force-overwrite the identity currently in use
315        let temporarily_use_anonymous_identity = identity_in_use == name && force;
316
317        if self.require_identity_exists(log, name).is_ok() {
318            trace!(log, "Identity already exists.");
319            if force {
320                if temporarily_use_anonymous_identity {
321                    self.use_identity_named(log, ANONYMOUS_IDENTITY_NAME)
322                        .map_err(SwitchToAnonymousIdentityFailed)?;
323                }
324                self.remove(log, name, true, None)
325                    .map_err(CreateNewIdentityError::RemoveIdentityFailed)?;
326            } else {
327                return Err(CreateNewIdentityError::IdentityAlreadyExists());
328            }
329        }
330
331        fn create_identity_config(
332            log: &Logger,
333            mode: IdentityStorageMode,
334            name: &str,
335            hardware_config: Option<HardwareIdentityConfiguration>,
336        ) -> Result<IdentityConfiguration, CreateIdentityConfigError> {
337            if let Some(hsm) = hardware_config {
338                Ok(IdentityConfiguration {
339                    hsm: Some(hsm),
340                    ..Default::default()
341                })
342            } else {
343                match mode {
344                    IdentityStorageMode::Keyring => {
345                        if keyring_mock::keyring_available(log)
346                            .map_err(KeyringAvailabilityCheckFailed)?
347                        {
348                            Ok(IdentityConfiguration {
349                                keyring_identity_suffix: Some(String::from(name)),
350                                ..Default::default()
351                            })
352                        } else {
353                            Ok(IdentityConfiguration {
354                                encryption: Some(
355                                    EncryptionConfiguration::new()
356                                        .map_err(GenerateFreshEncryptionConfigurationFailed)?,
357                                ),
358                                ..Default::default()
359                            })
360                        }
361                    }
362                    IdentityStorageMode::PasswordProtected => Ok(IdentityConfiguration {
363                        encryption: Some(
364                            EncryptionConfiguration::new()
365                                .map_err(GenerateFreshEncryptionConfigurationFailed)?,
366                        ),
367                        ..Default::default()
368                    }),
369                    IdentityStorageMode::Plaintext => Ok(IdentityConfiguration::default()),
370                }
371            }
372        }
373
374        // Use a temporary directory to prepare all identity parts in so that we don't end up with broken parts if the
375        // creation process fails half-way through.
376        let temp_identity_name = format!("{TEMP_IDENTITY_PREFIX}{name}");
377        let temp_identity_dir = self.get_identity_dir_path(&temp_identity_name);
378        if temp_identity_dir.exists() {
379            // clean traces from previous identity creation attempts
380            crate::fs::remove_dir_all(&temp_identity_dir)?;
381        }
382
383        let identity_config;
384        match parameters {
385            IdentityCreationParameters::Pem { mode } => {
386                let (pem_content, mnemonic) =
387                    generate_key().map_err(CreateNewIdentityError::GenerateKeyFailed)?;
388                identity_config = create_identity_config(log, mode, name, None)
389                    .map_err(CreateNewIdentityError::CreateIdentityConfigFailed)?;
390                pem_safekeeping::save_pem(
391                    log,
392                    self.file_locations(),
393                    &temp_identity_name,
394                    &identity_config,
395                    pem_content.as_slice(),
396                )
397                .map_err(CreateNewIdentityError::SavePemFailed)?;
398                eprintln!(
399                    "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.",
400                    mnemonic.phrase()
401                );
402            }
403            IdentityCreationParameters::PemFile { src_pem_file, mode } => {
404                identity_config = create_identity_config(log, mode, name, None)
405                    .map_err(CreateNewIdentityError::CreateIdentityConfigFailed)?;
406                let (src_pem_content, _) = pem_safekeeping::load_pem_from_file(&src_pem_file, None)
407                    .map_err(CreateNewIdentityError::LoadPemFromFileFailed)?;
408                pem_utils::validate_pem_file(&src_pem_content)
409                    .map_err(CreateNewIdentityError::ValidatePemFileFailed)?;
410                pem_safekeeping::save_pem(
411                    log,
412                    self.file_locations(),
413                    &temp_identity_name,
414                    &identity_config,
415                    src_pem_content.as_slice(),
416                )
417                .map_err(CreateNewIdentityError::SavePemFailed)?;
418            }
419            IdentityCreationParameters::Hardware { hsm } => {
420                identity_config =
421                    create_identity_config(log, IdentityStorageMode::default(), name, Some(hsm))
422                        .map_err(CreateNewIdentityError::CreateIdentityConfigFailed)?;
423                crate::fs::create_dir_all(&temp_identity_dir)
424                    .map_err(CreateTemporaryIdentityDirectoryFailed)?;
425            }
426            IdentityCreationParameters::SeedPhrase { mnemonic, mode } => {
427                identity_config = create_identity_config(log, mode, name, None)
428                    .map_err(CreateNewIdentityError::CreateIdentityConfigFailed)?;
429                let mnemonic = Mnemonic::from_phrase(&mnemonic, Language::English)
430                    .map_err(|e| CreateMnemonicFromPhraseFailed(format!("{e}")))?;
431                let key = mnemonic_to_key(&mnemonic)
432                    .map_err(CreateNewIdentityError::ConvertMnemonicToKeyFailed)?;
433                let pem = key
434                    .to_sec1_pem(k256::pkcs8::LineEnding::CRLF)
435                    .map_err(|e| ConvertSecretKeyToSec1PemFailed(Box::new(e)))?;
436                let pem_content = pem.as_bytes();
437                pem_safekeeping::save_pem(
438                    log,
439                    self.file_locations(),
440                    &temp_identity_name,
441                    &identity_config,
442                    pem_content,
443                )
444                .map_err(CreateNewIdentityError::SavePemFailed)?;
445            }
446        }
447        let identity_config_location = self.get_identity_json_path(&temp_identity_name);
448        save_identity_configuration(log, &identity_config_location, &identity_config)
449            .map_err(CreateNewIdentityError::SaveIdentityConfigurationFailed)?;
450
451        // Everything is created. Now move from the temporary directory to the actual identity location.
452        let identity_dir = self.get_identity_dir_path(name);
453        crate::fs::rename(&temp_identity_dir, &identity_dir)?;
454
455        if temporarily_use_anonymous_identity {
456            self.use_identity_named(log, &identity_in_use)
457                .map_err(SwitchBackToIdentityFailed)?;
458        }
459        Ok(())
460    }
461
462    /// Return a sorted list of all available identity names
463    pub fn get_identity_names(&self, log: &Logger) -> Result<Vec<String>, ReadDirError> {
464        let mut names = crate::fs::read_dir(self.file_locations.root())?
465            .filter_map(|entry_result| match entry_result {
466                Ok(dir_entry) => match dir_entry.file_type() {
467                    Ok(file_type) if file_type.is_dir() => Some(dir_entry),
468                    _ => None,
469                },
470                _ => None,
471            })
472            .map(|entry| entry.file_name().to_string_lossy().to_string())
473            .filter(|identity_name| self.require_identity_exists(log, identity_name).is_ok())
474            .collect::<Vec<_>>();
475        names.push(ANONYMOUS_IDENTITY_NAME.to_string());
476
477        names.sort();
478
479        Ok(names)
480    }
481
482    /// Return the name of the currently selected (active) identity
483    pub fn get_selected_identity_name(&self) -> &String {
484        &self.selected_identity
485    }
486
487    pub(crate) fn file_locations(&self) -> &IdentityFileLocations {
488        &self.file_locations
489    }
490
491    /// Returns the pem file content of the selected identity
492    pub fn export(&self, log: &Logger, name: &str) -> Result<String, ExportIdentityError> {
493        self.require_identity_exists(log, name)
494            .map_err(ExportIdentityError::IdentityDoesNotExist)?;
495        let config = self
496            .get_identity_config_or_default(name)
497            .map_err(ExportIdentityError::GetIdentityConfigFailed)?;
498        let (pem_content, _) = pem_safekeeping::load_pem(log, &self.file_locations, name, &config)
499            .map_err(ExportIdentityError::LoadPemFailed)?;
500
501        validate_pem_file(&pem_content).map_err(ExportIdentityError::ValidatePemFileFailed)?;
502        String::from_utf8(pem_content).map_err(TranslatePemContentToTextFailed)
503    }
504
505    /// Remove a named identity.
506    /// Removing the selected identity is not allowed.
507    /// Removing an identity that is connected to non-ephemeral wallets is only allowed if drop_wallets is true.
508    /// If display_linked_wallets_to contains a logger, this will log all the wallets the identity is connected to.
509    pub fn remove(
510        &self,
511        log: &Logger,
512        name: &str,
513        drop_wallets: bool,
514        display_linked_wallets_to: Option<&Logger>,
515    ) -> Result<(), RemoveIdentityError> {
516        self.require_identity_exists(log, name)
517            .map_err(RemoveIdentityError::RequireIdentityExistsFailed)?;
518
519        if name == ANONYMOUS_IDENTITY_NAME {
520            return Err(RemoveIdentityError::CannotDeleteAnonymousIdentity());
521        }
522
523        if self.configuration.default == name {
524            return Err(RemoveIdentityError::CannotDeleteDefaultIdentity());
525        }
526
527        let wallet_config_file = self.get_persistent_wallet_config_file(name);
528        if wallet_config_file.exists() {
529            if let Some(logger) = display_linked_wallets_to {
530                DfxIdentity::display_linked_wallets(logger, &wallet_config_file)
531                    .map_err(DisplayLinkedWalletsFailed)?;
532            }
533            if drop_wallets {
534                remove_identity_file(&wallet_config_file)?;
535            } else {
536                return Err(DropWalletsFlagRequiredToRemoveIdentityWithWallets());
537            }
538        }
539
540        if let Ok(config) = self.get_identity_config_or_default(name) {
541            if let Some(suffix) = config.keyring_identity_suffix {
542                keyring_mock::delete_pem_from_keyring(&suffix)
543                    .map_err(RemoveIdentityError::RemoveIdentityFromKeyringFailed)?;
544            }
545        }
546        remove_identity_file(&self.get_identity_json_path(name))?;
547        remove_identity_file(&self.file_locations.get_plaintext_identity_pem_path(name))?;
548        remove_identity_file(&self.file_locations.get_encrypted_identity_pem_path(name))?;
549
550        let dir = self.get_identity_dir_path(name);
551        if dir.exists() {
552            crate::fs::remove_dir(&dir).map_err(RemoveIdentityDirectoryFailed)?;
553        }
554
555        Ok(())
556    }
557
558    /// Rename an identity.
559    /// If renaming the selected (default) identity, changes that
560    /// to refer to the new identity name.
561    pub fn rename(
562        &mut self,
563        log: &Logger,
564        project_temp_dir: Option<PathBuf>,
565        from: &str,
566        to: &str,
567    ) -> Result<bool, RenameIdentityError> {
568        if to == ANONYMOUS_IDENTITY_NAME {
569            return Err(RenameIdentityError::CannotCreateAnonymousIdentity());
570        }
571        self.require_identity_exists(log, from)
572            .map_err(RenameIdentityError::IdentityDoesNotExist)?;
573
574        let identity_config = self
575            .get_identity_config_or_default(from)
576            .map_err(GetIdentityConfigFailed)?;
577        let from_dir = self.get_identity_dir_path(from);
578        let to_dir = self.get_identity_dir_path(to);
579
580        if to_dir.exists() {
581            return Err(RenameIdentityError::IdentityAlreadyExists());
582        }
583
584        DfxIdentity::map_wallets_to_renamed_identity(project_temp_dir, from, to)
585            .map_err(MapWalletsToRenamedIdentityFailed)?;
586        crate::fs::rename(&from_dir, &to_dir)?;
587        if let Some(keyring_identity_suffix) = &identity_config.keyring_identity_suffix {
588            debug!(log, "Migrating keyring content.");
589            let (pem, _) =
590                pem_safekeeping::load_pem(log, &self.file_locations, from, &identity_config)
591                    .map_err(LoadPemFailed)?;
592            let new_config = IdentityConfiguration {
593                keyring_identity_suffix: Some(to.to_string()),
594                ..identity_config
595            };
596            pem_safekeeping::save_pem(log, &self.file_locations, to, &new_config, pem.as_ref())
597                .map_err(SavePemFailed)?;
598            let config_path = self.get_identity_json_path(to);
599            save_identity_configuration(log, &config_path, &new_config)
600                .map_err(RenameIdentityError::SaveIdentityConfigurationFailed)?;
601            keyring_mock::delete_pem_from_keyring(keyring_identity_suffix)
602                .map_err(RenameIdentityError::RemoveIdentityFromKeyringFailed)?;
603        }
604
605        if from == self.configuration.default {
606            self.write_default_identity(to)
607                .map_err(SwitchDefaultIdentitySettingsFailed)?;
608            Ok(true)
609        } else {
610            Ok(false)
611        }
612    }
613
614    /// Select an identity by name to use by default
615    pub fn use_identity_named(
616        &mut self,
617        log: &Logger,
618        name: &str,
619    ) -> Result<(), UseIdentityByNameError> {
620        self.require_identity_exists(log, name)
621            .map_err(UseIdentityByNameError::RequireIdentityExistsFailed)?;
622        self.write_default_identity(name)
623            .map_err(WriteDefaultIdentityFailed)?;
624        self.configuration.default = name.to_string();
625        Ok(())
626    }
627
628    fn write_default_identity(&self, name: &str) -> Result<(), WriteDefaultIdentityError> {
629        let config = Configuration {
630            default: String::from(name),
631        };
632        save_configuration(&self.identity_json_path, &config)
633            .map_err(SaveIdentityManagerConfigurationFailed)?;
634        Ok(())
635    }
636
637    /// Determines if there are enough files present to consider the identity as existing.
638    /// Does NOT guarantee that the identity will load correctly.
639    pub fn require_identity_exists(
640        &self,
641        log: &Logger,
642        name: &str,
643    ) -> Result<(), RequireIdentityExistsError> {
644        trace!(log, "Checking if identity '{name}' exists.");
645        if name == ANONYMOUS_IDENTITY_NAME {
646            return Ok(());
647        }
648
649        if name.starts_with(TEMP_IDENTITY_PREFIX) {
650            return Err(RequireIdentityExistsError::ReservedIdentityName(
651                String::from(name),
652            ));
653        }
654
655        let json_path = self.get_identity_json_path(name);
656        let plaintext_pem_path = self.file_locations.get_plaintext_identity_pem_path(name);
657        let encrypted_pem_path = self.file_locations.get_encrypted_identity_pem_path(name);
658
659        if !plaintext_pem_path.exists() && !encrypted_pem_path.exists() && !json_path.exists() {
660            Err(RequireIdentityExistsError::IdentityDoesNotExist(
661                String::from(name),
662                json_path,
663            ))
664        } else {
665            Ok(())
666        }
667    }
668
669    pub fn get_identity_dir_path(&self, identity: &str) -> PathBuf {
670        self.file_locations.get_identity_dir_path(identity)
671    }
672
673    /// Returns the path where wallets on persistent/non-ephemeral networks are stored.
674    fn get_persistent_wallet_config_file(&self, identity: &str) -> PathBuf {
675        self.get_identity_dir_path(identity)
676            .join(WALLET_CONFIG_FILENAME)
677    }
678
679    /// Returns the path where an identity's `IdentityConfiguration` is stored.
680    pub fn get_identity_json_path(&self, identity: &str) -> PathBuf {
681        self.get_identity_dir_path(identity).join(IDENTITY_JSON)
682    }
683
684    /// Returns a map of (name, principal) pairs for unencrypted identities.
685    /// In the future, we may refactor the code to include encrypted principals as well.
686    pub fn get_unencrypted_principal_map(&self, log: &Logger) -> BTreeMap<String, String> {
687        use ic_agent::Identity;
688        let mut res = if let Ok(names) = self.get_identity_names(log) {
689            names
690                .iter()
691                .filter_map(|name| {
692                    let config = self.get_identity_config_or_default(name).ok()?;
693                    if let IdentityConfiguration {
694                        encryption: None,
695                        keyring_identity_suffix: None,
696                        hsm: None,
697                    } = config
698                    {
699                        let sender = self.load_identity(name, log).ok()?.sender().ok()?;
700                        Some((name.clone(), sender.to_text()))
701                    } else {
702                        None
703                    }
704                })
705                .collect()
706        } else {
707            BTreeMap::new()
708        };
709        res.insert(
710            ANONYMOUS_IDENTITY_NAME.to_string(),
711            Principal::anonymous().to_string(),
712        );
713        res
714    }
715
716    pub fn get_identity_config_or_default(
717        &self,
718        identity: &str,
719    ) -> Result<IdentityConfiguration, GetIdentityConfigOrDefaultError> {
720        let json_path = self.get_identity_json_path(identity);
721        if json_path.exists() {
722            load_json_file(&json_path)
723                .map_err(|err| LoadIdentityConfigurationFailed(identity.to_string(), err))
724        } else {
725            Ok(IdentityConfiguration::default())
726        }
727    }
728}
729
730pub(super) fn get_dfx_hsm_pin() -> Result<String, String> {
731    std::env::var("DFX_HSM_PIN")
732        .map_err(|_| "There is no DFX_HSM_PIN environment variable.".to_string())
733}
734
735fn initialize(
736    logger: &Logger,
737    identity_json_path: &Path,
738    identity_root_path: &Path,
739) -> Result<Configuration, InitializeIdentityManagerError> {
740    slog::info!(
741        logger,
742        r#"Creating the "default" identity.
743WARNING: The "default" identity is not stored securely. Do not use it to control a lot of cycles/ICP.
744To create a more secure identity, create and use an identity that is protected by a password using the following commands:
745    dfx identity new <my-secure-identity-name> # creates a password protected identity
746    dfx identity use <my-secure-identity-name> # uses this identity by default
747"#
748    );
749
750    let identity_dir = identity_root_path.join(DEFAULT_IDENTITY_NAME);
751    let identity_pem_path = identity_dir.join(IDENTITY_PEM);
752    if !identity_pem_path.exists() {
753        if !identity_dir.exists() {
754            crate::fs::create_dir_all(&identity_dir).map_err(CreateIdentityDirectoryFailed)?;
755        }
756
757        let maybe_creds_pem_path = get_legacy_creds_pem_path()?;
758        if maybe_creds_pem_path
759            .as_ref()
760            .map(|p| p.exists())
761            .unwrap_or_default()
762        {
763            let creds_pem_path =
764                maybe_creds_pem_path.expect("Unreachable - Just checked for existence.");
765            slog::info!(
766                logger,
767                "  - migrating key from {} to {}",
768                creds_pem_path.display(),
769                identity_pem_path.display()
770            );
771            crate::fs::copy(&creds_pem_path, &identity_pem_path)
772                .map_err(MigrateLegacyIdentityFailed)?;
773        } else {
774            slog::info!(
775                logger,
776                "  - generating new key at {}",
777                identity_pem_path.display()
778            );
779            let (key, mnemonic) = generate_key().map_err(GenerateKeyFailed)?;
780            pem_safekeeping::write_pem_to_file(&identity_pem_path, None, key.as_slice())
781                .map_err(WritePemToFileFailed)?;
782            eprintln!(
783                "Your seed phrase: {}\nThis can be used to reconstruct your key in case of emergency, so write it down in a safe place.",
784                mnemonic.phrase()
785            );
786        }
787    } else {
788        slog::info!(
789            logger,
790            "  - using key already in place at {}",
791            identity_pem_path.display()
792        );
793    }
794
795    let config = Configuration {
796        default: String::from(DEFAULT_IDENTITY_NAME),
797    };
798    save_configuration(identity_json_path, &config)
799        .map_err(InitializeIdentityManagerError::SaveConfigurationFailed)?;
800    slog::info!(logger, r#"Created the "default" identity."#);
801
802    Ok(config)
803}
804
805fn get_legacy_creds_pem_path() -> Result<Option<PathBuf>, GetLegacyCredentialsPemPathError> {
806    if cfg!(windows) {
807        // No legacy path on Windows - there was no Windows support when paths were changed
808        Ok(None)
809    } else {
810        let config_root = crate::config::directories::DFX_CONFIG_ROOT
811            .lock()
812            .unwrap()
813            .clone();
814        let home = get_user_home().map_err(GetLegacyPemPathFailed)?;
815        let root = config_root.unwrap_or(home);
816
817        Ok(Some(
818            PathBuf::from(root)
819                .join(".dfinity")
820                .join("identity")
821                .join("creds.pem"),
822        ))
823    }
824}
825
826fn load_configuration(path: &Path) -> Result<Configuration, StructuredFileError> {
827    load_json_file(path)
828}
829
830fn save_configuration(path: &Path, config: &Configuration) -> Result<(), StructuredFileError> {
831    save_json_file(path, config)
832}
833
834pub(super) fn save_identity_configuration(
835    log: &Logger,
836    path: &Path,
837    config: &IdentityConfiguration,
838) -> Result<(), SaveIdentityConfigurationError> {
839    trace!(log, "Writing identity configuration to {}", path.display());
840    ensure_parent_dir_exists(path).map_err(EnsureIdentityConfigurationDirExistsFailed)?;
841
842    save_json_file(path, &config)
843        .map_err(SaveIdentityConfigurationError::SaveIdentityConfigurationFailed)
844}
845
846/// Removes the file if it exists.
847fn remove_identity_file(file: &Path) -> Result<(), RemoveIdentityError> {
848    if file.exists() {
849        crate::fs::remove_file(file)?;
850    }
851    Ok(())
852}
853
854/// Generates a new secp256k1 key.
855pub(super) fn generate_key() -> Result<(Vec<u8>, Mnemonic), GenerateKeyError> {
856    let mnemonic = Mnemonic::new(MnemonicType::for_key_size(256).unwrap(), Language::English);
857    let secret =
858        mnemonic_to_key(&mnemonic).map_err(GenerateKeyError::ConvertMnemonicToKeyFailed)?;
859    let pem = secret
860        .to_sec1_pem(LineEnding::CRLF)
861        .map_err(|e| GenerateFreshSecp256k1KeyFailed(Box::new(e)))?;
862    Ok((pem.as_bytes().to_vec(), mnemonic))
863}
864
865pub fn mnemonic_to_key(mnemonic: &Mnemonic) -> Result<SecretKey, ConvertMnemonicToKeyError> {
866    const DEFAULT_DERIVATION_PATH: &str = "m/44'/223'/0'/0/0";
867    let path = DEFAULT_DERIVATION_PATH.parse().unwrap();
868    let seed = Seed::new(mnemonic, "");
869    let pk =
870        XPrv::derive_from_path(seed.as_bytes(), &path).map_err(DeriveExtendedKeyFromPathFailed)?;
871    Ok(SecretKey::from(pk.private_key()))
872}