Skip to main content

priv/native/ios/mob_biometric_nif.m

/* mob_biometric_nif — iOS biometric tier-1 plugin NIF (Objective-C).
 *
 * Extracted from mob-core ios/mob_nif.m (nif_biometric_authenticate, lines
 * 2392-2420): LAContext evaluatePolicy with
 * LAPolicyDeviceOwnerAuthenticationWithBiometrics. Self-contained — core's
 * mob_send2 is a private static, so this ships its own (bio_send2). No
 * permission registration: biometric auth uses the device's existing
 * enrollment, there is no runtime permission dialog (Face ID's
 * NSFaceIDUsageDescription plist key is merged from the plugin manifest).
 * Compiled as ObjC (-fobjc-arc) via the plugin objc-NIF path (manifest
 * lang: :objc).
 */
#import <Foundation/Foundation.h>
#import <LocalAuthentication/LocalAuthentication.h>
#include <erl_nif.h>

// Self-contained {atom, atom} send (core's mob_send2 is a private static).
static void bio_send2(const ErlNifPid *pid, const char *a1, const char *a2) {
  ErlNifEnv *e = enif_alloc_env();
  ERL_NIF_TERM msg = enif_make_tuple2(e, enif_make_atom(e, a1), enif_make_atom(e, a2));
  enif_send(NULL, (ErlNifPid *)pid, e, msg);
  enif_free_env(e);
}

// ── Biometric authentication ──────────────────────────────────────────────
// Delivers {:biometric, :success | :failure | :not_available} to the caller.
//
// Outcome mapping is aligned with the Android bridge (platform BiometricPrompt,
// mob_biometric 0.1.3): only an explicit user/system/app cancellation is
// :failure; every other non-success (lockout, repeated mismatch, not
// available / not enrolled, passcode not set, …) is :not_available — matching
// Android's onAuthenticationError, where USER_CANCELED/CANCELED -> :failure and
// everything else -> :not_available.
static const char *bio_outcome_for_error(NSInteger code) {
  switch (code) {
    case LAErrorUserCancel:
    case LAErrorSystemCancel:
    case LAErrorAppCancel:
      return "failure";
    default:
      // LAErrorBiometryLockout / LAErrorAuthenticationFailed /
      // LAErrorBiometryNotAvailable / LAErrorBiometryNotEnrolled /
      // LAErrorPasscodeNotSet / LAErrorUserFallback / … -> not available.
      return "not_available";
  }
}

static ERL_NIF_TERM nif_biometric_authenticate(ErlNifEnv *env, int argc,
                                               const ERL_NIF_TERM argv[]) {
    ErlNifBinary bin;
    if (!enif_inspect_binary(env, argv[0], &bin) &&
        !enif_inspect_iolist_as_binary(env, argv[0], &bin))
        return enif_make_badarg(env);
    NSString *reason = [[NSString alloc] initWithBytes:bin.data
                                                length:bin.size
                                              encoding:NSUTF8StringEncoding];
    ErlNifPid pid;
    enif_self(env, &pid);

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      LAContext *ctx = [[LAContext alloc] init];
      NSError *err = nil;
      if ([ctx canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&err]) {
          [ctx evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
              localizedReason:reason
                        reply:^(BOOL ok, NSError *e) {
                          bio_send2(&pid, "biometric",
                                    ok ? "success" : bio_outcome_for_error(e.code));
                        }];
      } else {
          bio_send2(&pid, "biometric", "not_available");
      }
    });
    return enif_make_atom(env, "ok");
}

// ── Registration ──────────────────────────────────────────────────────────
static ErlNifFunc nif_funcs[] = {
    {"biometric_authenticate", 1, nif_biometric_authenticate, 0},
};

ERL_NIF_INIT(mob_biometric_nif, nif_funcs, NULL, NULL, NULL, NULL)