Browse Source

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.
Shelley Vohr 6 years ago
parent
commit
46a24c82ff

+ 1 - 0
BUILD.gn

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

+ 2 - 0
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",

+ 4 - 0
atom/browser/api/atom_api_system_preferences.h

@@ -95,6 +95,10 @@ class SystemPreferences : public mate::EventEmitter<SystemPreferences>
 
   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);
 
   // TODO(codebytere): Write tests for these methods once we

+ 71 - 0
atom/browser/api/atom_api_system_preferences_mac.mm

@@ -8,14 +8,19 @@
 
 #import <AVFoundation/AVFoundation.h>
 #import <Cocoa/Cocoa.h>
+#import <LocalAuthentication/LocalAuthentication.h>
+#import <Security/Security.h>
 
 #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<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
 bool SystemPreferences::IsTrustedAccessibilityClient(bool prompt) {
   NSDictionary* options = @{(id)kAXTrustedCheckOptionPrompt : @(prompt)};

+ 17 - 0
atom/browser/mac/atom_application.h

@@ -7,6 +7,7 @@
 #include "base/mac/scoped_sending_event.h"
 
 #import <AVFoundation/AVFoundation.h>
+#import <LocalAuthentication/LocalAuthentication.h>
 
 // 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;
 

+ 26 - 0
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<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_
 
 * `prompt` Boolean - whether or not the user will be informed via prompt if the current process is untrusted.