1 import { KEY_LENGTH_BYTES } from '@proton/crypto/lib/subtle/aesGcm';
2 import { uint8ArrayToString } from '@proton/shared/lib/helpers/encoding';
4 import { biometric as winBiometrics } from '../../../native';
5 import type { BiometricsFactory, BiometricsPlatformHandler } from './types';
7 const factory: BiometricsFactory = (getWindow) => {
8 const checkPresence = async (reason = 'Proton Pass wants to unlock') => {
9 const handle = getWindow()?.getNativeWindowHandle();
10 if (!handle) return false;
11 return winBiometrics.checkPresence(handle, reason).catch(() => false);
14 const biometrics: BiometricsPlatformHandler = {
15 canCheckPresence: () => winBiometrics.canCheckPresence().catch(() => false),
16 checkPresence: (_, reason) => checkPresence(reason),
17 getDecryptionKey: (_, challenge) => winBiometrics.getDecryptionKey(challenge).catch(() => null),
18 getSecret: async (_, key, version) => {
19 if (!(await checkPresence())) throw new Error('Biometric authentication failed');
21 const secretBytes = await winBiometrics.getSecret(key).catch(() => null);
22 if (!secretBytes) return null;
24 /** Version 1 (Legacy): Secrets were stored as UTF-16 encoded strings in a Vec<u8>,
25 * retrieved via `U16String::from_ptr`. Now stored as `Uint8`. For retrocompatibility:
26 * 1. Reinterpret Uint8Array, extracting first byte of each UTF-16 pair
27 * 2. Slice to KEY_LENGTH_BYTES for correct secret length */
29 const secretBytesUint8 = (() => {
30 const bytes = new Uint8Array(Math.floor(secretBytes.length / 2));
31 for (let i = 0, j = 0; i < secretBytes.length; i += 2, j++) bytes[j] = secretBytes[i];
32 return bytes.slice(0, KEY_LENGTH_BYTES);
35 return uint8ArrayToString(secretBytesUint8);
38 /* Version 2+: Secrets are stored as raw byte arrays without
39 * any conversions to and from strings */
40 return uint8ArrayToString(secretBytes);
42 setSecret: (_, key, secret) => winBiometrics.setSecret(key, secret),
43 deleteSecret: (_, key) => winBiometrics.deleteSecret(key),
49 export default factory;