mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
Compare commits
7 Commits
rust-v0.11
...
greg/macos
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4232b4c070 | ||
|
|
2e4757d37d | ||
|
|
e95658e7bf | ||
|
|
a4c075cbe2 | ||
|
|
fe12ea7350 | ||
|
|
de57f51afb | ||
|
|
e46830c2a9 |
4
.github/actions/macos-code-sign/README.md
vendored
Normal file
4
.github/actions/macos-code-sign/README.md
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# macos-code-sign
|
||||
|
||||
This action signs the default Rust macOS release binaries and applies the shared keychain
|
||||
entitlement plist to those binaries during codesign.
|
||||
35
.github/actions/macos-code-sign/action.yml
vendored
35
.github/actions/macos-code-sign/action.yml
vendored
@@ -117,8 +117,6 @@ runs:
|
||||
- name: Sign macOS binaries
|
||||
if: ${{ inputs.sign-binaries == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
TARGET: ${{ inputs.target }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -132,16 +130,35 @@ runs:
|
||||
keychain_args+=(--keychain "${APPLE_CODESIGN_KEYCHAIN}")
|
||||
fi
|
||||
|
||||
entitlements_file="${GITHUB_WORKSPACE}/.github/macos/codex-shared-keychain.entitlements"
|
||||
if [[ ! -f "$entitlements_file" ]]; then
|
||||
echo "Entitlements file $entitlements_file not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for binary in codex codex-responses-api-proxy; do
|
||||
path="codex-rs/target/${TARGET}/release/${binary}"
|
||||
codesign --force --options runtime --timestamp --sign "$APPLE_CODESIGN_IDENTITY" "${keychain_args[@]}" "$path"
|
||||
path="codex-rs/target/${{ inputs.target }}/release/${binary}"
|
||||
if [[ ! -e "$path" ]]; then
|
||||
echo "Sign path $path not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
codesign_args=(
|
||||
--force
|
||||
--options runtime
|
||||
--timestamp
|
||||
--sign "$APPLE_CODESIGN_IDENTITY"
|
||||
--entitlements "$entitlements_file"
|
||||
)
|
||||
codesign_args+=("${keychain_args[@]}")
|
||||
|
||||
codesign "${codesign_args[@]}" "$path"
|
||||
done
|
||||
|
||||
- name: Notarize macOS binaries
|
||||
if: ${{ inputs.sign-binaries == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
TARGET: ${{ inputs.target }}
|
||||
APPLE_NOTARIZATION_KEY_P8: ${{ inputs.apple-notarization-key-p8 }}
|
||||
APPLE_NOTARIZATION_KEY_ID: ${{ inputs.apple-notarization-key-id }}
|
||||
APPLE_NOTARIZATION_ISSUER_ID: ${{ inputs.apple-notarization-issuer-id }}
|
||||
@@ -166,7 +183,7 @@ runs:
|
||||
|
||||
notarize_binary() {
|
||||
local binary="$1"
|
||||
local source_path="codex-rs/target/${TARGET}/release/${binary}"
|
||||
local source_path="codex-rs/target/${{ inputs.target }}/release/${binary}"
|
||||
local archive_path="${RUNNER_TEMP}/${binary}.zip"
|
||||
|
||||
if [[ ! -f "$source_path" ]]; then
|
||||
@@ -187,7 +204,6 @@ runs:
|
||||
if: ${{ inputs.sign-dmg == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
TARGET: ${{ inputs.target }}
|
||||
APPLE_NOTARIZATION_KEY_P8: ${{ inputs.apple-notarization-key-p8 }}
|
||||
APPLE_NOTARIZATION_KEY_ID: ${{ inputs.apple-notarization-key-id }}
|
||||
APPLE_NOTARIZATION_ISSUER_ID: ${{ inputs.apple-notarization-issuer-id }}
|
||||
@@ -210,8 +226,7 @@ runs:
|
||||
|
||||
source "$GITHUB_ACTION_PATH/notary_helpers.sh"
|
||||
|
||||
dmg_name="codex-${TARGET}.dmg"
|
||||
dmg_path="codex-rs/target/${TARGET}/release/${dmg_name}"
|
||||
dmg_path="codex-rs/target/${{ inputs.target }}/release/codex-${{ inputs.target }}.dmg"
|
||||
|
||||
if [[ ! -f "$dmg_path" ]]; then
|
||||
echo "dmg $dmg_path not found"
|
||||
@@ -224,7 +239,7 @@ runs:
|
||||
fi
|
||||
|
||||
codesign --force --timestamp --sign "$APPLE_CODESIGN_IDENTITY" "${keychain_args[@]}" "$dmg_path"
|
||||
notarize_submission "$dmg_name" "$dmg_path" "$notary_key_path"
|
||||
notarize_submission "codex-${{ inputs.target }}.dmg" "$dmg_path" "$notary_key_path"
|
||||
xcrun stapler staple "$dmg_path"
|
||||
|
||||
- name: Remove signing keychain
|
||||
|
||||
10
.github/macos/codex-shared-keychain.entitlements
vendored
Normal file
10
.github/macos/codex-shared-keychain.entitlements
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>2DC432GLL2.com.openai.codex.shared</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
4
codex-rs/Cargo.lock
generated
4
codex-rs/Cargo.lock
generated
@@ -1655,6 +1655,7 @@ dependencies = [
|
||||
"codex-core",
|
||||
"codex-exec",
|
||||
"codex-execpolicy",
|
||||
"codex-keyring-store",
|
||||
"codex-login",
|
||||
"codex-mcp-server",
|
||||
"codex-protocol",
|
||||
@@ -2101,7 +2102,10 @@ dependencies = [
|
||||
name = "codex-keyring-store"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"keyring",
|
||||
"security-framework 3.5.1",
|
||||
"security-framework-sys",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ codex-config = { workspace = true }
|
||||
codex-core = { workspace = true }
|
||||
codex-exec = { workspace = true }
|
||||
codex-execpolicy = { workspace = true }
|
||||
codex-keyring-store = { workspace = true }
|
||||
codex-login = { workspace = true }
|
||||
codex-mcp-server = { workspace = true }
|
||||
codex-protocol = { workspace = true }
|
||||
|
||||
@@ -21,6 +21,7 @@ use codex_exec::Cli as ExecCli;
|
||||
use codex_exec::Command as ExecCommand;
|
||||
use codex_exec::ReviewArgs;
|
||||
use codex_execpolicy::ExecPolicyCheckCommand;
|
||||
use codex_keyring_store::migrate_existing_codex_items_to_access_group;
|
||||
use codex_responses_api_proxy::Args as ResponsesApiProxyArgs;
|
||||
use codex_state::StateRuntime;
|
||||
use codex_state::state_db_path;
|
||||
@@ -601,6 +602,14 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
root_config_overrides.raw_overrides.extend(toggle_overrides);
|
||||
let root_remote = remote.remote;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
if let Err(error) = migrate_existing_codex_items_to_access_group() {
|
||||
tracing::warn!(
|
||||
error = %error,
|
||||
"failed to migrate existing Codex keychain items into the shared macOS access group"
|
||||
);
|
||||
}
|
||||
|
||||
match subcommand {
|
||||
None => {
|
||||
prepend_config_flags(
|
||||
|
||||
@@ -15,7 +15,10 @@ tracing = { workspace = true }
|
||||
keyring = { workspace = true, features = ["linux-native-async-persistent"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.10"
|
||||
keyring = { workspace = true, features = ["apple-native"] }
|
||||
security-framework = { version = "3", features = ["OSX_10_15"] }
|
||||
security-framework-sys = "2.15"
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
keyring = { workspace = true, features = ["windows-native"] }
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
use keyring::Entry;
|
||||
use keyring::Error as KeyringError;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::fmt::Debug;
|
||||
use tracing::trace;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
use keyring::Entry;
|
||||
|
||||
pub fn migrate_existing_codex_items_to_access_group() -> Result<(), CredentialStoreError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CredentialStoreError {
|
||||
Other(KeyringError),
|
||||
@@ -38,6 +47,89 @@ impl fmt::Display for CredentialStoreError {
|
||||
|
||||
impl Error for CredentialStoreError {}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn load_password(service: &str, account: &str) -> Result<Option<String>, CredentialStoreError> {
|
||||
macos::load_password(service, account).map_err(|error| {
|
||||
trace!(
|
||||
"keyring.load macos native error, service={service}, account={account}, error={error}"
|
||||
);
|
||||
CredentialStoreError::new(KeyringError::PlatformFailure(Box::new(error)))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn load_password(service: &str, account: &str) -> Result<Option<String>, CredentialStoreError> {
|
||||
let entry = Entry::new(service, account).map_err(CredentialStoreError::new)?;
|
||||
match entry.get_password() {
|
||||
Ok(password) => {
|
||||
trace!("keyring.load success, service={service}, account={account}");
|
||||
Ok(Some(password))
|
||||
}
|
||||
Err(keyring::Error::NoEntry) => {
|
||||
trace!("keyring.load no entry, service={service}, account={account}");
|
||||
Ok(None)
|
||||
}
|
||||
Err(error) => {
|
||||
trace!("keyring.load error, service={service}, account={account}, error={error}");
|
||||
Err(CredentialStoreError::new(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn save_password(service: &str, account: &str, value: &str) -> Result<(), CredentialStoreError> {
|
||||
macos::save_password(service, account, value).map_err(|error| {
|
||||
trace!(
|
||||
"keyring.save macos native error, service={service}, account={account}, error={error}"
|
||||
);
|
||||
CredentialStoreError::new(KeyringError::PlatformFailure(Box::new(error)))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn save_password(service: &str, account: &str, value: &str) -> Result<(), CredentialStoreError> {
|
||||
let entry = Entry::new(service, account).map_err(CredentialStoreError::new)?;
|
||||
match entry.set_password(value) {
|
||||
Ok(()) => {
|
||||
trace!("keyring.save success, service={service}, account={account}");
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => {
|
||||
trace!("keyring.save error, service={service}, account={account}, error={error}");
|
||||
Err(CredentialStoreError::new(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn delete_password(service: &str, account: &str) -> Result<bool, CredentialStoreError> {
|
||||
macos::delete_password(service, account).map_err(|error| {
|
||||
trace!(
|
||||
"keyring.delete macos native error, service={service}, account={account}, error={error}"
|
||||
);
|
||||
CredentialStoreError::new(KeyringError::PlatformFailure(Box::new(error)))
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
fn delete_password(service: &str, account: &str) -> Result<bool, CredentialStoreError> {
|
||||
let entry = Entry::new(service, account).map_err(CredentialStoreError::new)?;
|
||||
match entry.delete_credential() {
|
||||
Ok(()) => {
|
||||
trace!("keyring.delete success, service={service}, account={account}");
|
||||
Ok(true)
|
||||
}
|
||||
Err(keyring::Error::NoEntry) => {
|
||||
trace!("keyring.delete no entry, service={service}, account={account}");
|
||||
Ok(false)
|
||||
}
|
||||
Err(error) => {
|
||||
trace!("keyring.delete error, service={service}, account={account}, error={error}");
|
||||
Err(CredentialStoreError::new(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared credential store abstraction for keyring-backed implementations.
|
||||
pub trait KeyringStore: Debug + Send + Sync {
|
||||
fn load(&self, service: &str, account: &str) -> Result<Option<String>, CredentialStoreError>;
|
||||
@@ -51,21 +143,7 @@ pub struct DefaultKeyringStore;
|
||||
impl KeyringStore for DefaultKeyringStore {
|
||||
fn load(&self, service: &str, account: &str) -> Result<Option<String>, CredentialStoreError> {
|
||||
trace!("keyring.load start, service={service}, account={account}");
|
||||
let entry = Entry::new(service, account).map_err(CredentialStoreError::new)?;
|
||||
match entry.get_password() {
|
||||
Ok(password) => {
|
||||
trace!("keyring.load success, service={service}, account={account}");
|
||||
Ok(Some(password))
|
||||
}
|
||||
Err(keyring::Error::NoEntry) => {
|
||||
trace!("keyring.load no entry, service={service}, account={account}");
|
||||
Ok(None)
|
||||
}
|
||||
Err(error) => {
|
||||
trace!("keyring.load error, service={service}, account={account}, error={error}");
|
||||
Err(CredentialStoreError::new(error))
|
||||
}
|
||||
}
|
||||
load_password(service, account)
|
||||
}
|
||||
|
||||
fn save(&self, service: &str, account: &str, value: &str) -> Result<(), CredentialStoreError> {
|
||||
@@ -73,36 +151,12 @@ impl KeyringStore for DefaultKeyringStore {
|
||||
"keyring.save start, service={service}, account={account}, value_len={}",
|
||||
value.len()
|
||||
);
|
||||
let entry = Entry::new(service, account).map_err(CredentialStoreError::new)?;
|
||||
match entry.set_password(value) {
|
||||
Ok(()) => {
|
||||
trace!("keyring.save success, service={service}, account={account}");
|
||||
Ok(())
|
||||
}
|
||||
Err(error) => {
|
||||
trace!("keyring.save error, service={service}, account={account}, error={error}");
|
||||
Err(CredentialStoreError::new(error))
|
||||
}
|
||||
}
|
||||
save_password(service, account, value)
|
||||
}
|
||||
|
||||
fn delete(&self, service: &str, account: &str) -> Result<bool, CredentialStoreError> {
|
||||
trace!("keyring.delete start, service={service}, account={account}");
|
||||
let entry = Entry::new(service, account).map_err(CredentialStoreError::new)?;
|
||||
match entry.delete_credential() {
|
||||
Ok(()) => {
|
||||
trace!("keyring.delete success, service={service}, account={account}");
|
||||
Ok(true)
|
||||
}
|
||||
Err(keyring::Error::NoEntry) => {
|
||||
trace!("keyring.delete no entry, service={service}, account={account}");
|
||||
Ok(false)
|
||||
}
|
||||
Err(error) => {
|
||||
trace!("keyring.delete error, service={service}, account={account}, error={error}");
|
||||
Err(CredentialStoreError::new(error))
|
||||
}
|
||||
}
|
||||
delete_password(service, account)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
399
codex-rs/keyring-store/src/macos.rs
Normal file
399
codex-rs/keyring-store/src/macos.rs
Normal file
@@ -0,0 +1,399 @@
|
||||
use security_framework::base::Error as SecurityError;
|
||||
use security_framework::passwords::PasswordOptions;
|
||||
use security_framework::passwords::delete_generic_password_options;
|
||||
use security_framework::passwords::generic_password;
|
||||
use security_framework::passwords::set_generic_password_options;
|
||||
use security_framework_sys::base::errSecItemNotFound;
|
||||
use std::fmt;
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct MacOsAccessGroupError {
|
||||
details: String,
|
||||
}
|
||||
|
||||
impl MacOsAccessGroupError {
|
||||
fn new(details: impl Into<String>) -> Self {
|
||||
Self {
|
||||
details: details.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for MacOsAccessGroupError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"failed to access macOS keychain item in shared access group: {}",
|
||||
self.details
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for MacOsAccessGroupError {}
|
||||
|
||||
const SHARED_ACCESS_GROUP: &str = "2DC432GLL2.com.openai.codex.shared";
|
||||
|
||||
pub(crate) fn load_password(
|
||||
service: &str,
|
||||
account: &str,
|
||||
) -> Result<Option<String>, MacOsAccessGroupError> {
|
||||
load_password_with_store(&NativePasswordStore, service, account)
|
||||
}
|
||||
|
||||
pub(crate) fn save_password(
|
||||
service: &str,
|
||||
account: &str,
|
||||
value: &str,
|
||||
) -> Result<(), MacOsAccessGroupError> {
|
||||
save_password_with_store(&NativePasswordStore, service, account, value)
|
||||
}
|
||||
|
||||
pub(crate) fn delete_password(service: &str, account: &str) -> Result<bool, MacOsAccessGroupError> {
|
||||
delete_password_with_store(&NativePasswordStore, service, account)
|
||||
}
|
||||
|
||||
trait PasswordStore {
|
||||
fn load_shared_password_bytes(
|
||||
&self,
|
||||
service: &str,
|
||||
account: &str,
|
||||
) -> Result<Option<Vec<u8>>, MacOsAccessGroupError>;
|
||||
|
||||
fn load_legacy_password_bytes(
|
||||
&self,
|
||||
service: &str,
|
||||
account: &str,
|
||||
) -> Result<Option<Vec<u8>>, MacOsAccessGroupError>;
|
||||
|
||||
fn save_shared_password_bytes(
|
||||
&self,
|
||||
service: &str,
|
||||
account: &str,
|
||||
value: &[u8],
|
||||
) -> Result<(), MacOsAccessGroupError>;
|
||||
|
||||
fn delete_shared_password(
|
||||
&self,
|
||||
service: &str,
|
||||
account: &str,
|
||||
) -> Result<bool, MacOsAccessGroupError>;
|
||||
|
||||
fn delete_legacy_password(
|
||||
&self,
|
||||
service: &str,
|
||||
account: &str,
|
||||
) -> Result<bool, MacOsAccessGroupError>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct NativePasswordStore;
|
||||
|
||||
impl PasswordStore for NativePasswordStore {
|
||||
fn load_shared_password_bytes(
|
||||
&self,
|
||||
service: &str,
|
||||
account: &str,
|
||||
) -> Result<Option<Vec<u8>>, MacOsAccessGroupError> {
|
||||
load_password_bytes(
|
||||
shared_password_options(service, account),
|
||||
"read shared generic password",
|
||||
)
|
||||
}
|
||||
|
||||
fn load_legacy_password_bytes(
|
||||
&self,
|
||||
service: &str,
|
||||
account: &str,
|
||||
) -> Result<Option<Vec<u8>>, MacOsAccessGroupError> {
|
||||
load_password_bytes(
|
||||
legacy_password_options(service, account),
|
||||
"read legacy generic password",
|
||||
)
|
||||
}
|
||||
|
||||
fn save_shared_password_bytes(
|
||||
&self,
|
||||
service: &str,
|
||||
account: &str,
|
||||
value: &[u8],
|
||||
) -> Result<(), MacOsAccessGroupError> {
|
||||
set_generic_password_options(value, shared_password_options(service, account))
|
||||
.map_err(|error| wrap_security_error("write shared generic password", error))
|
||||
}
|
||||
|
||||
fn delete_shared_password(
|
||||
&self,
|
||||
service: &str,
|
||||
account: &str,
|
||||
) -> Result<bool, MacOsAccessGroupError> {
|
||||
delete_password_options(
|
||||
shared_password_options(service, account),
|
||||
"delete shared generic password",
|
||||
)
|
||||
}
|
||||
|
||||
fn delete_legacy_password(
|
||||
&self,
|
||||
service: &str,
|
||||
account: &str,
|
||||
) -> Result<bool, MacOsAccessGroupError> {
|
||||
delete_password_options(
|
||||
legacy_password_options(service, account),
|
||||
"delete legacy generic password",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn load_password_with_store(
|
||||
store: &impl PasswordStore,
|
||||
service: &str,
|
||||
account: &str,
|
||||
) -> Result<Option<String>, MacOsAccessGroupError> {
|
||||
if let Some(password) = store.load_shared_password_bytes(service, account)? {
|
||||
return decode_password(password, service, account).map(Some);
|
||||
}
|
||||
|
||||
let Some(password) = store.load_legacy_password_bytes(service, account)? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if let Err(error) = store.save_shared_password_bytes(service, account, &password) {
|
||||
warn!(
|
||||
error = %error,
|
||||
service,
|
||||
account,
|
||||
"failed to backfill legacy macOS keychain item into the shared access group"
|
||||
);
|
||||
}
|
||||
|
||||
decode_password(password, service, account).map(Some)
|
||||
}
|
||||
|
||||
fn save_password_with_store(
|
||||
store: &impl PasswordStore,
|
||||
service: &str,
|
||||
account: &str,
|
||||
value: &str,
|
||||
) -> Result<(), MacOsAccessGroupError> {
|
||||
store.save_shared_password_bytes(service, account, value.as_bytes())
|
||||
}
|
||||
|
||||
fn delete_password_with_store(
|
||||
store: &impl PasswordStore,
|
||||
service: &str,
|
||||
account: &str,
|
||||
) -> Result<bool, MacOsAccessGroupError> {
|
||||
let shared_removed = store.delete_shared_password(service, account)?;
|
||||
let legacy_removed = store.delete_legacy_password(service, account)?;
|
||||
Ok(shared_removed || legacy_removed)
|
||||
}
|
||||
|
||||
fn decode_password(
|
||||
password: Vec<u8>,
|
||||
service: &str,
|
||||
account: &str,
|
||||
) -> Result<String, MacOsAccessGroupError> {
|
||||
String::from_utf8(password).map_err(|error| {
|
||||
MacOsAccessGroupError::new(format!(
|
||||
"decode password for service={service}, account={account}: {error}"
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
fn load_password_bytes(
|
||||
options: PasswordOptions,
|
||||
action: &str,
|
||||
) -> Result<Option<Vec<u8>>, MacOsAccessGroupError> {
|
||||
match generic_password(options) {
|
||||
Ok(password) => Ok(Some(password)),
|
||||
Err(error) if error.code() == errSecItemNotFound => Ok(None),
|
||||
Err(error) => Err(wrap_security_error(action, error)),
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_password_options(
|
||||
options: PasswordOptions,
|
||||
action: &str,
|
||||
) -> Result<bool, MacOsAccessGroupError> {
|
||||
match delete_generic_password_options(options) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(error) if error.code() == errSecItemNotFound => Ok(false),
|
||||
Err(error) => Err(wrap_security_error(action, error)),
|
||||
}
|
||||
}
|
||||
|
||||
fn shared_password_options(service: &str, account: &str) -> PasswordOptions {
|
||||
let mut options = PasswordOptions::new_generic_password(service, account);
|
||||
options.use_protected_keychain();
|
||||
options.set_access_group(SHARED_ACCESS_GROUP);
|
||||
options
|
||||
}
|
||||
|
||||
fn legacy_password_options(service: &str, account: &str) -> PasswordOptions {
|
||||
PasswordOptions::new_generic_password(service, account)
|
||||
}
|
||||
|
||||
fn wrap_security_error(action: &str, error: SecurityError) -> MacOsAccessGroupError {
|
||||
MacOsAccessGroupError::new(format!("{action}: {error}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::cell::Cell;
|
||||
use std::cell::RefCell;
|
||||
|
||||
#[derive(Default)]
|
||||
struct MockPasswordStore {
|
||||
shared: RefCell<Option<Vec<u8>>>,
|
||||
legacy: RefCell<Option<Vec<u8>>>,
|
||||
save_shared_error: RefCell<Option<MacOsAccessGroupError>>,
|
||||
shared_writes: Cell<usize>,
|
||||
}
|
||||
|
||||
impl MockPasswordStore {
|
||||
fn with_shared_password(self, value: &str) -> Self {
|
||||
self.shared.replace(Some(value.as_bytes().to_vec()));
|
||||
self
|
||||
}
|
||||
|
||||
fn with_legacy_password(self, value: &str) -> Self {
|
||||
self.legacy.replace(Some(value.as_bytes().to_vec()));
|
||||
self
|
||||
}
|
||||
|
||||
fn with_save_shared_error(self, details: &str) -> Self {
|
||||
self.save_shared_error
|
||||
.replace(Some(MacOsAccessGroupError::new(details)));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PasswordStore for MockPasswordStore {
|
||||
fn load_shared_password_bytes(
|
||||
&self,
|
||||
_service: &str,
|
||||
_account: &str,
|
||||
) -> Result<Option<Vec<u8>>, MacOsAccessGroupError> {
|
||||
Ok(self.shared.borrow().clone())
|
||||
}
|
||||
|
||||
fn load_legacy_password_bytes(
|
||||
&self,
|
||||
_service: &str,
|
||||
_account: &str,
|
||||
) -> Result<Option<Vec<u8>>, MacOsAccessGroupError> {
|
||||
Ok(self.legacy.borrow().clone())
|
||||
}
|
||||
|
||||
fn save_shared_password_bytes(
|
||||
&self,
|
||||
_service: &str,
|
||||
_account: &str,
|
||||
value: &[u8],
|
||||
) -> Result<(), MacOsAccessGroupError> {
|
||||
if let Some(error) = self.save_shared_error.borrow().clone() {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
self.shared.replace(Some(value.to_vec()));
|
||||
self.shared_writes.set(self.shared_writes.get() + 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_shared_password(
|
||||
&self,
|
||||
_service: &str,
|
||||
_account: &str,
|
||||
) -> Result<bool, MacOsAccessGroupError> {
|
||||
Ok(self.shared.borrow_mut().take().is_some())
|
||||
}
|
||||
|
||||
fn delete_legacy_password(
|
||||
&self,
|
||||
_service: &str,
|
||||
_account: &str,
|
||||
) -> Result<bool, MacOsAccessGroupError> {
|
||||
Ok(self.legacy.borrow_mut().take().is_some())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_prefers_shared_password_when_present() {
|
||||
let store = MockPasswordStore::default()
|
||||
.with_shared_password("shared-value")
|
||||
.with_legacy_password("legacy-value");
|
||||
|
||||
let password = load_password_with_store(&store, "Codex Auth", "test-account")
|
||||
.unwrap()
|
||||
.expect("expected password");
|
||||
|
||||
assert_eq!(password, "shared-value");
|
||||
assert_eq!(
|
||||
store.shared.borrow().clone(),
|
||||
Some(b"shared-value".to_vec())
|
||||
);
|
||||
assert_eq!(
|
||||
store.legacy.borrow().clone(),
|
||||
Some(b"legacy-value".to_vec())
|
||||
);
|
||||
assert_eq!(store.shared_writes.get(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_backfills_shared_password_from_legacy_without_deleting_legacy() {
|
||||
let store = MockPasswordStore::default().with_legacy_password("legacy-value");
|
||||
|
||||
let password = load_password_with_store(&store, "Codex Auth", "test-account")
|
||||
.unwrap()
|
||||
.expect("expected password");
|
||||
|
||||
assert_eq!(password, "legacy-value");
|
||||
assert_eq!(
|
||||
store.shared.borrow().clone(),
|
||||
Some(b"legacy-value".to_vec())
|
||||
);
|
||||
assert_eq!(
|
||||
store.legacy.borrow().clone(),
|
||||
Some(b"legacy-value".to_vec())
|
||||
);
|
||||
assert_eq!(store.shared_writes.get(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_returns_legacy_password_when_shared_backfill_fails() {
|
||||
let store = MockPasswordStore::default()
|
||||
.with_legacy_password("legacy-value")
|
||||
.with_save_shared_error("shared write failed");
|
||||
|
||||
let password = load_password_with_store(&store, "Codex Auth", "test-account")
|
||||
.unwrap()
|
||||
.expect("expected password");
|
||||
|
||||
assert_eq!(password, "legacy-value");
|
||||
assert_eq!(store.shared.borrow().clone(), None);
|
||||
assert_eq!(
|
||||
store.legacy.borrow().clone(),
|
||||
Some(b"legacy-value".to_vec())
|
||||
);
|
||||
assert_eq!(store.shared_writes.get(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_writes_only_to_shared_password() {
|
||||
let store = MockPasswordStore::default().with_legacy_password("legacy-value");
|
||||
|
||||
save_password_with_store(&store, "Codex Auth", "test-account", "shared-value").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
store.shared.borrow().clone(),
|
||||
Some(b"shared-value".to_vec())
|
||||
);
|
||||
assert_eq!(
|
||||
store.legacy.borrow().clone(),
|
||||
Some(b"legacy-value".to_vec())
|
||||
);
|
||||
assert_eq!(store.shared_writes.get(), 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user