Skip to content

Concurrency issues #470

@SergioMorchon

Description

@SergioMorchon

Bug report

Description

  • I use this plugin in a decoupled and concurrent way
  • The isAvailable response was sent into a loadBiometricSecret response, being secret a 'biometrics' literal. I suspect this happens from Fingerprint.java#executeIsAvailable.
  • I expect not to mix the responses across the different methods

Environment

  • Plugin version: 5.0.1
  • Android 13

Logs

// pseudocode
isAvailable();
isAvailable().then(() => loadBiometricSecret()).then(console.log) // sometimes logs the secret, sometimes logs 'biometric'. Maybe such 'biometric' literal comes from the previously `isAvailable` request, that resolved after this second call.
image

Proposal

To have different channels to resolve each type of response.

Mitigation

Having a lock for each kind of request, aiming to:

  • Debounce the requests of the same method
  • Queue the requests of different methods
    • It should ideally differentiate also the arguments, that's not in this proposal

This is what I have, if it helps:

type IsAvailableOptions = Readonly<{
    allowBackup: boolean;
}>;

type ShowOptions = Readonly<{
    title?: string;
    subtitle?: string;
    description?: string;
    fallbackButtonTitle?: string;
    disableBackup?: boolean;
    cancelButtonTitle?: string;
    confirmationRequired?: boolean;
}>;

type RegisterBiometricSecretOptions = Readonly<{
    title?: string;
    subtitle?: string;
    description?: string;
    fallbackButtonTitle?: string;
    disableBackup?: boolean;
    cancelButtonTitle?: string;
    confirmationRequired?: boolean;
    secret: string;
    invalidateOnEnrollment?: boolean;
}>;

type LoadBiometricSecretOptions = Readonly<{
    title?: string;
    subtitle?: string;
    description?: string;
    fallbackButtonTitle?: string;
    disableBackup?: boolean;
    cancelButtonTitle?: string;
    confirmationRequired?: boolean;
}>;

type Fingerprint = Readonly<{
    isAvailable: (
        onSuccess: (available: boolean) => void,
        onError?: (error: unknown) => void,
        options?: IsAvailableOptions
    ) => void;
    show: (
        options: ShowOptions,
        onSuccess: () => void,
        onError?: (error: unknown) => void
    ) => void;
    registerBiometricSecret: (
        options: RegisterBiometricSecretOptions,
        onSuccess: () => void,
        onError?: (error: unknown) => void
    ) => void;
    loadBiometricSecret: (
        options: LoadBiometricSecretOptions,
        onSuccess: (secret: string) => void,
        onError?: (error: unknown) => void
    ) => void;
    BIOMETRIC_UNKNOWN_ERROR: -100;
    BIOMETRIC_UNAVAILABLE: -101;
    BIOMETRIC_AUTHENTICATION_FAILED: -102;
    BIOMETRIC_SDK_NOT_SUPPORTED: -103;
    BIOMETRIC_HARDWARE_NOT_SUPPORTED: -104;
    BIOMETRIC_PERMISSION_NOT_GRANTED: -105;
    BIOMETRIC_NOT_ENROLLED: -106;
    BIOMETRIC_INTERNAL_PLUGIN_ERROR: -107;
    BIOMETRIC_DISMISSED: -108;
    BIOMETRIC_PIN_OR_PATTERN_DISMISSED: -109;
    BIOMETRIC_SCREEN_GUARD_UNSECURED: -110;
    BIOMETRIC_LOCKED_OUT: -111;
    BIOMETRIC_LOCKED_OUT_PERMANENT: -112;
    BIOMETRIC_NO_SECRET_FOUND: -113;
}>;

declare global {
    interface Window {
        readonly Fingerprint?: Fingerprint;
    }
}

let isAvailablePromise: Promise<boolean>;
let registerPromise: Promise<void>;
let loadPromise: Promise<string | null>;

const waitForOngoingPromise = () =>
    new Promise<void>(resolve =>
        (
            isAvailablePromise ??
            registerPromise ??
            loadPromise ??
            Promise.resolve()
        ).finally(resolve)
    );

export const isAvailable = async (options?: IsAvailableOptions) => {
    await waitForOngoingPromise();
    if (!isAvailablePromise) {
        isAvailablePromise = new Promise<boolean>(resolve => {
            if (!window.Fingerprint) {
                resolve(false);
                return;
            }

            window.Fingerprint.isAvailable(
                resolve,
                () => resolve(false),
                options
            );
        });
    }

    return isAvailablePromise;
};

const getNotAvailableError = () =>
    new Error('Fingerprint service not available');

const isDismissedError = (error: any) =>
    window.Fingerprint &&
    error?.code === window.Fingerprint.BIOMETRIC_DISMISSED;

export const registerBiometricSecret = async (
    options: RegisterBiometricSecretOptions
) => {
    await waitForOngoingPromise();
    if (!registerPromise) {
        registerPromise = new Promise<void>((resolve, reject) => {
            if (!window.Fingerprint) {
                reject(getNotAvailableError());
                return;
            }

            window.Fingerprint.registerBiometricSecret(
                options,
                resolve,
                error => (isDismissedError(error) ? resolve() : reject(error))
            );
        });
    }

    return registerPromise;
};

export const loadBiometricSecret = async (
    options?: LoadBiometricSecretOptions
) => {
    await waitForOngoingPromise();
    if (!loadPromise) {
        loadPromise = new Promise<string | null>((resolve, reject) => {
            if (!window.Fingerprint) {
                reject(getNotAvailableError());
                return;
            }

            window.Fingerprint.loadBiometricSecret(
                options ?? {},
                resolve,
                error =>
                    isDismissedError(error) ? resolve(null) : reject(error)
            );
        });
    }

    return loadPromise;
};

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions