From 5673f116c951214a528b856fa6baafff3d6143f8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 8 Jun 2021 17:44:45 -0700 Subject: [PATCH] Add a platform API for accessing the keychain --- gpui/src/platform.rs | 4 ++ gpui/src/platform/mac/platform.rs | 110 ++++++++++++++++++++++++++++++ gpui/src/platform/test.rs | 6 ++ 3 files changed, 120 insertions(+) diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index 9c7788822f6b15255b07d8aef44fab887e744c59..d3164e50f22fc719197990315227ba80820de506 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -40,9 +40,13 @@ pub trait Platform: Send + Sync { ) -> Box; fn key_window_id(&self) -> Option; fn quit(&self); + fn write_to_clipboard(&self, item: ClipboardItem); fn read_from_clipboard(&self) -> Option; 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)>; } pub(crate) trait ForegroundPlatform { diff --git a/gpui/src/platform/mac/platform.rs b/gpui/src/platform/mac/platform.rs index 6ec08b4cd0c8cf9afc9aac0f7f0f3d5a115ebf2a..1f1056b3a8a0c6a198d5f7fd0cf26714c7eefa30 100644 --- a/gpui/src/platform/mac/platform.rs +++ b/gpui/src/platform/mac/platform.rs @@ -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)> { + 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::() + .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::() + .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::() + .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; diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 41b96064c3ec6fa92da2183f381dbecbfd430ec1..2927b36b4d5325544f8a657bea773d9112f211c6 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -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)> { + None + } } impl Window {