diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 37ba37e4e1..babfe4712a 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -23099,7 +23099,7 @@ "node": ">=14.0.0" }, "optionalDependencies": { - "@zowe/secrets-for-zowe-sdk": "7.18.5" + "@zowe/secrets-for-zowe-sdk": "7.18.6" } }, "packages/cli/node_modules/brace-expansion": { @@ -23170,7 +23170,7 @@ }, "packages/secrets": { "name": "@zowe/secrets-for-zowe-sdk", - "version": "7.18.5", + "version": "7.18.6", "hasInstallScript": true, "license": "EPL-2.0", "devDependencies": { @@ -29664,7 +29664,7 @@ "@zowe/imperative": "5.18.1", "@zowe/perf-timing": "1.0.7", "@zowe/provisioning-for-zowe-sdk": "7.18.2", - "@zowe/secrets-for-zowe-sdk": "7.18.5", + "@zowe/secrets-for-zowe-sdk": "7.18.6", "@zowe/zos-console-for-zowe-sdk": "7.18.2", "@zowe/zos-files-for-zowe-sdk": "7.18.2", "@zowe/zos-jobs-for-zowe-sdk": "7.18.2", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 6a563fb015..775ce1c001 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to the Zowe CLI package will be documented in this file. +## Recent Changes + +- BugFix: Bump Secrets SDK to `7.18.6` to use `core-foundation-rs` instead of the now-archived `security-framework` crate, and to include the edge-case bug fix for Linux. + ## `7.18.5` - BugFix: Bump Secrets SDK to `7.18.5` to resolve build failures for FreeBSD users. diff --git a/packages/cli/package.json b/packages/cli/package.json index 96ab4282d9..7bc36ee6fd 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -93,7 +93,7 @@ "which": "^2.0.2" }, "optionalDependencies": { - "@zowe/secrets-for-zowe-sdk": "7.18.5" + "@zowe/secrets-for-zowe-sdk": "7.18.6" }, "engines": { "node": ">=14.0.0" diff --git a/packages/secrets/CHANGELOG.md b/packages/secrets/CHANGELOG.md index 020b0a8324..054df8cec3 100644 --- a/packages/secrets/CHANGELOG.md +++ b/packages/secrets/CHANGELOG.md @@ -2,6 +2,11 @@ All notable changes to the Zowe Secrets SDK package will be documented in this file. +## `7.18.6` + +- BugFix: Use `core-foundation-rs` instead of `security-framework` for macOS logic, as `security-framework` is now archived. [#1802](https://github.com/zowe/zowe-cli/issues/1802) +- BugFix: Resolve bug where `findCredentials` scenarios with one match causes a segmentation fault on Linux. + ## `7.18.5` - BugFix: Enable `KeyringError::Library` enum variant to fix building on FreeBSD targets. diff --git a/packages/secrets/package.json b/packages/secrets/package.json index 85a3fb9b77..d265ccde63 100644 --- a/packages/secrets/package.json +++ b/packages/secrets/package.json @@ -3,7 +3,7 @@ "description": "Credential management facilities for Imperative, Zowe CLI, and extenders.", "repository": "https://github.com/zowe/zowe-cli.git", "author": "Zowe", - "version": "7.18.5", + "version": "7.18.6", "homepage": "https://github.com/zowe/zowe-cli/tree/master/packages/secrets#readme", "bugs": { "url": "https://github.com/zowe/zowe-cli/issues" diff --git a/packages/secrets/src/keyring/Cargo.lock b/packages/secrets/src/keyring/Cargo.lock index 9894558af8..ca9f2a105d 100644 --- a/packages/secrets/src/keyring/Cargo.lock +++ b/packages/secrets/src/keyring/Cargo.lock @@ -11,24 +11,12 @@ dependencies = [ "memchr", ] -[[package]] -name = "anyhow" -version = "1.0.72" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" - [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - [[package]] name = "bitflags" version = "2.3.3" @@ -157,11 +145,10 @@ dependencies = [ [[package]] name = "gio" -version = "0.17.10" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6973e92937cf98689b6a054a9e56c657ed4ff76de925e36fc331a15f0c5d30a" +checksum = "57052f84e8e5999b258e8adf8f5f2af0ac69033864936b8b6838321db2f759b1" dependencies = [ - "bitflags 1.3.2", "futures-channel", "futures-core", "futures-io", @@ -177,9 +164,9 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.17.10" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ccf87c30a12c469b6d958950f6a9c09f2be20b7773f7e70d20b867fdf2628c3" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" dependencies = [ "glib-sys", "gobject-sys", @@ -190,11 +177,11 @@ dependencies = [ [[package]] name = "glib" -version = "0.17.10" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3fad45ba8d4d2cea612b432717e834f48031cd8853c8aaf43b2c79fec8d144b" +checksum = "1c316afb01ce8067c5eaab1fc4f2cd47dc21ce7b6296358605e2ffab23ccbd19" dependencies = [ - "bitflags 1.3.2", + "bitflags", "futures-channel", "futures-core", "futures-executor", @@ -213,24 +200,23 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.17.10" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca5c79337338391f1ab8058d6698125034ce8ef31b72a442437fa6c8580de26" +checksum = "f8da903822b136d42360518653fcf154455defc437d3e7a81475bf9a95ff1e47" dependencies = [ - "anyhow", "heck", "proc-macro-crate", "proc-macro-error", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.28", ] [[package]] name = "glib-sys" -version = "0.17.10" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d80aa6ea7bba0baac79222204aa786a6293078c210abe69ef1336911d4bdc4f0" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" dependencies = [ "libc", "system-deps", @@ -238,9 +224,9 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.17.10" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd34c3317740a6358ec04572c1bcfd3ac0b5b6529275fae255b237b314bb8062" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" dependencies = [ "glib-sys", "libc", @@ -274,14 +260,16 @@ name = "keyring" version = "1.0.0" dependencies = [ "cfg-if", + "core-foundation", + "core-foundation-sys", "gio", "glib", + "glib-sys", "libsecret", "libsecret-sys", "napi", "napi-build", "napi-derive", - "security-framework", "thiserror", "windows-sys", ] @@ -304,23 +292,21 @@ dependencies = [ [[package]] name = "libsecret" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accb700635d0b1b296d83c93fa5d112400168d04481db0ccca946293af9f0206" +checksum = "ac6fae6ebe590e06ef9d01b125e46b7d4c05ccbd5961f12b4aefe2ecd010220f" dependencies = [ - "bitflags 1.3.2", "gio", "glib", "libc", "libsecret-sys", - "once_cell", ] [[package]] name = "libsecret-sys" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b06012ca123d27ccceffa112d0f930c23eb549a2447dc710e99f5dc2d1040b2" +checksum = "9b716fc5e1c82eb0d28665882628382ab0e0a156a6d73580e33f0ac6ac8d2540" dependencies = [ "gio-sys", "glib-sys", @@ -342,7 +328,7 @@ version = "2.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ede2d12cd6fce44da537a4be1f5510c73be2506c2e32dfaaafd1f36968f3a0e" dependencies = [ - "bitflags 2.3.3", + "bitflags", "ctor", "napi-derive", "napi-sys", @@ -498,29 +484,6 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" -[[package]] -name = "security-framework" -version = "2.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "semver" version = "1.0.18" diff --git a/packages/secrets/src/keyring/Cargo.toml b/packages/secrets/src/keyring/Cargo.toml index ba973e38ef..24829da005 100644 --- a/packages/secrets/src/keyring/Cargo.toml +++ b/packages/secrets/src/keyring/Cargo.toml @@ -27,13 +27,15 @@ features = [ version = "0.48.0" [target.'cfg(target_os = "macos")'.dependencies] -security-framework = "2.9.1" +core-foundation = "0.9.3" +core-foundation-sys = "0.8.4" [target.'cfg(any(target_os = "freebsd", target_os = "linux"))'.dependencies] -glib = "0.17.10" -gio = "0.17.10" -libsecret = "0.3.0" -libsecret-sys = "0.3.0" +glib = "0.18.2" +glib-sys = "0.18.1" +gio = "0.18.2" +libsecret = "0.4.0" +libsecret-sys = "0.4.0" [build-dependencies] napi-build = "2" @@ -41,4 +43,4 @@ napi-build = "2" [profile.release] lto = true opt-level = "z" # Optimize for size. -strip = "symbols" \ No newline at end of file +strip = "symbols" diff --git a/packages/secrets/src/keyring/__test__/index.spec.mjs b/packages/secrets/src/keyring/__test__/index.spec.mjs index 87abb1e383..6ca2cd4c60 100644 --- a/packages/secrets/src/keyring/__test__/index.spec.mjs +++ b/packages/secrets/src/keyring/__test__/index.spec.mjs @@ -163,6 +163,17 @@ test.serial( } ); +test.serial("findCredentials works when only one credential is found", async (t) => { + await setPassword("TestKeyring2", "TestOneCred", "pass"); + + const creds = await findCredentials("TestKeyring2"); + t.deepEqual(creds, [{ + account: "TestOneCred", + password: "pass" + }]); + await deletePassword("TestKeyring2", "TestOneCred"); +}); + test.serial("findPassword for ASCII string", async (t) => { const pw = await findPassword("TestKeyring/TestASCII"); t.is(pw, "ASCII string"); diff --git a/packages/secrets/src/keyring/src/os/error.rs b/packages/secrets/src/keyring/src/os/error.rs index 572ca2489d..7d7c251d32 100644 --- a/packages/secrets/src/keyring/src/os/error.rs +++ b/packages/secrets/src/keyring/src/os/error.rs @@ -11,6 +11,7 @@ pub enum KeyringError { #[error("[keyring] {name:?} library returned an error:\n\n{details:?}")] Library { name: String, details: String }, + #[cfg(not(target_os = "macos"))] #[error("[keyring] An OS error has occurred:\n\n{0}")] Os(String), diff --git a/packages/secrets/src/keyring/src/os/mac/error.rs b/packages/secrets/src/keyring/src/os/mac/error.rs new file mode 100644 index 0000000000..980527b003 --- /dev/null +++ b/packages/secrets/src/keyring/src/os/mac/error.rs @@ -0,0 +1,66 @@ +use crate::os::mac::ffi::SecCopyErrorMessageString; +use core_foundation::base::TCFType; +use core_foundation::string::CFString; +use core_foundation_sys::base::OSStatus; +use std::fmt::{Debug, Display, Formatter}; +use std::num::NonZeroI32; + +#[derive(Copy, Clone)] +pub struct Error(NonZeroI32); + +/// errSecItemNotFound +pub const ERR_SEC_ITEM_NOT_FOUND: i32 = -25300; + +impl Error { + #[inline] + #[must_use] + pub fn from_code(code: OSStatus) -> Self { + Self(NonZeroI32::new(code).unwrap_or_else(|| NonZeroI32::new(1).unwrap())) + } + + pub fn code(self) -> i32 { + self.0.get() as _ + } + + /// Gets the message matching an OSStatus error code, if one exists. + pub fn message(&self) -> Option { + unsafe { + let s = SecCopyErrorMessageString(self.code(), std::ptr::null_mut()); + if s.is_null() { + None + } else { + Some(CFString::wrap_under_create_rule(s).to_string()) + } + } + } +} + +impl Debug for Error { + fn fmt(&self, fmt: &mut Formatter<'_>) -> std::fmt::Result { + let mut builder = fmt.debug_struct("Error"); + builder.field("code", &self.0); + if let Some(message) = self.message() { + builder.field("message", &message); + } + builder.finish() + } +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self.message() { + Some(msg) => write!(f, "{}", msg), + None => write!(f, "code: {}", self.code()), + } + } +} + +/// Handles the OSStatus code from macOS FFI calls (error handling helper fn) +#[inline(always)] +pub fn handle_os_status(err: OSStatus) -> Result<(), Error> { + match err { + // errSecSuccess + 0 => Ok(()), + err => Err(Error::from_code(err)), + } +} diff --git a/packages/secrets/src/keyring/src/os/mac/ffi.rs b/packages/secrets/src/keyring/src/os/mac/ffi.rs new file mode 100644 index 0000000000..e76e92c040 --- /dev/null +++ b/packages/secrets/src/keyring/src/os/mac/ffi.rs @@ -0,0 +1,118 @@ +use core_foundation_sys::base::{CFTypeID, CFTypeRef, OSStatus}; +use core_foundation_sys::dictionary::CFDictionaryRef; +use core_foundation_sys::string::CFStringRef; +use std::ffi::{c_char, c_void}; + +/// +/// Keychain item reference types. +/// +/// See lib/SecBase.h here: +/// https://opensource.apple.com/source/libsecurity_keychain/libsecurity_keychain-55050.2/ +/// +pub enum OpaqueSecKeychainItemRef {} +pub enum OpaqueSecKeychainRef {} +pub type SecKeychainItemRef = *mut OpaqueSecKeychainItemRef; +pub type SecKeychainRef = *mut OpaqueSecKeychainRef; + +/// +/// Certificate item reference types. +/// https://developer.apple.com/documentation/security/opaqueseccertificateref +/// +pub enum OpaqueSecCertificateRef {} +pub type SecCertificateRef = *mut OpaqueSecCertificateRef; + +/// +/// Identity reference types. +/// https://developer.apple.com/documentation/security/opaquesecidentityref +/// +pub enum OpaqueSecIdentityRef {} +pub type SecIdentityRef = *mut OpaqueSecIdentityRef; + +/// +/// Key reference types. +/// https://developer.apple.com/documentation/security/seckeyref +/// +pub enum OpaqueSecKeyRef {} +pub type SecKeyRef = *mut OpaqueSecKeyRef; + +/// +/// Keychain attribute structure for searching items. +/// https://developer.apple.com/documentation/security/seckeychainattribute, +/// https://developer.apple.com/documentation/security/seckeychainattributelist +/// +#[repr(C)] +#[derive(Copy, Clone)] +pub struct SecKeychainAttribute { + pub tag: u32, + pub length: u32, + pub data: *mut c_void, +} +#[repr(C)] +#[derive(Copy, Clone)] +pub struct SecKeychainAttributeList { + pub count: u32, + pub attr: *mut SecKeychainAttribute, +} + +/* + * Defined below are the C functions that the Rust logic + * uses to interact with macOS's Security.framework. + * + * Since we can call C functions directly using Rust FFI, we just need to define + * the function prototypes ahead of time, and link them to the Security.framework library. + * Rust will evaluate these symbols during compile time. + */ +#[link(name = "Security", kind = "framework")] +extern "C" { + // keychain.rs: + pub fn SecCopyErrorMessageString(status: OSStatus, reserved: *mut c_void) -> CFStringRef; + pub fn SecKeychainAddGenericPassword( + keychain: SecKeychainRef, + service_name_length: u32, + service_name: *const c_char, + account_name_length: u32, + account_name: *const c_char, + password_length: u32, + password_data: *const c_void, + item_ref: *mut SecKeychainItemRef, + ) -> OSStatus; + pub fn SecKeychainCopyDefault(keychain: *mut SecKeychainRef) -> OSStatus; + pub fn SecKeychainFindGenericPassword( + keychain_or_array: CFTypeRef, + service_name_len: u32, + service_name: *const c_char, + account_name_len: u32, + account_name: *const c_char, + password_len: *mut u32, + password: *mut *mut c_void, + item_ref: *mut SecKeychainItemRef, + ) -> OSStatus; + pub fn SecKeychainGetTypeID() -> CFTypeID; + + // keychain_item.rs: + pub fn SecKeychainItemDelete(item_ref: SecKeychainItemRef) -> OSStatus; + pub fn SecKeychainItemGetTypeID() -> CFTypeID; + pub fn SecKeychainItemModifyAttributesAndData( + item_ref: SecKeychainItemRef, + attr_list: *const SecKeychainAttributeList, + length: u32, + data: *const c_void, + ) -> OSStatus; + + // keychain_search.rs: + pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus; + pub static kSecAttrAccount: CFStringRef; + pub static kSecAttrLabel: CFStringRef; + pub static kSecAttrService: CFStringRef; + pub static kSecClass: CFStringRef; + pub static kSecClassGenericPassword: CFStringRef; + pub static kSecMatchLimit: CFStringRef; + pub static kSecReturnAttributes: CFStringRef; + pub static kSecReturnData: CFStringRef; + pub static kSecReturnRef: CFStringRef; + + // misc.rs: + pub fn SecCertificateGetTypeID() -> CFTypeID; + pub fn SecIdentityGetTypeID() -> CFTypeID; + pub fn SecKeyGetTypeID() -> CFTypeID; +} diff --git a/packages/secrets/src/keyring/src/os/mac/keychain.rs b/packages/secrets/src/keyring/src/os/mac/keychain.rs new file mode 100644 index 0000000000..250b7c2583 --- /dev/null +++ b/packages/secrets/src/keyring/src/os/mac/keychain.rs @@ -0,0 +1,114 @@ +use crate::os::mac::error::{handle_os_status, Error}; +use crate::os::mac::ffi::{ + SecKeychainAddGenericPassword, SecKeychainCopyDefault, SecKeychainFindGenericPassword, + SecKeychainGetTypeID, SecKeychainRef, +}; +use crate::os::mac::keychain_item::SecKeychainItem; +use core_foundation::{base::TCFType, declare_TCFType, impl_TCFType}; +use std::ops::Deref; + +/* + * SecKeychain: https://developer.apple.com/documentation/security/seckeychain + * SecKeychainRef: https://developer.apple.com/documentation/security/seckeychainref + */ +declare_TCFType! { + SecKeychain, SecKeychainRef +} +impl_TCFType!(SecKeychain, SecKeychainRef, SecKeychainGetTypeID); + +/* Wrapper struct for handling passwords within SecKeychainItem objects. */ +pub struct KeychainItemPassword { + pub data: *const u8, + pub data_len: usize, +} + +impl AsRef<[u8]> for KeychainItemPassword { + #[inline] + fn as_ref(&self) -> &[u8] { + unsafe { std::slice::from_raw_parts(self.data, self.data_len) } + } +} + +impl Deref for KeychainItemPassword { + type Target = [u8]; + #[inline] + fn deref(&self) -> &Self::Target { + self.as_ref() + } +} + +impl SecKeychain { + pub fn default() -> Result { + let mut keychain = std::ptr::null_mut(); + unsafe { + handle_os_status(SecKeychainCopyDefault(&mut keychain))?; + } + unsafe { Ok(Self::wrap_under_create_rule(keychain)) } + } + + /// + /// set_password + /// Attempts to set the password within the keychain for a given service and account. + /// + /// Returns: + /// - Nothing if the password was set successfully, or + /// - An `Error` object if an error was encountered + /// + pub fn set_password(&self, service: &str, account: &str, password: &[u8]) -> Result<(), Error> { + match self.find_password(service, account) { + Ok((_, mut item)) => item.set_password(password), + _ => unsafe { + handle_os_status(SecKeychainAddGenericPassword( + self.as_CFTypeRef() as *mut _, + service.len() as u32, + service.as_ptr().cast(), + account.len() as u32, + account.as_ptr().cast(), + password.len() as u32, + password.as_ptr().cast(), + std::ptr::null_mut(), + )) + }, + } + } + + /// + /// find_password + /// Attempts to find a password within the keychain matching a given service and account. + /// + /// Returns: + /// - A pair containing the KeychainItem object with its password data if the password was found, or + /// - An `Error` object if an error was encountered + /// + pub fn find_password( + &self, + service: &str, + account: &str, + ) -> Result<(KeychainItemPassword, SecKeychainItem), Error> { + let keychain_ref = self.as_CFTypeRef(); + + let mut len = 0; + let mut data = std::ptr::null_mut(); + let mut item = std::ptr::null_mut(); + + unsafe { + handle_os_status(SecKeychainFindGenericPassword( + keychain_ref, + service.len() as u32, + service.as_ptr().cast(), + account.len() as u32, + account.as_ptr().cast(), + &mut len, + &mut data, + &mut item, + ))?; + Ok(( + KeychainItemPassword { + data: data as *const _, + data_len: len as usize, + }, + SecKeychainItem::wrap_under_create_rule(item), + )) + } + } +} diff --git a/packages/secrets/src/keyring/src/os/mac/keychain_item.rs b/packages/secrets/src/keyring/src/os/mac/keychain_item.rs new file mode 100644 index 0000000000..8008e2781f --- /dev/null +++ b/packages/secrets/src/keyring/src/os/mac/keychain_item.rs @@ -0,0 +1,56 @@ +use crate::os::mac::error::{handle_os_status, Error}; +use crate::os::mac::ffi::{ + SecKeychainItemDelete, SecKeychainItemGetTypeID, SecKeychainItemModifyAttributesAndData, + SecKeychainItemRef, +}; +use core_foundation::base::TCFType; +use core_foundation::{declare_TCFType, impl_TCFType}; + +/* + * SecKeychainItem: https://developer.apple.com/documentation/security/seckeychainitem + * SecKeychainItemRef: https://developer.apple.com/documentation/security/seckeychainitemref + */ +declare_TCFType! { + SecKeychainItem, SecKeychainItemRef +} +impl_TCFType! { + SecKeychainItem, + SecKeychainItemRef, + SecKeychainItemGetTypeID +} + +impl SecKeychainItem { + /// + /// delete + /// Attempts to delete this keychain item from the keychain. + /// + /// Returns: + /// - Nothing if the deletion request was successful, or + /// - An `Error` object if an error was encountered + /// + #[inline] + pub fn delete(self) -> Result<(), Error> { + unsafe { handle_os_status(SecKeychainItemDelete(self.as_CFTypeRef() as *mut _)) } + } + + /// + /// set_password + /// Attempts to set the password for this keychain item. + /// + /// Returns: + /// - Nothing if the password was set successfully, or + /// - An `Error` object if an error was encountered + /// + pub fn set_password(&mut self, password: &[u8]) -> Result<(), Error> { + unsafe { + handle_os_status(SecKeychainItemModifyAttributesAndData( + self.as_CFTypeRef() as *mut _, + std::ptr::null(), + password.len() as u32, + password.as_ptr().cast(), + ))?; + } + + Ok(()) + } +} diff --git a/packages/secrets/src/keyring/src/os/mac/keychain_search.rs b/packages/secrets/src/keyring/src/os/mac/keychain_search.rs new file mode 100644 index 0000000000..8de998dfbe --- /dev/null +++ b/packages/secrets/src/keyring/src/os/mac/keychain_search.rs @@ -0,0 +1,249 @@ +use crate::os::mac::error::{handle_os_status, Error}; +use crate::os::mac::ffi::{ + kSecAttrAccount, kSecAttrLabel, kSecAttrService, kSecClass, kSecClassGenericPassword, + kSecMatchLimit, kSecReturnAttributes, kSecReturnData, kSecReturnRef, SecItemCopyMatching, +}; +use crate::os::mac::keychain_item::SecKeychainItem; +use crate::os::mac::misc::{SecCertificate, SecIdentity, SecKey}; +use core_foundation::array::CFArray; +use core_foundation::base::{CFType, TCFType}; +use core_foundation::boolean::CFBoolean; +use core_foundation::data::CFData; +use core_foundation::date::CFDate; +use core_foundation::dictionary::CFDictionary; +use core_foundation::number::CFNumber; +use core_foundation::string::CFString; +use core_foundation_sys::base::{CFCopyDescription, CFGetTypeID, CFRelease, CFTypeRef}; +use std::collections::HashMap; + +/// Keychain Search structure to reference when making searches within the keychain. +#[derive(Default)] +pub struct KeychainSearch { + label: Option, + service: Option, + account: Option, + load_attrs: bool, + load_data: bool, + load_refs: bool, +} + +/// Reference enum for categorizing search results based on item type. +pub enum Reference { + Identity(SecIdentity), + Certificate(SecCertificate), + Key(SecKey), + KeychainItem(SecKeychainItem), +} + +/// Enum for organizing types of items found during the keychain search operation. +pub enum SearchResult { + Ref(Reference), + Dict(CFDictionary), + Data(Vec), +} + +impl SearchResult { + /// + /// parse_dict + /// Tries to parse a CFDictionary object into a hashmap of string pairs. + /// + /// Returns: + /// - `Some(hash_map)` containing the attribute keys/values if parsed successfully + /// - `None` otherwise + #[must_use] + pub fn parse_dict(&self) -> Option> { + match *self { + Self::Dict(ref d) => unsafe { + // build map of attributes to return for this search result + let mut retmap = HashMap::new(); + let (keys, values) = d.get_keys_and_values(); + for (k, v) in keys.iter().zip(values.iter()) { + // get key as CFString from pointer + let key_cfstr = CFString::wrap_under_get_rule((*k).cast()); + + // get value based on CFType + let val: String = match CFGetTypeID(*v) { + cfstring if cfstring == CFString::type_id() => { + format!("{}", CFString::wrap_under_get_rule((*v).cast())) + } + cfdata if cfdata == CFData::type_id() => { + let buf = CFData::wrap_under_get_rule((*v).cast()); + let mut vec = Vec::new(); + vec.extend_from_slice(buf.bytes()); + format!("{}", String::from_utf8_lossy(&vec)) + } + cfdate if cfdate == CFDate::type_id() => format!( + "{}", + CFString::wrap_under_create_rule(CFCopyDescription(*v)) + ), + _ => String::from("unknown"), + }; + retmap.insert(format!("{}", key_cfstr), val); + } + Some(retmap) + }, + _ => None, + } + } +} + +/// +/// get_item +/// +/// item: The item reference to convert to a SearchResult +/// Returns: +/// - a SearchResult enum variant based on the item reference provided. +/// +unsafe fn get_item(item: CFTypeRef) -> SearchResult { + let type_id = CFGetTypeID(item); + + // if type is a raw buffer, return Vec of bytes based on item size + if type_id == CFData::type_id() { + let data = CFData::wrap_under_get_rule(item as *mut _); + let mut buf = Vec::new(); + buf.extend_from_slice(data.bytes()); + return SearchResult::Data(buf); + } + + // if type is dictionary of items, cast as CFDictionary object + if type_id == CFDictionary::<*const u8, *const u8>::type_id() { + return SearchResult::Dict(CFDictionary::wrap_under_get_rule(item as *mut _)); + } + + // if type is a single Keychain item, return it as a reference + if type_id == SecKeychainItem::type_id() { + return SearchResult::Ref(Reference::KeychainItem( + SecKeychainItem::wrap_under_get_rule(item as *mut _), + )); + } + + // handle certificate, cryptographic key, and identity types as + // they can also appear in search results for the keychain + let reference = match type_id { + r if r == SecCertificate::type_id() => { + Reference::Certificate(SecCertificate::wrap_under_get_rule(item as *mut _)) + } + r if r == SecKey::type_id() => Reference::Key(SecKey::wrap_under_get_rule(item as *mut _)), + r if r == SecIdentity::type_id() => { + Reference::Identity(SecIdentity::wrap_under_get_rule(item as *mut _)) + } + _ => panic!("Bad type received from SecItemCopyMatching: {}", type_id), + }; + + SearchResult::Ref(reference) +} + +impl KeychainSearch { + #[inline(always)] + #[must_use] + pub fn new() -> Self { + Self::default() + } + + pub fn label(&mut self, label: &str) -> &mut Self { + self.label = Some(CFString::new(label)); + self + } + + pub fn with_attrs(&mut self) -> &mut Self { + self.load_attrs = true; + self + } + + pub fn with_data(&mut self) -> &mut Self { + self.load_data = true; + self + } + + pub fn with_refs(&mut self) -> &mut Self { + self.load_refs = true; + self + } + + /// Executes a search within the keychain, factoring in the set search options. + /// + /// Returns: + /// - If successful, a `Vec` containing a list of search results + /// - an `Error` object otherwise + pub fn execute(&self) -> Result, Error> { + let mut params = vec![]; + + unsafe { + params.push(( + CFString::wrap_under_get_rule(kSecClass), + CFType::wrap_under_get_rule(kSecClassGenericPassword.cast()), + )); + + // Handle any parameters that were configured before execution (label, service, account) + if let Some(ref label) = self.label { + params.push(( + CFString::wrap_under_get_rule(kSecAttrLabel), + label.as_CFType(), + )); + } + if let Some(ref service) = self.service { + params.push(( + CFString::wrap_under_get_rule(kSecAttrService), + service.as_CFType(), + )); + } + if let Some(ref acc) = self.account { + params.push(( + CFString::wrap_under_get_rule(kSecAttrAccount), + acc.as_CFType(), + )); + } + + // Add params to fetch data, attributes, and/or refs if requested from search options + if self.load_data { + params.push(( + CFString::wrap_under_get_rule(kSecReturnData), + CFBoolean::true_value().into_CFType(), + )); + } + if self.load_attrs { + params.push(( + CFString::wrap_under_get_rule(kSecReturnAttributes), + CFBoolean::true_value().into_CFType(), + )); + } + if self.load_refs { + params.push(( + CFString::wrap_under_get_rule(kSecReturnRef), + CFBoolean::true_value().into_CFType(), + )); + } + + // Remove the default limit of 0 by requesting all items that match the search + params.push(( + CFString::wrap_under_get_rule(kSecMatchLimit), + CFNumber::from(i32::MAX).into_CFType(), + )); + + let params = CFDictionary::from_CFType_pairs(¶ms); + let mut ret = std::ptr::null(); + + // handle copy operation status and get type ID based on return value + handle_os_status(SecItemCopyMatching(params.as_concrete_TypeRef(), &mut ret))?; + if ret.is_null() { + return Ok(vec![]); + } + let type_id = CFGetTypeID(ret); + + // Build vector of items based on return reference type + let mut items = vec![]; + if type_id == CFArray::::type_id() { + let array: CFArray = CFArray::wrap_under_create_rule(ret as *mut _); + for item in array.iter() { + items.push(get_item(item.as_CFTypeRef())); + } + } else { + items.push(get_item(ret)); + + CFRelease(ret); + } + + Ok(items) + } + } +} diff --git a/packages/secrets/src/keyring/src/os/mac/misc.rs b/packages/secrets/src/keyring/src/os/mac/misc.rs new file mode 100644 index 0000000000..1af69da4df --- /dev/null +++ b/packages/secrets/src/keyring/src/os/mac/misc.rs @@ -0,0 +1,21 @@ +use crate::os::mac::ffi::{ + SecCertificateGetTypeID, SecCertificateRef, SecIdentityGetTypeID, SecIdentityRef, + SecKeyGetTypeID, SecKeyRef, +}; +use core_foundation::base::TCFType; +use core_foundation::{declare_TCFType, impl_TCFType}; + +// Structure that represents identities within the keychain +// https://developer.apple.com/documentation/security/secidentity +declare_TCFType!(SecIdentity, SecIdentityRef); +impl_TCFType!(SecIdentity, SecIdentityRef, SecIdentityGetTypeID); + +// Structure that represents certificates within the keychain +// https://developer.apple.com/documentation/security/seccertificate +declare_TCFType!(SecCertificate, SecCertificateRef); +impl_TCFType!(SecCertificate, SecCertificateRef, SecCertificateGetTypeID); + +// Structure that represents cryptographic keys within the keychain +// https://developer.apple.com/documentation/security/seckey +declare_TCFType!(SecKey, SecKeyRef); +impl_TCFType!(SecKey, SecKeyRef, SecKeyGetTypeID); diff --git a/packages/secrets/src/keyring/src/os/mac.rs b/packages/secrets/src/keyring/src/os/mac/mod.rs similarity index 69% rename from packages/secrets/src/keyring/src/os/mac.rs rename to packages/secrets/src/keyring/src/os/mac/mod.rs index 8497ecd696..3ff763774c 100644 --- a/packages/secrets/src/keyring/src/os/mac.rs +++ b/packages/secrets/src/keyring/src/os/mac/mod.rs @@ -1,28 +1,33 @@ -extern crate security_framework; use super::error::KeyringError; -use security_framework::{ - item::{ItemClass, ItemSearchOptions}, - os::macos::keychain::SecKeychain, -}; +mod error; +mod ffi; +mod keychain; +mod keychain_item; +mod keychain_search; +mod misc; -const ERR_SEC_ITEM_NOT_FOUND: i32 = -25300; +use error::Error; -impl From for KeyringError { - fn from(error: security_framework::base::Error) -> Self { +use crate::os::mac::error::ERR_SEC_ITEM_NOT_FOUND; +use crate::os::mac::keychain_search::{KeychainSearch, SearchResult}; +use keychain::SecKeychain; + +impl From for KeyringError { + fn from(error: Error) -> Self { KeyringError::Library { - name: "security_framework".to_owned(), - details: format!("{:?}", error), + name: "macOS Security.framework".to_owned(), + details: format!("{:?}", error.message()), } } } -/// +/// /// Attempts to set a password for a given service and account. -/// +/// /// - `service`: The service name for the new credential /// - `account`: The account name for the new credential -/// +/// /// Returns: /// - `true` if the credential was stored successfully /// - A `KeyringError` if there were any issues interacting with the credential vault @@ -33,40 +38,40 @@ pub fn set_password( password: &mut String, ) -> Result { let keychain = SecKeychain::default().unwrap(); - match keychain.set_generic_password(service.as_str(), account.as_str(), password.as_bytes()) { + match keychain.set_password(service.as_str(), account.as_str(), password.as_bytes()) { Ok(()) => Ok(true), Err(err) => Err(KeyringError::from(err)), } } -/// +/// /// Returns a password contained in the given service and account, if found. -/// +/// /// - `service`: The service name that matches the credential of interest /// - `account`: The account name that matches the credential of interest -/// +/// /// Returns: /// - `Some(password)` if a matching credential was found; `None` otherwise /// - A `KeyringError` if there were any issues interacting with the credential vault -/// +/// pub fn get_password(service: &String, account: &String) -> Result, KeyringError> { let keychain = SecKeychain::default().unwrap(); - match keychain.find_generic_password(service.as_str(), account.as_str()) { + match keychain.find_password(service.as_str(), account.as_str()) { Ok((pw, _)) => Ok(Some(String::from_utf8(pw.to_owned())?)), Err(err) if err.code() == ERR_SEC_ITEM_NOT_FOUND => Ok(None), Err(err) => Err(KeyringError::from(err)), } } -/// +/// /// Returns the first password (if any) that matches the given service pattern. -/// +/// /// - `service`: The service pattern that matches the credential of interest -/// +/// /// Returns: /// - `Some(password)` if a matching credential was found; `None` otherwise /// - A `KeyringError` if there were any issues interacting with the credential vault -/// +/// pub fn find_password(service: &String) -> Result, KeyringError> { let cred_attrs: Vec<&str> = service.split("/").collect(); if cred_attrs.len() < 2 { @@ -78,7 +83,7 @@ pub fn find_password(service: &String) -> Result, KeyringError> { } let keychain = SecKeychain::default().unwrap(); - match keychain.find_generic_password(cred_attrs[0], cred_attrs[1]) { + match keychain.find_password(cred_attrs[0], cred_attrs[1]) { Ok((pw, _)) => { let pw_str = String::from_utf8(pw.to_owned())?; return Ok(Some(pw_str)); @@ -87,21 +92,21 @@ pub fn find_password(service: &String) -> Result, KeyringError> { } } -/// +/// /// Attempts to delete the password associated with a given service and account. -/// +/// /// - `service`: The service name of the credential to delete /// - `account`: The account name of the credential to delete -/// +/// /// Returns: /// - `true` if a matching credential was deleted; `false` otherwise /// - A `KeyringError` if there were any issues interacting with the credential vault -/// +/// pub fn delete_password(service: &String, account: &String) -> Result { let keychain = SecKeychain::default().unwrap(); - match keychain.find_generic_password(service.as_str(), account.as_str()) { + match keychain.find_password(service.as_str(), account.as_str()) { Ok((_, item)) => { - item.delete(); + item.delete()?; return Ok(true); } Err(err) if err.code() == ERR_SEC_ITEM_NOT_FOUND => Ok(false), @@ -109,39 +114,45 @@ pub fn delete_password(service: &String, account: &String) -> Result, ) -> Result { - match ItemSearchOptions::new() - .class(ItemClass::generic_password()) + match KeychainSearch::new() .label(service.as_str()) - .limit(i32::MAX as i64) - .load_attributes(true) - .load_data(true) - .load_refs(true) - .search() + .with_attrs() + .with_data() + .with_refs() + .execute() { Ok(search_results) => { - for result in search_results { - if let Some(result_map) = result.simplify_dict() { - credentials.push(( - result_map.get("acct").unwrap().to_owned(), - result_map.get("v_Data").unwrap().to_owned(), - )) - } - } - return Ok(!credentials.is_empty()); + *credentials = search_results + .iter() + .filter_map(|result| match result { + SearchResult::Dict(_) => { + return match result.parse_dict() { + Some(attrs) => Some(( + attrs.get("acct").unwrap().to_owned(), + attrs.get("v_Data").unwrap().to_owned(), + )), + None => None, + }; + } + _ => None, + }) + .collect(); + + Ok(!credentials.is_empty()) } Err(err) if err.code() == ERR_SEC_ITEM_NOT_FOUND => Ok(false), Err(err) => Err(KeyringError::from(err)), diff --git a/packages/secrets/src/keyring/src/os/unix.rs b/packages/secrets/src/keyring/src/os/unix.rs index 3229ba46b9..723817a7d3 100644 --- a/packages/secrets/src/keyring/src/os/unix.rs +++ b/packages/secrets/src/keyring/src/os/unix.rs @@ -1,5 +1,7 @@ +extern crate glib_sys; extern crate libsecret; use glib::translate::{FromGlibPtrContainer, ToGlibPtr}; +use glib_sys::g_hash_table_unref; use libsecret::{ prelude::CollectionExtManual, traits::ItemExt, SearchFlags, Service, ServiceFlags, }; @@ -18,7 +20,7 @@ impl From for KeyringError { /// /// Returns the libsecret schema that corresponds to service and account attributes. -/// +/// fn get_schema() -> libsecret::Schema { libsecret::Schema::new( "org.freedesktop.Secret.Generic", @@ -32,20 +34,20 @@ fn get_schema() -> libsecret::Schema { /// /// Builds an attribute map with the given service and account names. -/// +/// /// Returns: /// - A `HashMap` built with the `service` and `account` values provided. Used for attribute functions. -/// +/// fn get_attribute_map<'a>(service: &'a str, account: &'a str) -> HashMap<&'a str, &'a str> { HashMap::from([("service", service), ("account", account)]) } -/// +/// /// Attempts to set a password for a given service and account. -/// +/// /// - `service`: The service name for the new credential /// - `account`: The account name for the new credential -/// +/// /// Returns: /// - `true` if the credential was stored successfully /// - A `KeyringError` if there were any issues interacting with the credential vault @@ -71,16 +73,16 @@ pub fn set_password( } } -/// +/// /// Returns a password contained in the given service and account, if found. -/// +/// /// - `service`: The service name that matches the credential of interest /// - `account`: The account name that matches the credential of interest -/// +/// /// Returns: /// - `Some(password)` if a matching credential was found; `None` otherwise /// - A `KeyringError` if there were any issues interacting with the credential vault -/// +/// pub fn get_password(service: &String, account: &String) -> Result, KeyringError> { let attributes = get_attribute_map(service.as_str(), account.as_str()); @@ -93,15 +95,15 @@ pub fn get_password(service: &String, account: &String) -> Result } } -/// +/// /// Returns the first password (if any) that matches the given service pattern. -/// +/// /// - `service`: The service pattern that matches the credential of interest -/// +/// /// Returns: /// - `Some(password)` if a matching credential was found; `None` otherwise /// - A `KeyringError` if there were any issues interacting with the credential vault -/// +/// pub fn find_password(service: &String) -> Result, KeyringError> { let attributes = if service.contains("/") && service.len() > 1 { // In format "service/account" @@ -120,16 +122,16 @@ pub fn find_password(service: &String) -> Result, KeyringError> { } } -/// +/// /// Attempts to delete the password associated with a given service and account. -/// +/// /// - `service`: The service name of the credential to delete /// - `account`: The account name of the credential to delete -/// +/// /// Returns: /// - `true` if a matching credential was deleted; `false` otherwise /// - A `KeyringError` if there were any issues interacting with the credential vault -/// +/// pub fn delete_password(service: &String, account: &String) -> Result { match libsecret::password_clear_sync( Some(&get_schema()), @@ -144,16 +146,16 @@ pub fn delete_password(service: &String, account: &String) -> Result, @@ -179,32 +181,29 @@ pub fn find_credentials( match collection.search_sync( Some(&get_schema()), HashMap::from([("service", service.as_str())]), - SearchFlags::ALL | SearchFlags::LOAD_SECRETS, + SearchFlags::ALL | SearchFlags::UNLOCK | SearchFlags::LOAD_SECRETS, gio::Cancellable::NONE, ) { Ok(vec) => { let valid_creds: Vec<(String, String)> = vec .iter() - .filter_map(|item| { - let attrs: HashMap = unsafe { - let attrs = - libsecret_sys::secret_item_get_attributes(item.to_glib_none().0); - FromGlibPtrContainer::from_glib_full(attrs) - }; - match item.secret() { - Some(secret) => { - let bytes = secret.get(); - unsafe { - libsecret_sys::secret_value_unref(secret.as_ptr() as *mut _); - } - - let acc = attrs.get("account").unwrap().clone(); - let pw = String::from_utf8(bytes).unwrap_or("".to_string()); - - Some((acc, pw)) + .filter_map(|item| match item.secret() { + Some(secret) => { + let attrs: HashMap = unsafe { + let attrs = + libsecret_sys::secret_item_get_attributes(item.to_glib_none().0); + FromGlibPtrContainer::from_glib_full(attrs) + }; + let bytes = secret.get(); + let pw = String::from_utf8(bytes).unwrap_or("".to_string()); + + let acc = attrs.get("account").unwrap().clone(); + unsafe { + g_hash_table_unref(attrs.to_glib_full()); } - None => None, + Some((acc, pw)) } + None => None, }) .collect(); *credentials = valid_creds;