Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Concurrency issues #470

Open
SergioMorchon opened this issue Aug 1, 2023 · 0 comments
Open

Concurrency issues #470

SergioMorchon opened this issue Aug 1, 2023 · 0 comments
Labels

Comments

@SergioMorchon
Copy link

SergioMorchon commented Aug 1, 2023

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;
};
@SergioMorchon SergioMorchon changed the title Concurrent issues Concurrency issues Aug 1, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant