Add a platform API for accessing the keychain

This commit is contained in:
Max Brunsfeld 2021-06-08 17:44:45 -07:00
parent 3214fef727
commit 5673f116c9
3 changed files with 120 additions and 0 deletions

View file

@ -40,9 +40,13 @@ pub trait Platform: Send + Sync {
) -> Box<dyn Window>;
fn key_window_id(&self) -> Option<usize>;
fn quit(&self);
fn write_to_clipboard(&self, item: ClipboardItem);
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
fn open_url(&self, url: &str);
fn write_credentials(&self, url: &str, username: &str, password: &[u8]);
fn read_credentials(&self, url: &str) -> Option<(String, Vec<u8>)>;
}
pub(crate) trait ForegroundPlatform {

View file

@ -10,6 +10,13 @@ use cocoa::{
base::{id, nil, selector},
foundation::{NSArray, NSAutoreleasePool, NSData, NSInteger, NSString, NSURL},
};
use core_foundation::{
base::{CFType, CFTypeRef, OSStatus, TCFType as _},
boolean::CFBoolean,
data::CFData,
dictionary::{CFDictionary, CFDictionaryRef, CFMutableDictionary},
string::{CFString, CFStringRef},
};
use ctor::ctor;
use objc::{
class,
@ -459,6 +466,86 @@ impl platform::Platform for MacPlatform {
msg_send![workspace, openURL: url]
}
}
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) {
let url = CFString::from(url);
let username = CFString::from(username);
let password = CFData::from_buffer(password);
unsafe {
use security::*;
// First, check if there are already credentials for the given server. If so, then
// update the username and password.
let mut verb = "updating";
let mut query_attrs = CFMutableDictionary::with_capacity(2);
query_attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
query_attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
let mut attrs = CFMutableDictionary::with_capacity(4);
attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
attrs.set(kSecAttrAccount as *const _, username.as_CFTypeRef());
attrs.set(kSecValueData as *const _, password.as_CFTypeRef());
let mut status = SecItemUpdate(
query_attrs.as_concrete_TypeRef(),
attrs.as_concrete_TypeRef(),
);
// If there were no existing credentials for the given server, then create them.
if status == errSecItemNotFound {
verb = "creating";
status = SecItemAdd(attrs.as_concrete_TypeRef(), ptr::null_mut());
}
if status != errSecSuccess {
panic!("{} password failed: {}", verb, status);
}
}
}
fn read_credentials(&self, url: &str) -> Option<(String, Vec<u8>)> {
let url = CFString::from(url);
let cf_true = CFBoolean::true_value().as_CFTypeRef();
unsafe {
use security::*;
// Find any credentials for the given server URL.
let mut attrs = CFMutableDictionary::with_capacity(5);
attrs.set(kSecClass as *const _, kSecClassInternetPassword as *const _);
attrs.set(kSecAttrServer as *const _, url.as_CFTypeRef());
attrs.set(kSecReturnAttributes as *const _, cf_true);
attrs.set(kSecReturnData as *const _, cf_true);
let mut result = CFTypeRef::from(ptr::null_mut());
let status = SecItemCopyMatching(attrs.as_concrete_TypeRef(), &mut result);
match status {
security::errSecSuccess => {}
security::errSecItemNotFound => return None,
_ => panic!("reading password failed: {}", status),
}
let result = CFType::wrap_under_create_rule(result)
.downcast::<CFDictionary>()
.expect("keychain item was not a dictionary");
let username = result
.find(kSecAttrAccount as *const _)
.expect("account was missing from keychain item");
let username = CFType::wrap_under_get_rule(*username)
.downcast::<CFString>()
.expect("account was not a string");
let password = result
.find(kSecValueData as *const _)
.expect("password was missing from keychain item");
let password = CFType::wrap_under_get_rule(*password)
.downcast::<CFData>()
.expect("password was not a string");
Some((username.to_string(), password.bytes().to_vec()))
}
}
}
unsafe fn get_foreground_platform(object: &mut Object) -> &MacForegroundPlatform {
@ -550,6 +637,29 @@ unsafe fn ns_string(string: &str) -> id {
NSString::alloc(nil).init_str(string).autorelease()
}
mod security {
#![allow(non_upper_case_globals)]
use super::*;
#[link(name = "Security", kind = "framework")]
extern "C" {
pub static kSecClass: CFStringRef;
pub static kSecClassInternetPassword: CFStringRef;
pub static kSecAttrServer: CFStringRef;
pub static kSecAttrAccount: CFStringRef;
pub static kSecValueData: CFStringRef;
pub static kSecReturnAttributes: CFStringRef;
pub static kSecReturnData: CFStringRef;
pub fn SecItemAdd(attributes: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
pub fn SecItemUpdate(query: CFDictionaryRef, attributes: CFDictionaryRef) -> OSStatus;
pub fn SecItemCopyMatching(query: CFDictionaryRef, result: *mut CFTypeRef) -> OSStatus;
}
pub const errSecSuccess: OSStatus = 0;
pub const errSecItemNotFound: OSStatus = -25300;
}
#[cfg(test)]
mod tests {
use crate::platform::Platform;

View file

@ -123,6 +123,12 @@ impl super::Platform for Platform {
}
fn open_url(&self, _: &str) {}
fn write_credentials(&self, _: &str, _: &str, _: &[u8]) {}
fn read_credentials(&self, _: &str) -> Option<(String, Vec<u8>)> {
None
}
}
impl Window {