1use crate::config::directories::{get_shared_wallet_config_path, get_user_dfx_config_dir};
6use crate::config::model::network_descriptor::NetworkDescriptor;
7use crate::error::wallet_config::SaveWalletConfigError;
8use crate::error::{
9 identity::{
10 CallSenderFromWalletError,
11 CallSenderFromWalletError::{
12 ParsePrincipalFromIdFailedAndGetWalletCanisterIdFailed,
13 ParsePrincipalFromIdFailedAndNoWallet,
14 },
15 LoadPemIdentityError,
16 LoadPemIdentityError::ReadIdentityFileFailed,
17 MapWalletsToRenamedIdentityError,
18 MapWalletsToRenamedIdentityError::RenameWalletGlobalConfigKeyFailed,
19 NewHardwareIdentityError,
20 NewHardwareIdentityError::InstantiateHardwareIdentityFailed,
21 NewIdentityError, RenameWalletGlobalConfigKeyError,
22 RenameWalletGlobalConfigKeyError::RenameWalletFailed,
23 },
24 wallet_config::{WalletConfigError, WalletConfigError::LoadWalletConfigFailed},
25};
26use crate::fs::composite::ensure_parent_dir_exists;
27use crate::identity::identity_file_locations::IdentityFileLocations;
28use crate::identity::wallet::wallet_canister_id;
29use crate::json::{load_json_file, save_json_file};
30use candid::Principal;
31use ic_agent::Signature;
32use ic_agent::agent::EnvelopeContent;
33use ic_agent::identity::{
34 AnonymousIdentity, BasicIdentity, Delegation, Secp256k1Identity, SignedDelegation,
35};
36use ic_identity_hsm::HardwareIdentity;
37pub use identity_manager::{
38 HardwareIdentityConfiguration, IdentityConfiguration, IdentityCreationParameters,
39 IdentityManager,
40};
41use serde::{Deserialize, Serialize};
42use slog::{Logger, info};
43use std::collections::BTreeMap;
44use std::path::{Path, PathBuf};
45
46mod identity_file_locations;
47pub mod identity_manager;
48pub mod keyring_mock;
49pub mod pem_safekeeping;
50pub mod pem_utils;
51pub mod wallet;
52
53pub const ANONYMOUS_IDENTITY_NAME: &str = "anonymous";
54pub const IDENTITY_JSON: &str = "identity.json";
55pub const TEMP_IDENTITY_PREFIX: &str = if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
56 "___s_temp___"
57} else {
58 "___temp___"
59};
60
61pub const WALLET_CONFIG_FILENAME: &str = "wallets.json";
62const HSM_SLOT_INDEX: usize = 0;
63
64#[derive(Debug, Serialize, Deserialize)]
65pub struct WalletNetworkMap {
66 #[serde(flatten)]
67 pub networks: BTreeMap<String, Principal>,
68}
69
70#[derive(Debug, Serialize, Deserialize)]
71pub struct WalletGlobalConfig {
72 pub identities: BTreeMap<String, WalletNetworkMap>,
73}
74
75pub struct Identity {
76 name: String,
78
79 pub insecure: bool,
82
83 inner: Box<dyn ic_agent::Identity + Sync + Send>,
85
86 identity_type: IdentityType,
87}
88
89impl Identity {
90 pub fn anonymous() -> Self {
91 Self {
92 name: ANONYMOUS_IDENTITY_NAME.to_string(),
93 inner: Box::new(AnonymousIdentity {}),
94 insecure: false,
95 identity_type: IdentityType::Anonymous,
96 }
97 }
98
99 fn basic(
100 name: &str,
101 pem_content: &[u8],
102 identity_type: IdentityType,
103 ) -> Result<Self, LoadPemIdentityError> {
104 let inner = Box::new(
105 BasicIdentity::from_pem(pem_content)
106 .map_err(|e| ReadIdentityFileFailed(name.into(), Box::new(e)))?,
107 );
108
109 Ok(Self {
110 name: name.to_string(),
111 inner,
112 insecure: identity_type == IdentityType::Plaintext,
113 identity_type,
114 })
115 }
116
117 fn secp256k1(
118 name: &str,
119 pem_content: &[u8],
120 identity_type: IdentityType,
121 ) -> Result<Self, LoadPemIdentityError> {
122 let inner = Box::new(
123 Secp256k1Identity::from_pem(pem_content)
124 .map_err(|e| ReadIdentityFileFailed(name.into(), Box::new(e)))?,
125 );
126
127 Ok(Self {
128 name: name.to_string(),
129 inner,
130 insecure: identity_type == IdentityType::Plaintext,
131 identity_type,
132 })
133 }
134
135 fn hardware(
136 name: &str,
137 hsm: HardwareIdentityConfiguration,
138 ) -> Result<Self, NewHardwareIdentityError> {
139 let inner = Box::new(
140 HardwareIdentity::new(
141 hsm.pkcs11_lib_path,
142 HSM_SLOT_INDEX,
143 &hsm.key_id,
144 identity_manager::get_dfx_hsm_pin,
145 )
146 .map_err(|e| InstantiateHardwareIdentityFailed(name.into(), Box::new(e)))?,
147 );
148 Ok(Self {
149 name: name.to_string(),
150 inner,
151 insecure: false,
152 identity_type: IdentityType::Hsm,
153 })
154 }
155
156 pub(crate) fn new(
157 name: &str,
158 config: IdentityConfiguration,
159 locations: &IdentityFileLocations,
160 log: &Logger,
161 ) -> Result<Self, NewIdentityError> {
162 if let Some(hsm) = config.hsm {
163 Identity::hardware(name, hsm).map_err(NewIdentityError::NewHardwareIdentityFailed)
164 } else {
165 let (pem_content, identity_type) =
166 pem_safekeeping::load_pem(log, locations, name, &config)
167 .map_err(NewIdentityError::LoadPemFailed)?;
168 Identity::secp256k1(name, &pem_content, identity_type)
169 .or_else(|e| Identity::basic(name, &pem_content, identity_type).map_err(|_| e))
170 .map_err(NewIdentityError::LoadPemIdentityFailed)
171 }
172 }
173
174 #[allow(dead_code)]
176 pub fn name(&self) -> &str {
177 &self.name
178 }
179
180 pub fn identity_type(&self) -> IdentityType {
181 self.identity_type
182 }
183
184 pub fn display_linked_wallets(
186 logger: &Logger,
187 wallet_config: &Path,
188 ) -> Result<(), WalletConfigError> {
189 let config = Identity::load_wallet_config(wallet_config)?;
190 info!(
191 logger,
192 "This identity is connected to the following wallets:"
193 );
194 for (identity, map) in config.identities {
195 for (network, wallet) in map.networks {
196 info!(
197 logger,
198 " identity '{}' on network '{}' has wallet {}", identity, network, wallet
199 );
200 }
201 }
202 Ok(())
203 }
204
205 pub fn load_wallet_config(path: &Path) -> Result<WalletGlobalConfig, WalletConfigError> {
206 load_json_file(path).map_err(LoadWalletConfigFailed)
207 }
208
209 pub fn save_wallet_config(
210 path: &Path,
211 config: &WalletGlobalConfig,
212 ) -> Result<(), SaveWalletConfigError> {
213 ensure_parent_dir_exists(path)?;
214
215 save_json_file(path, &config)?;
216 Ok(())
217 }
218
219 fn rename_wallet_global_config_key(
220 original_identity: &str,
221 renamed_identity: &str,
222 wallet_path: PathBuf,
223 ) -> Result<(), RenameWalletGlobalConfigKeyError> {
224 Identity::load_wallet_config(&wallet_path)
225 .and_then(|mut config| {
226 let identities = &mut config.identities;
227 let v = identities
228 .remove(original_identity)
229 .unwrap_or(WalletNetworkMap {
230 networks: BTreeMap::new(),
231 });
232 identities.insert(renamed_identity.to_string(), v);
233 Identity::save_wallet_config(&wallet_path, &config)
234 .map_err(WalletConfigError::SaveWalletConfig)
235 })
236 .map_err(|err| {
237 RenameWalletFailed(
238 Box::new(original_identity.to_string()),
239 Box::new(renamed_identity.to_string()),
240 err,
241 )
242 })
243 }
244
245 pub fn map_wallets_to_renamed_identity(
247 project_temp_dir: Option<PathBuf>,
248 original_identity: &str,
249 renamed_identity: &str,
250 ) -> Result<(), MapWalletsToRenamedIdentityError> {
251 let persistent_wallet_path = get_user_dfx_config_dir()
252 .map_err(MapWalletsToRenamedIdentityError::GetConfigDirectoryFailed)?
253 .join("identity")
254 .join(original_identity)
255 .join(WALLET_CONFIG_FILENAME);
256 if persistent_wallet_path.exists() {
257 Identity::rename_wallet_global_config_key(
258 original_identity,
259 renamed_identity,
260 persistent_wallet_path,
261 )
262 .map_err(RenameWalletGlobalConfigKeyFailed)?;
263 }
264 if let Some(shared_local_wallet_path) = get_shared_wallet_config_path("local")? {
265 if shared_local_wallet_path.exists() {
266 Identity::rename_wallet_global_config_key(
267 original_identity,
268 renamed_identity,
269 shared_local_wallet_path,
270 )
271 .map_err(RenameWalletGlobalConfigKeyFailed)?;
272 }
273 }
274 if let Some(temp_dir) = project_temp_dir {
275 let local_wallet_path = temp_dir.join("local").join(WALLET_CONFIG_FILENAME);
276 if local_wallet_path.exists() {
277 Identity::rename_wallet_global_config_key(
278 original_identity,
279 renamed_identity,
280 local_wallet_path,
281 )
282 .map_err(RenameWalletGlobalConfigKeyFailed)?;
283 }
284 }
285 Ok(())
286 }
287}
288
289impl ic_agent::Identity for Identity {
290 fn sender(&self) -> Result<Principal, String> {
291 self.inner.sender()
292 }
293
294 fn public_key(&self) -> Option<Vec<u8>> {
295 self.inner.public_key()
296 }
297
298 fn delegation_chain(&self) -> Vec<SignedDelegation> {
299 self.inner.delegation_chain()
300 }
301
302 fn sign(&self, content: &EnvelopeContent) -> Result<Signature, String> {
303 self.inner.sign(content)
304 }
305
306 fn sign_arbitrary(&self, content: &[u8]) -> Result<Signature, String> {
307 self.inner.sign_arbitrary(content)
308 }
309
310 fn sign_delegation(&self, content: &Delegation) -> Result<Signature, String> {
311 self.inner.sign_delegation(content)
312 }
313}
314
315impl AsRef<Identity> for Identity {
316 fn as_ref(&self) -> &Identity {
317 self
318 }
319}
320
321#[derive(Serialize, Copy, Clone, Debug, PartialEq, Eq)]
322#[serde(rename_all = "kebab-case")]
323pub enum IdentityType {
324 Keyring,
325 Plaintext,
326 EncryptedLocal,
327 Hsm,
328 Anonymous,
329}
330
331#[derive(Debug, PartialEq, Eq, Copy, Clone)]
332pub enum CallSender {
333 SelectedId,
334 Impersonate(Principal),
335 Wallet(Principal),
336}
337
338impl CallSender {
341 pub fn from(
342 wallet_principal_or_identity_name: &Option<String>,
343 network: &NetworkDescriptor,
344 ) -> Result<Self, CallSenderFromWalletError> {
345 let sender = if let Some(s) = wallet_principal_or_identity_name {
346 match Principal::from_text(s) {
347 Ok(principal) => CallSender::Wallet(principal),
348 Err(principal_err) => match wallet_canister_id(network, s) {
349 Ok(Some(principal)) => CallSender::Wallet(principal),
350 Ok(None) => {
351 return Err(ParsePrincipalFromIdFailedAndNoWallet(
352 s.clone(),
353 principal_err,
354 ));
355 }
356 Err(wallet_err) => {
357 return Err(ParsePrincipalFromIdFailedAndGetWalletCanisterIdFailed(
358 s.clone(),
359 principal_err,
360 wallet_err,
361 ));
362 }
363 },
364 }
365 } else {
366 CallSender::SelectedId
367 };
368 Ok(sender)
369 }
370}