Skip to content

Commit affc2d6

Browse files
fix: document macos keychain issues (#4309)
1 parent 09ea9e8 commit affc2d6

File tree

11 files changed

+95
-12
lines changed

11 files changed

+95
-12
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ the `/http_gateway` endpoint, you should instead use `dfx info pocketic-config-p
4040

4141
### feat: add dfx native support for aarch64-Darwin
4242

43-
Add dfx native support for aarch64-Darwin.
43+
Add dfx native support for aarch64-Darwin. Using it may require editing your identities. See the [migration guide](./docs/migration/dfx-0.28.0-migration-guide.md) for more information.
4444

4545
## Dependencies
4646

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# dfx 0.28.0 migration guide
2+
3+
## Keychain issues on macOS arm64
4+
5+
This release brings the first arm64 macOS version of `dfx`. On arm64 macOS, there is an issue that prevents `dfx` from accessing identities stored in the keychain if an x64 version of `dfx` also has access to it. In order to restore arm64 `dfx`'s access to identities, you will need to remove x64 `dfx`'s access.
6+
7+
You may want to export identities first using the previous version of dfx in case you make a mistake:
8+
9+
```
10+
dfx +<older-version> identity export <identity-name>
11+
```
12+
13+
First, open the "Keychain Access" application. You may see a popup telling you to try a different "Passwords" app instead, ignore it and open "Keychain Access".
14+
15+
Next, scroll through the "Passwords" section of the "login" keychain. If it is blank, try switching to a different keychain, and then switch back to "login". Find the two or more entries labeled `internet_computer_identities`.
16+
17+
![The Keychain Access app, on the passwords screen of the login keychain with an IC identity highlighted](images/dfx-0.28.0-keychain_access.png)
18+
19+
For each `internet_computer_identities` entry you see, double-click it and switch to the "Access Control" tab. If you see `dfx` on that list, click it and then click the 'minus' button to remove it. If `dfx` shows up multiple times, remove each such entry. Finally, click "Save Changes".
20+
21+
![A properties window for an individual password, showing the dfx entry on the Access Control screen.](images/dfx-0.28.0-keychain_access_focused.png)
22+
23+
After these steps, `dfx` should no longer give you this error.
24+
25+
If your workflow requires using multiple versions of dfx, note that this error will be re-created each time you "Always Allow" an x64 or pre-0.28 version of dfx to access your identities. You may want to copy (via export/import) identities so post-0.28 versions use different identities from pre-0.28 versions; that way they will not step on each other's toes.
Loading
Loading

src/dfx-core/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ tiny-bip39 = "1.0.0"
5252
time = { workspace = true, features = ["serde", "serde-human-readable"] }
5353
url.workspace = true
5454

55+
[target.'cfg(target_os = "macos")'.dependencies]
56+
security-framework = "3"
57+
5558
[dev-dependencies]
5659
futures.workspace = true
5760
proptest = "1.0"

src/dfx-core/src/error/identity.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::error::fs::{
33
ReadFileError, ReadPermissionsError, RemoveDirectoryAndContentsError, RemoveDirectoryError,
44
RemoveFileError, RenameError, SetPermissionsError, WriteFileError,
55
};
6+
use crate::error::keyring::KeyringMaintenanceError;
67
use crate::error::{
78
config::ConfigError,
89
encryption::EncryptionError,
@@ -43,6 +44,9 @@ pub enum ConvertMnemonicToKeyError {
4344
pub enum CreateIdentityConfigError {
4445
#[error("Failed to generate a fresh encryption configuration")]
4546
GenerateFreshEncryptionConfigurationFailed(#[source] EncryptionError),
47+
48+
#[error("Failed to check for keyring availability")]
49+
KeyringAvailabilityCheckFailed(#[source] KeyringMaintenanceError),
4650
}
4751

4852
#[derive(Error, Debug)]

src/dfx-core/src/error/keyring.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,17 @@ pub enum KeyringError {
2929

3030
#[error("Failed to set password for keyring")]
3131
SetPasswordFailed(#[source] keyring::Error),
32+
33+
#[error(transparent)]
34+
MaintenanceRequired(#[from] KeyringMaintenanceError),
3235
}
36+
37+
#[derive(Error, Debug)]
38+
#[error("\
39+
A macOS issue prevents arm64 versions of dfx from accessing your identities while an x64 version of dfx also has access.
40+
41+
You will need to go into Keychain Access and remove dfx from the 'Access Control' tab of all 'internet_computer_identities' keys.
42+
43+
For more information, see the dfx 0.28.0 migration guide: https://github.com/dfinity/sdk/blob/0.28.1/docs/migration/dfx-0.28.0-migration-guide.md
44+
")]
45+
pub struct KeyringMaintenanceError;

src/dfx-core/src/identity/identity_manager.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use crate::error::identity::{
88
ConvertMnemonicToKeyError,
99
ConvertMnemonicToKeyError::DeriveExtendedKeyFromPathFailed,
1010
CreateIdentityConfigError,
11-
CreateIdentityConfigError::GenerateFreshEncryptionConfigurationFailed,
11+
CreateIdentityConfigError::{
12+
GenerateFreshEncryptionConfigurationFailed, KeyringAvailabilityCheckFailed,
13+
},
1214
CreateNewIdentityError,
1315
CreateNewIdentityError::{
1416
ConvertSecretKeyToSec1PemFailed, CreateMnemonicFromPhraseFailed,
@@ -340,7 +342,9 @@ impl IdentityManager {
340342
} else {
341343
match mode {
342344
IdentityStorageMode::Keyring => {
343-
if keyring_mock::keyring_available(log) {
345+
if keyring_mock::keyring_available(log)
346+
.map_err(KeyringAvailabilityCheckFailed)?
347+
{
344348
Ok(IdentityConfiguration {
345349
keyring_identity_suffix: Some(String::from(name)),
346350
..Default::default()

src/dfx-core/src/identity/keyring_mock.rs

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
use super::TEMP_IDENTITY_PREFIX;
2-
use crate::error::keyring::KeyringError;
32
use crate::error::keyring::KeyringError::{
43
DecodePemFailed, DeletePasswordFailed, GetPasswordFailed, LoadMockKeyringFailed,
54
MockKeyNotFound, MockUnavailable, NewEntryFailed, SaveMockKeyringFailed, SetPasswordFailed,
65
};
6+
use crate::error::keyring::{KeyringError, KeyringMaintenanceError};
77
use crate::json::{load_json_file, save_json_file};
88
use keyring;
99
use serde::{Deserialize, Serialize};
@@ -75,7 +75,10 @@ pub fn load_pem_from_keyring(identity_name_suffix: &str) -> Result<Vec<u8>, Keyr
7575
KeyringMockMode::NoMock => {
7676
let entry = keyring::Entry::new(KEYRING_SERVICE_NAME, &keyring_identity_name)
7777
.map_err(NewEntryFailed)?;
78-
let encoded_pem = entry.get_password().map_err(GetPasswordFailed)?;
78+
let encoded_pem = entry
79+
.get_password()
80+
.handle_macos_acl_error()?
81+
.map_err(GetPasswordFailed)?;
7982
let pem = hex::decode(encoded_pem).map_err(DecodePemFailed)?;
8083
Ok(pem)
8184
}
@@ -104,6 +107,7 @@ pub fn write_pem_to_keyring(
104107
.map_err(NewEntryFailed)?;
105108
entry
106109
.set_password(&encoded_pem)
110+
.handle_macos_acl_error()?
107111
.map_err(SetPasswordFailed)?;
108112
Ok(())
109113
}
@@ -118,7 +122,7 @@ pub fn write_pem_to_keyring(
118122
}
119123

120124
/// Determines if keyring is available by trying to write a dummy entry.
121-
pub fn keyring_available(log: &Logger) -> bool {
125+
pub fn keyring_available(log: &Logger) -> Result<bool, KeyringMaintenanceError> {
122126
match KeyringMockMode::current_mode() {
123127
KeyringMockMode::NoMock => {
124128
trace!(log, "Checking for keyring availability.");
@@ -128,13 +132,16 @@ pub fn keyring_available(log: &Logger) -> bool {
128132
KEYRING_IDENTITY_PREFIX, TEMP_IDENTITY_PREFIX, "dummy"
129133
);
130134
if let Ok(entry) = keyring::Entry::new(KEYRING_SERVICE_NAME, &dummy_entry_name) {
131-
entry.set_password("dummy entry").is_ok()
135+
Ok(entry
136+
.set_password("dummy entry")
137+
.handle_macos_acl_error()?
138+
.is_ok())
132139
} else {
133-
false
140+
Ok(false)
134141
}
135142
}
136-
KeyringMockMode::MockReject => false,
137-
KeyringMockMode::MockAvailable => true,
143+
KeyringMockMode::MockReject => Ok(false),
144+
KeyringMockMode::MockAvailable => Ok(true),
138145
}
139146
}
140147

@@ -144,7 +151,7 @@ pub fn delete_pem_from_keyring(identity_name_suffix: &str) -> Result<(), Keyring
144151
KeyringMockMode::NoMock => {
145152
let entry = keyring::Entry::new(KEYRING_SERVICE_NAME, &keyring_identity_name)
146153
.map_err(NewEntryFailed)?;
147-
if entry.get_password().is_ok() {
154+
if entry.get_password().handle_macos_acl_error()?.is_ok() {
148155
entry.delete_credential().map_err(DeletePasswordFailed)?;
149156
}
150157
}
@@ -157,3 +164,24 @@ pub fn delete_pem_from_keyring(identity_name_suffix: &str) -> Result<(), Keyring
157164
}
158165
Ok(())
159166
}
167+
168+
trait KeyringResultExt: Sized {
169+
fn handle_macos_acl_error(self) -> Result<Self, KeyringMaintenanceError>;
170+
}
171+
172+
impl<T> KeyringResultExt for Result<T, keyring::Error> {
173+
fn handle_macos_acl_error(self) -> Result<Self, KeyringMaintenanceError> {
174+
match self {
175+
Ok(value) => Ok(Ok(value)),
176+
#[cfg(target_os = "macos")]
177+
Err(keyring::Error::PlatformFailure(err))
178+
if err
179+
.downcast_ref::<security_framework::base::Error>()
180+
.is_some_and(|err| err.code() == -67671) =>
181+
{
182+
Err(KeyringMaintenanceError)
183+
}
184+
Err(e) => Ok(Err(e)),
185+
}
186+
}
187+
}

0 commit comments

Comments
 (0)