// Copyright 2025 Signal Messenger, LLC // SPDX-License-Identifier: AGPL-3.0-only import { systemPreferences } from 'electron'; import { exec } from 'child_process'; import { checkAvailability as checkAvailabilityWindowsUcv, requestVerification as requestVerificationWindowsUcv, } from '@signalapp/windows-ucv'; import { createLogger } from '../../logging/log'; import OS from './osMain'; import { missingCaseError } from '../missingCaseError'; const log = createLogger('promptOSAuthMain'); export type PromptOSAuthReasonType = 'enable-backups' | 'view-aep'; export type PromptOSAuthResultType = | 'error' | 'success' | 'unauthorized' | 'unauthorized-no-windows-ucv' | 'unsupported'; /** * Use an OS API to request the user to authenticate themselves as a security check * before viewing sensitive account credentials. * Return values: 'success' indicates successful authentication. * 'unauthorized' indicates authentication is possible, but failed or was canceled. * 'unauthorized-no-windows-ucv' indicates the Windows API was not available or not setup. * Because this is the default case on Windows without Windows Hello enabled, * this response should be treated as an auth failure, and not bypassed. * 'unsupported' indicates the OS is not supported. Authentication can be skipped * or user asked to use a fallback method (e.g. using the primary mobile device). */ export async function promptOSAuth({ reason, localeString, }: { reason: PromptOSAuthReasonType; localeString: string | undefined; }): Promise { if (OS.isWindows()) { return promptOSAuthWindows(localeString ?? ''); } if (OS.isMacOS()) { return promptOSAuthMacOS(localeString ?? ''); } if (OS.isLinux()) { return promptOSAuthLinux(reason); } return 'unsupported'; } async function promptOSAuthMacOS( text: string ): Promise { try { await systemPreferences.promptTouchID(text); return 'success'; } catch { return 'unauthorized'; } } async function promptOSAuthWindows( text: string ): Promise { // For Windows a verification device is required for the UserConsentVerifier API. // If unavailable, then the UI must fail and require the user to setup verification. const availability = await checkAvailabilityWindowsUcv(); log.info(`Windows UCV availability=${availability}`); if (availability !== 'available') { return 'unauthorized-no-windows-ucv'; } const result = await requestVerificationWindowsUcv(text); if (result === 'verified') { return 'success'; } return 'unauthorized'; } async function promptOSAuthLinux( reason: PromptOSAuthReasonType ): Promise { const isAvailable = await isPromptOSAuthAvailableLinux(); if (!isAvailable) { return 'unsupported'; } // Avoid string interpolation in exec() command let command: string; if (reason === 'enable-backups') { command = 'pkcheck -u --process $$ --action-id org.signalapp.enable-backups'; } else if (reason === 'view-aep') { command = 'pkcheck -u --process $$ --action-id org.signalapp.view-aep'; } else { throw missingCaseError(reason); } return new Promise(resolve => { exec(command).on('exit', code => { if (code === 0) { resolve('success'); } else if (code === 3) { resolve('unauthorized'); } else { resolve('error'); } }); }); } async function isPromptOSAuthAvailableLinux(): Promise { return new Promise((resolve, _reject) => { exec('command -v pkcheck').on('exit', code => { resolve(code === 0); }); }); }