diff --git a/BUILD.gn b/BUILD.gn index eb7cd3d0f062..6e17575dc740 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -690,6 +690,7 @@ if (is_mac) { libs = [ "AVFoundation.framework", "Carbon.framework", + "LocalAuthentication.framework", "QuartzCore.framework", "Quartz.framework", "Security.framework", diff --git a/atom/browser/api/atom_api_system_preferences.cc b/atom/browser/api/atom_api_system_preferences.cc index 4943fd52d57a..ee95c17153ac 100644 --- a/atom/browser/api/atom_api_system_preferences.cc +++ b/atom/browser/api/atom_api_system_preferences.cc @@ -93,6 +93,8 @@ void SystemPreferences::BuildPrototype( .SetMethod("setAppLevelAppearance", &SystemPreferences::SetAppLevelAppearance) .SetMethod("getSystemColor", &SystemPreferences::GetSystemColor) + .SetMethod("canPromptTouchID", &SystemPreferences::CanPromptTouchID) + .SetMethod("promptTouchID", &SystemPreferences::PromptTouchID) .SetMethod("isTrustedAccessibilityClient", &SystemPreferences::IsTrustedAccessibilityClient) .SetMethod("getMediaAccessStatus", diff --git a/atom/browser/api/atom_api_system_preferences.h b/atom/browser/api/atom_api_system_preferences.h index 8e0bf7d86467..2a71ed952cc3 100644 --- a/atom/browser/api/atom_api_system_preferences.h +++ b/atom/browser/api/atom_api_system_preferences.h @@ -95,6 +95,10 @@ class SystemPreferences : public mate::EventEmitter std::string GetSystemColor(const std::string& color, mate::Arguments* args); + bool CanPromptTouchID(); + v8::Local PromptTouchID(v8::Isolate* isolate, + const std::string& reason); + static bool IsTrustedAccessibilityClient(bool prompt); // TODO(codebytere): Write tests for these methods once we diff --git a/atom/browser/api/atom_api_system_preferences_mac.mm b/atom/browser/api/atom_api_system_preferences_mac.mm index 4a55058fd543..8d65153eab76 100644 --- a/atom/browser/api/atom_api_system_preferences_mac.mm +++ b/atom/browser/api/atom_api_system_preferences_mac.mm @@ -8,14 +8,19 @@ #import #import +#import +#import #include "atom/browser/mac/atom_application.h" #include "atom/browser/mac/dict_util.h" #include "atom/common/native_mate_converters/gurl_converter.h" #include "atom/common/native_mate_converters/value_converter.h" +#include "base/mac/scoped_cftyperef.h" #include "base/mac/sdk_forward_declarations.h" +#include "base/sequenced_task_runner.h" #include "base/strings/stringprintf.h" #include "base/strings/sys_string_conversions.h" +#include "base/threading/sequenced_task_runner_handle.h" #include "base/values.h" #include "native_mate/object_template_builder.h" #include "net/base/mac/url_conversions.h" @@ -438,6 +443,72 @@ std::string SystemPreferences::GetSystemColor(const std::string& color, } } +bool SystemPreferences::CanPromptTouchID() { + if (@available(macOS 10.12.2, *)) { + base::scoped_nsobject context([[LAContext alloc] init]); + if (![context + canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics + error:nil]) + return false; + if (@available(macOS 10.13.2, *)) + return [context biometryType] == LABiometryTypeTouchID; + return true; + } + return false; +} + +void OnTouchIDCompleted(scoped_refptr promise) { + promise->Resolve(); +} + +void OnTouchIDFailed(scoped_refptr promise, + const std::string& reason) { + promise->RejectWithErrorMessage(reason); +} + +v8::Local SystemPreferences::PromptTouchID( + v8::Isolate* isolate, + const std::string& reason) { + scoped_refptr promise = new util::Promise(isolate); + if (@available(macOS 10.12.2, *)) { + base::scoped_nsobject context([[LAContext alloc] init]); + base::ScopedCFTypeRef access_control = + base::ScopedCFTypeRef( + SecAccessControlCreateWithFlags( + kCFAllocatorDefault, + kSecAttrAccessibleWhenUnlockedThisDeviceOnly, + kSecAccessControlPrivateKeyUsage | + kSecAccessControlUserPresence, + nullptr)); + + scoped_refptr runner = + base::SequencedTaskRunnerHandle::Get(); + + [context + evaluateAccessControl:access_control + operation:LAAccessControlOperationUseKeySign + localizedReason:[NSString stringWithUTF8String:reason.c_str()] + reply:^(BOOL success, NSError* error) { + if (!success) { + runner->PostTask( + FROM_HERE, + base::BindOnce( + &OnTouchIDFailed, promise, + std::string([error.localizedDescription + UTF8String]))); + } else { + runner->PostTask( + FROM_HERE, + base::BindOnce(&OnTouchIDCompleted, promise)); + } + }]; + } else { + promise->RejectWithErrorMessage( + "This API is not available on macOS versions older than 10.12.2"); + } + return promise->GetHandle(); +} + // static bool SystemPreferences::IsTrustedAccessibilityClient(bool prompt) { NSDictionary* options = @{(id)kAXTrustedCheckOptionPrompt : @(prompt)}; diff --git a/atom/browser/mac/atom_application.h b/atom/browser/mac/atom_application.h index 6a533dea4306..dc28939a3bbd 100644 --- a/atom/browser/mac/atom_application.h +++ b/atom/browser/mac/atom_application.h @@ -7,6 +7,7 @@ #include "base/mac/scoped_sending_event.h" #import +#import // Forward Declare Appearance APIs @interface NSApplication (HighSierraSDK) @@ -16,6 +17,22 @@ - (void)setAppearance:(NSAppearance*)appearance API_AVAILABLE(macosx(10.14)); @end +#if !defined(MAC_OS_X_VERSION_10_13_2) + +// forward declare Touch ID APIs +typedef NS_ENUM(NSInteger, LABiometryType) { + LABiometryTypeNone = 0, + LABiometryTypeFaceID = 1, + LABiometryTypeTouchID = 2, +} API_AVAILABLE(macosx(10.13.2)); + +@interface LAContext (HighSierraPointTwoSDK) +@property(nonatomic, readonly) + LABiometryType biometryType API_AVAILABLE(macosx(10.13.2)); +@end + +#endif + // forward declare Access APIs typedef NSString* AVMediaType NS_EXTENSIBLE_STRING_ENUM; diff --git a/docs/api/system-preferences.md b/docs/api/system-preferences.md index c9bd8b8ab541..29ff9eea6d33 100644 --- a/docs/api/system-preferences.md +++ b/docs/api/system-preferences.md @@ -380,6 +380,32 @@ You can use the `setAppLevelAppearance` API to set this value. Sets the appearance setting for your application, this should override the system default and override the value of `getEffectiveAppearance`. +### `systemPreferences.canPromptTouchID()` _macOS_ + +Returns `Boolean` - whether or not this device has the ability to use Touch ID. + +**NOTE:** This API will return `false` on macOS systems older than Sierra 10.12.2. + +### `systemPreferences.promptTouchID(reason)` _macOS_ + +* `reason` String - The reason you are asking for Touch ID authentication + +Returns `Promise` - resolves if the user has successfully authenticated with Touch ID. + +```javascript +const { systemPreferences } = require('electron') + +systemPreferences.promptTouchID('To get consent for a Security-Gated Thing').then(success => { + console.log('You have successfully authenticated with Touch ID!') +}).catch(err => { + console.log(err) +}) +``` + +This API itself will not protect your user data; rather, it is a mechanism to allow you to do so. Native apps will need to set [Access Control Constants](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags?language=objc) like [`kSecAccessControlUserPresence`](https://developer.apple.com/documentation/security/secaccesscontrolcreateflags/ksecaccesscontroluserpresence?language=objc) on the their keychain entry so that reading it would auto-prompt for Touch ID biometric consent. This could be done with [`node-keytar`](https://github.com/atom/node-keytar), such that one would store an encryption key with `node-keytar` and only fetch it if `promptTouchID()` resolves. + +**NOTE:** This API will return a rejected Promise on macOS systems older than Sierra 10.12.2. + ### `systemPreferences.isTrustedAccessibilityClient(prompt)` _macOS_ * `prompt` Boolean - whether or not the user will be informed via prompt if the current process is untrusted.