feat: add Touch ID authentication support for macOS (#16707)

This PR adds Touch ID authentication support for macOS with two new `SystemPreferences` methods.

1. `systemPreferences.promptForTouchID()` returns a Promise that resolves with `true` if successful and rejects with an error message if authentication could not be completed.
2. `systemPreferences.isTouchIDAvailable()` returns a Boolean that's `true` if this device is a Mac running a supported OS that has the necessary hardware for Touch ID and `false` otherwise.
This commit is contained in:
Shelley Vohr 2019-02-13 18:36:28 -08:00 committed by GitHub
parent 228805353f
commit 46a24c82ff
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 121 additions and 0 deletions

View file

@ -690,6 +690,7 @@ if (is_mac) {
libs = [ libs = [
"AVFoundation.framework", "AVFoundation.framework",
"Carbon.framework", "Carbon.framework",
"LocalAuthentication.framework",
"QuartzCore.framework", "QuartzCore.framework",
"Quartz.framework", "Quartz.framework",
"Security.framework", "Security.framework",

View file

@ -93,6 +93,8 @@ void SystemPreferences::BuildPrototype(
.SetMethod("setAppLevelAppearance", .SetMethod("setAppLevelAppearance",
&SystemPreferences::SetAppLevelAppearance) &SystemPreferences::SetAppLevelAppearance)
.SetMethod("getSystemColor", &SystemPreferences::GetSystemColor) .SetMethod("getSystemColor", &SystemPreferences::GetSystemColor)
.SetMethod("canPromptTouchID", &SystemPreferences::CanPromptTouchID)
.SetMethod("promptTouchID", &SystemPreferences::PromptTouchID)
.SetMethod("isTrustedAccessibilityClient", .SetMethod("isTrustedAccessibilityClient",
&SystemPreferences::IsTrustedAccessibilityClient) &SystemPreferences::IsTrustedAccessibilityClient)
.SetMethod("getMediaAccessStatus", .SetMethod("getMediaAccessStatus",

View file

@ -95,6 +95,10 @@ class SystemPreferences : public mate::EventEmitter<SystemPreferences>
std::string GetSystemColor(const std::string& color, mate::Arguments* args); std::string GetSystemColor(const std::string& color, mate::Arguments* args);
bool CanPromptTouchID();
v8::Local<v8::Promise> PromptTouchID(v8::Isolate* isolate,
const std::string& reason);
static bool IsTrustedAccessibilityClient(bool prompt); static bool IsTrustedAccessibilityClient(bool prompt);
// TODO(codebytere): Write tests for these methods once we // TODO(codebytere): Write tests for these methods once we

View file

@ -8,14 +8,19 @@
#import <AVFoundation/AVFoundation.h> #import <AVFoundation/AVFoundation.h>
#import <Cocoa/Cocoa.h> #import <Cocoa/Cocoa.h>
#import <LocalAuthentication/LocalAuthentication.h>
#import <Security/Security.h>
#include "atom/browser/mac/atom_application.h" #include "atom/browser/mac/atom_application.h"
#include "atom/browser/mac/dict_util.h" #include "atom/browser/mac/dict_util.h"
#include "atom/common/native_mate_converters/gurl_converter.h" #include "atom/common/native_mate_converters/gurl_converter.h"
#include "atom/common/native_mate_converters/value_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/mac/sdk_forward_declarations.h"
#include "base/sequenced_task_runner.h"
#include "base/strings/stringprintf.h" #include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h" #include "base/strings/sys_string_conversions.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "base/values.h" #include "base/values.h"
#include "native_mate/object_template_builder.h" #include "native_mate/object_template_builder.h"
#include "net/base/mac/url_conversions.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<LAContext> 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<util::Promise> promise) {
promise->Resolve();
}
void OnTouchIDFailed(scoped_refptr<util::Promise> promise,
const std::string& reason) {
promise->RejectWithErrorMessage(reason);
}
v8::Local<v8::Promise> SystemPreferences::PromptTouchID(
v8::Isolate* isolate,
const std::string& reason) {
scoped_refptr<util::Promise> promise = new util::Promise(isolate);
if (@available(macOS 10.12.2, *)) {
base::scoped_nsobject<LAContext> context([[LAContext alloc] init]);
base::ScopedCFTypeRef<SecAccessControlRef> access_control =
base::ScopedCFTypeRef<SecAccessControlRef>(
SecAccessControlCreateWithFlags(
kCFAllocatorDefault,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
kSecAccessControlPrivateKeyUsage |
kSecAccessControlUserPresence,
nullptr));
scoped_refptr<base::SequencedTaskRunner> 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 // static
bool SystemPreferences::IsTrustedAccessibilityClient(bool prompt) { bool SystemPreferences::IsTrustedAccessibilityClient(bool prompt) {
NSDictionary* options = @{(id)kAXTrustedCheckOptionPrompt : @(prompt)}; NSDictionary* options = @{(id)kAXTrustedCheckOptionPrompt : @(prompt)};

View file

@ -7,6 +7,7 @@
#include "base/mac/scoped_sending_event.h" #include "base/mac/scoped_sending_event.h"
#import <AVFoundation/AVFoundation.h> #import <AVFoundation/AVFoundation.h>
#import <LocalAuthentication/LocalAuthentication.h>
// Forward Declare Appearance APIs // Forward Declare Appearance APIs
@interface NSApplication (HighSierraSDK) @interface NSApplication (HighSierraSDK)
@ -16,6 +17,22 @@
- (void)setAppearance:(NSAppearance*)appearance API_AVAILABLE(macosx(10.14)); - (void)setAppearance:(NSAppearance*)appearance API_AVAILABLE(macosx(10.14));
@end @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 // forward declare Access APIs
typedef NSString* AVMediaType NS_EXTENSIBLE_STRING_ENUM; typedef NSString* AVMediaType NS_EXTENSIBLE_STRING_ENUM;

View file

@ -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 Sets the appearance setting for your application, this should override the
system default and override the value of `getEffectiveAppearance`. 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<void>` - 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_ ### `systemPreferences.isTrustedAccessibilityClient(prompt)` _macOS_
* `prompt` Boolean - whether or not the user will be informed via prompt if the current process is untrusted. * `prompt` Boolean - whether or not the user will be informed via prompt if the current process is untrusted.