mirror of
https://github.com/koodo-reader/koodo-reader.git
synced 2026-06-16 03:40:53 -04:00
feat: implement biometric authentication support with Windows Hello and Touch ID
This commit is contained in:
201
main.js
201
main.js
@@ -11,12 +11,14 @@ const {
|
||||
nativeTheme,
|
||||
protocol,
|
||||
screen,
|
||||
systemPreferences,
|
||||
} = require("electron");
|
||||
const path = require("path");
|
||||
const isDev = require("electron-is-dev");
|
||||
const Store = require("electron-store");
|
||||
const log = require("electron-log/main");
|
||||
const os = require("os");
|
||||
const { execFile } = require("child_process");
|
||||
const store = new Store();
|
||||
const fs = require("fs");
|
||||
const JSZip = require("jszip");
|
||||
@@ -41,6 +43,199 @@ let syncUtilCache = {};
|
||||
let pickerUtilCache = {};
|
||||
let downloadRequest = null;
|
||||
|
||||
const runPowerShellScript = (script) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const encodedCommand = Buffer.from(script, "utf16le").toString("base64");
|
||||
execFile(
|
||||
"powershell.exe",
|
||||
[
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-EncodedCommand",
|
||||
encodedCommand,
|
||||
],
|
||||
{
|
||||
windowsHide: true,
|
||||
timeout: 30000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
},
|
||||
(error, stdout, stderr) => {
|
||||
if (error) {
|
||||
reject(
|
||||
new Error((stderr || stdout || error.message || "").trim() || "")
|
||||
);
|
||||
return;
|
||||
}
|
||||
resolve((stdout || "").trim());
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const getWindowsHelloScript = (mode, message = "") => {
|
||||
const escapedMessage = message.replace(/'/g, "''");
|
||||
return `
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Add-Type -AssemblyName System.Runtime.WindowsRuntime
|
||||
|
||||
function Invoke-WinRtAsync {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)] $Operation,
|
||||
[Parameter(Mandatory = $true)] [Type[]] $ResultTypes
|
||||
)
|
||||
|
||||
$method = [System.WindowsRuntimeSystemExtensions].GetMethods() |
|
||||
Where-Object {
|
||||
$_.Name -eq 'AsTask' -and
|
||||
$_.IsGenericMethodDefinition -and
|
||||
$_.GetGenericArguments().Count -eq $ResultTypes.Count -and
|
||||
$_.GetParameters().Count -eq 1
|
||||
} |
|
||||
Select-Object -First 1
|
||||
|
||||
if (-not $method) {
|
||||
throw 'Unable to bridge Windows Runtime async operation.'
|
||||
}
|
||||
|
||||
$genericMethod = $method.MakeGenericMethod($ResultTypes)
|
||||
$task = $genericMethod.Invoke($null, @($Operation))
|
||||
return $task.GetAwaiter().GetResult()
|
||||
}
|
||||
|
||||
$verifier = [Windows.Security.Credentials.UI.UserConsentVerifier, Windows.Security.Credentials.UI, ContentType = WindowsRuntime]
|
||||
$availability = Invoke-WinRtAsync -Operation ($verifier::CheckAvailabilityAsync()) -ResultTypes @([Windows.Security.Credentials.UI.UserConsentVerifierAvailability])
|
||||
|
||||
if ('${mode}' -eq 'check') {
|
||||
[Console]::Out.Write((@{
|
||||
available = ($availability.ToString() -eq 'Available')
|
||||
status = $availability.ToString()
|
||||
} | ConvertTo-Json -Compress))
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($availability.ToString() -ne 'Available') {
|
||||
[Console]::Out.Write((@{
|
||||
success = $false
|
||||
code = 'Unavailable'
|
||||
status = $availability.ToString()
|
||||
} | ConvertTo-Json -Compress))
|
||||
exit 0
|
||||
}
|
||||
|
||||
$result = Invoke-WinRtAsync -Operation ($verifier::RequestVerificationAsync('${escapedMessage}')) -ResultTypes @([Windows.Security.Credentials.UI.UserConsentVerificationResult])
|
||||
|
||||
[Console]::Out.Write((@{
|
||||
success = ($result.ToString() -eq 'Verified')
|
||||
code = $result.ToString()
|
||||
status = $availability.ToString()
|
||||
} | ConvertTo-Json -Compress))
|
||||
`.trim();
|
||||
};
|
||||
|
||||
const getBiometricCapability = async () => {
|
||||
if (process.platform === "darwin") {
|
||||
const available =
|
||||
typeof systemPreferences.canPromptTouchID === "function" &&
|
||||
systemPreferences.canPromptTouchID();
|
||||
return {
|
||||
available,
|
||||
provider: "Touch ID",
|
||||
platform: process.platform,
|
||||
status: available ? "Available" : "Unavailable",
|
||||
};
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
try {
|
||||
const output = await runPowerShellScript(getWindowsHelloScript("check"));
|
||||
const result = output ? JSON.parse(output) : {};
|
||||
return {
|
||||
available: !!result.available,
|
||||
provider: "Windows Hello",
|
||||
platform: process.platform,
|
||||
status: result.status || "Unavailable",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
available: false,
|
||||
provider: "Windows Hello",
|
||||
platform: process.platform,
|
||||
status: "Error",
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
available: false,
|
||||
provider: "Biometric",
|
||||
platform: process.platform,
|
||||
status: "Unsupported",
|
||||
};
|
||||
};
|
||||
|
||||
const promptBiometricAuth = async (promptMessage = "Authenticate") => {
|
||||
if (process.platform === "darwin") {
|
||||
const available =
|
||||
typeof systemPreferences.canPromptTouchID === "function" &&
|
||||
systemPreferences.canPromptTouchID();
|
||||
if (!available) {
|
||||
return {
|
||||
success: false,
|
||||
code: "Unavailable",
|
||||
provider: "Touch ID",
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
await systemPreferences.promptTouchID(promptMessage);
|
||||
return {
|
||||
success: true,
|
||||
code: "Verified",
|
||||
provider: "Touch ID",
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
success: false,
|
||||
code: /cancel/i.test(message) ? "Canceled" : "Failed",
|
||||
provider: "Touch ID",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === "win32") {
|
||||
try {
|
||||
const output = await runPowerShellScript(
|
||||
getWindowsHelloScript("verify", promptMessage)
|
||||
);
|
||||
const result = output ? JSON.parse(output) : {};
|
||||
return {
|
||||
success: !!result.success,
|
||||
code:
|
||||
result.code === "Unavailable" && result.status
|
||||
? result.status
|
||||
: result.code || "Error",
|
||||
provider: "Windows Hello",
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
code: "Error",
|
||||
provider: "Windows Hello",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
code: "Unsupported",
|
||||
provider: "Biometric",
|
||||
};
|
||||
};
|
||||
|
||||
// Discord Rich Presence setup
|
||||
let discordRPCClient = null;
|
||||
let discordRPCReady = false;
|
||||
@@ -935,6 +1130,12 @@ const createMainWin = () => {
|
||||
ipcMain.handle("get-store-value", async (event, config) => {
|
||||
return store.get(config.key);
|
||||
});
|
||||
ipcMain.handle("get-biometric-capability", async () => {
|
||||
return await getBiometricCapability();
|
||||
});
|
||||
ipcMain.handle("prompt-biometric-auth", async (event, config) => {
|
||||
return await promptBiometricAuth(config?.message);
|
||||
});
|
||||
|
||||
ipcMain.handle("reset-reader-position", async (event) => {
|
||||
store.delete("windowX");
|
||||
|
||||
@@ -1033,6 +1033,7 @@
|
||||
"PINs do not match, please try again": "PIN 不匹配,请重试",
|
||||
"Incorrect password": "密码错误",
|
||||
"Incorrect PIN": "PIN 错误",
|
||||
"Biometric": "生物识别",
|
||||
"Select protection method": "选择保护方法",
|
||||
"Enter your current password": "输入您当前的密码",
|
||||
"Enter new password": "输入新密码",
|
||||
@@ -1046,6 +1047,15 @@
|
||||
"Protection method": "保护方法",
|
||||
"Use a custom password to protect the app": "使用自定义密码保护应用程序",
|
||||
"Use a 6-digit PIN to protect the app": "使用 6 位数 PIN 保护应用程序",
|
||||
"Use Touch ID or Windows Hello to protect the app": "使用 Touch ID 或 Windows Hello 保护应用程序",
|
||||
"Use biometric authentication to unlock the app": "使用生物识别解锁应用程序",
|
||||
"Verify with biometrics": "使用生物识别验证",
|
||||
"Authenticating with biometrics...": "正在进行生物识别验证...",
|
||||
"Biometric authentication failed, please try again": "生物识别验证失败,请重试",
|
||||
"Biometric authentication is not available on this device": "此设备当前不可用生物识别验证",
|
||||
"Authenticate to unlock the app": "验证以解锁应用",
|
||||
"Authenticate to change protection settings": "验证以更改保护设置",
|
||||
"Authenticate to enable biometric protection": "验证以启用生物识别保护",
|
||||
"No local dictionaries imported yet": "未导入任何本地词典",
|
||||
"Due to browser security restrictions, you may not be able to use this data source properly. If you encounter any issues, you can resolve them by installing our browser extension.": "由于浏览器的安全限制,您可能无法正常使用此数据源。如果您遇到任何问题,可以通过安装我们的浏览器扩展来解决。",
|
||||
"Install extension": "安装扩展",
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import React from "react";
|
||||
import "./protection.css";
|
||||
import { TokenService } from "../../assets/lib/kookit-extra-browser.min";
|
||||
import { verifyPassword, verifyPin } from "../../utils/protectionUtil";
|
||||
import {
|
||||
getBiometricErrorMessage,
|
||||
promptBiometricAuth,
|
||||
verifyPassword,
|
||||
verifyPin,
|
||||
} from "../../utils/protectionUtil";
|
||||
import { vexPasswordInputAsync } from "../../utils/common";
|
||||
import toast from "react-hot-toast";
|
||||
import i18n from "../../i18n";
|
||||
@@ -12,6 +17,8 @@ interface ProtectionOverlayState {
|
||||
method: string;
|
||||
pinValue: string;
|
||||
pinError: boolean;
|
||||
biometricError: string;
|
||||
isAuthenticating: boolean;
|
||||
}
|
||||
|
||||
class ProtectionOverlay extends React.Component<{}, ProtectionOverlayState> {
|
||||
@@ -24,6 +31,8 @@ class ProtectionOverlay extends React.Component<{}, ProtectionOverlayState> {
|
||||
method: "",
|
||||
pinValue: "",
|
||||
pinError: false,
|
||||
biometricError: "",
|
||||
isAuthenticating: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,8 +40,10 @@ class ProtectionOverlay extends React.Component<{}, ProtectionOverlayState> {
|
||||
const method = (await TokenService.getToken("protection_method")) || "";
|
||||
if (method) {
|
||||
this.setState({ isVisible: true, method }, () => {
|
||||
if (method !== "pin") {
|
||||
if (method === "password") {
|
||||
this.startPasswordAuth();
|
||||
} else if (method === "biometric") {
|
||||
this.startBiometricAuth();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -61,6 +72,27 @@ class ProtectionOverlay extends React.Component<{}, ProtectionOverlayState> {
|
||||
this.setState({ isVisible: false });
|
||||
};
|
||||
|
||||
startBiometricAuth = async () => {
|
||||
if (this.state.isAuthenticating) return;
|
||||
|
||||
this.setState({
|
||||
isAuthenticating: true,
|
||||
biometricError: "",
|
||||
});
|
||||
const result = await promptBiometricAuth(
|
||||
i18n.t("Authenticate to unlock the app")
|
||||
);
|
||||
if (result.success) {
|
||||
this.setState({ isVisible: false, isAuthenticating: false });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
isAuthenticating: false,
|
||||
biometricError: getBiometricErrorMessage(result.code, i18n.t.bind(i18n)),
|
||||
});
|
||||
};
|
||||
|
||||
waitForPin = (): Promise<string> => {
|
||||
return new Promise((resolve) => {
|
||||
this.pinResolve = resolve;
|
||||
@@ -91,7 +123,14 @@ class ProtectionOverlay extends React.Component<{}, ProtectionOverlayState> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isVisible, method, pinValue, pinError } = this.state;
|
||||
const {
|
||||
isVisible,
|
||||
method,
|
||||
pinValue,
|
||||
pinError,
|
||||
biometricError,
|
||||
isAuthenticating,
|
||||
} = this.state;
|
||||
if (!isVisible) return null;
|
||||
|
||||
if (method === "pin") {
|
||||
@@ -169,6 +208,32 @@ class ProtectionOverlay extends React.Component<{}, ProtectionOverlayState> {
|
||||
);
|
||||
}
|
||||
|
||||
if (method === "biometric") {
|
||||
return (
|
||||
<div className="protection-overlay">
|
||||
<div className="biometric-auth-container">
|
||||
<div className="pin-keypad-title">
|
||||
{i18n.t("Use biometric authentication to unlock the app")}
|
||||
</div>
|
||||
{biometricError && (
|
||||
<div className="pin-error-msg">{biometricError}</div>
|
||||
)}
|
||||
<button
|
||||
className="biometric-auth-button"
|
||||
onClick={this.startBiometricAuth}
|
||||
disabled={isAuthenticating}
|
||||
>
|
||||
{isAuthenticating ? (
|
||||
<Trans>Authenticating with biometrics...</Trans>
|
||||
) : (
|
||||
<Trans>Verify with biometrics</Trans>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <div className="protection-overlay" />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,17 @@
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.biometric-auth-container {
|
||||
background: var(--background-color-1, #fff);
|
||||
border-radius: 16px;
|
||||
padding: 32px 28px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
min-width: 320px;
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.pin-keypad-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
@@ -125,3 +136,34 @@
|
||||
.pin-key-cancel:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.biometric-auth-button {
|
||||
margin-top: 8px;
|
||||
min-width: 220px;
|
||||
min-height: 44px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: var(--primary-color, rgba(75, 75, 75, 1));
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
padding: 0 18px;
|
||||
transition:
|
||||
opacity 0.12s,
|
||||
transform 0.12s;
|
||||
}
|
||||
|
||||
.biometric-auth-button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.biometric-auth-button:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.biometric-auth-button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ import toast from "react-hot-toast";
|
||||
import { TokenService } from "../../../assets/lib/kookit-extra-browser.min";
|
||||
import {
|
||||
clearProtection,
|
||||
getBiometricCapability,
|
||||
getBiometricErrorMessage,
|
||||
promptBiometricAuth,
|
||||
setProtectionBiometric,
|
||||
verifyPassword,
|
||||
verifyPin,
|
||||
setProtectionPassword,
|
||||
@@ -18,6 +22,7 @@ class MoreSetting extends React.Component<MoreSettingProps, MoreSettingState> {
|
||||
super(props);
|
||||
this.state = {
|
||||
protectionMethod: "",
|
||||
biometricAvailable: false,
|
||||
isLoading: true,
|
||||
pinInputMode: "none",
|
||||
pinValue: "",
|
||||
@@ -27,8 +32,15 @@ class MoreSetting extends React.Component<MoreSettingProps, MoreSettingState> {
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const method = (await TokenService.getToken("protection_method")) || "";
|
||||
this.setState({ protectionMethod: method, isLoading: false });
|
||||
const [method, biometricCapability] = await Promise.all([
|
||||
TokenService.getToken("protection_method"),
|
||||
getBiometricCapability(),
|
||||
]);
|
||||
this.setState({
|
||||
protectionMethod: method || "",
|
||||
biometricAvailable: biometricCapability.available,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
|
||||
showPinKeypad = (mode: "setup" | "verify"): Promise<string | false> => {
|
||||
@@ -109,12 +121,20 @@ class MoreSetting extends React.Component<MoreSettingProps, MoreSettingState> {
|
||||
const ok = await verifyPin(pin as string);
|
||||
if (!ok) toast.error(this.props.t("Incorrect PIN"));
|
||||
return ok;
|
||||
} else if (protectionMethod === "biometric") {
|
||||
const result = await promptBiometricAuth(
|
||||
i18n.t("Authenticate to change protection settings")
|
||||
);
|
||||
if (!result.success) {
|
||||
toast.error(getBiometricErrorMessage(result.code, this.props.t));
|
||||
}
|
||||
return result.success;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
handleToggleProtection = async () => {
|
||||
const { protectionMethod } = this.state;
|
||||
const { protectionMethod, biometricAvailable } = this.state;
|
||||
if (protectionMethod) {
|
||||
const verified = await this.verifyCurrentMethod();
|
||||
if (!verified) return;
|
||||
@@ -126,6 +146,9 @@ class MoreSetting extends React.Component<MoreSettingProps, MoreSettingState> {
|
||||
{ value: "password", label: "Password" },
|
||||
{ value: "pin", label: "PIN" },
|
||||
];
|
||||
if (biometricAvailable) {
|
||||
methodOptions.push({ value: "biometric", label: "Biometric" });
|
||||
}
|
||||
const method = await vexSelectAsync(
|
||||
"Select protection method",
|
||||
methodOptions
|
||||
@@ -162,6 +185,23 @@ class MoreSetting extends React.Component<MoreSettingProps, MoreSettingState> {
|
||||
await setProtectionPin(pin as string);
|
||||
this.setState({ protectionMethod: "pin" });
|
||||
toast.success(this.props.t("Change successful"));
|
||||
} else if (method === "biometric") {
|
||||
if (!this.state.biometricAvailable) {
|
||||
toast.error(
|
||||
this.props.t("Biometric authentication is not available on this device")
|
||||
);
|
||||
return;
|
||||
}
|
||||
const result = await promptBiometricAuth(
|
||||
i18n.t("Authenticate to enable biometric protection")
|
||||
);
|
||||
if (!result.success) {
|
||||
toast.error(getBiometricErrorMessage(result.code, this.props.t));
|
||||
return;
|
||||
}
|
||||
await setProtectionBiometric();
|
||||
this.setState({ protectionMethod: "biometric" });
|
||||
toast.success(this.props.t("Change successful"));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -248,8 +288,10 @@ class MoreSetting extends React.Component<MoreSettingProps, MoreSettingState> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { protectionMethod, isLoading } = this.state;
|
||||
const { protectionMethod, biometricAvailable, isLoading } = this.state;
|
||||
const isEnabled = !!protectionMethod;
|
||||
const showBiometricOption =
|
||||
biometricAvailable || protectionMethod === "biometric";
|
||||
|
||||
if (isLoading) {
|
||||
return null;
|
||||
@@ -301,6 +343,9 @@ class MoreSetting extends React.Component<MoreSettingProps, MoreSettingState> {
|
||||
>
|
||||
<option value="password">{this.props.t("Password")}</option>
|
||||
<option value="pin">{this.props.t("PIN")}</option>
|
||||
{showBiometricOption && (
|
||||
<option value="biometric">{this.props.t("Biometric")}</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
<p className="setting-option-subtitle">
|
||||
@@ -310,6 +355,9 @@ class MoreSetting extends React.Component<MoreSettingProps, MoreSettingState> {
|
||||
{protectionMethod === "pin" && (
|
||||
<Trans>Use a 6-digit PIN to protect the app</Trans>
|
||||
)}
|
||||
{protectionMethod === "biometric" && (
|
||||
<Trans>Use Touch ID or Windows Hello to protect the app</Trans>
|
||||
)}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface MoreSettingProps extends RouteComponentProps<any> {
|
||||
|
||||
export interface MoreSettingState {
|
||||
protectionMethod: string;
|
||||
biometricAvailable: boolean;
|
||||
isLoading: boolean;
|
||||
pinInputMode: "none" | "setup-enter" | "setup-confirm" | "verify";
|
||||
pinValue: string;
|
||||
|
||||
@@ -2,6 +2,27 @@ import { TokenService } from "../assets/lib/kookit-extra-browser.min";
|
||||
|
||||
declare var window: any;
|
||||
|
||||
export interface BiometricCapability {
|
||||
available: boolean;
|
||||
provider: string;
|
||||
platform: string;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface BiometricAuthResult {
|
||||
success: boolean;
|
||||
code: string;
|
||||
provider?: string;
|
||||
}
|
||||
|
||||
const isElectronRenderer = () => {
|
||||
return (
|
||||
typeof window !== "undefined" &&
|
||||
typeof window.require === "function" &&
|
||||
!!window.require("electron")?.ipcRenderer
|
||||
);
|
||||
};
|
||||
|
||||
export async function sha256(text: string): Promise<string> {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(text);
|
||||
@@ -28,6 +49,63 @@ export async function verifyPin(input: string): Promise<boolean> {
|
||||
return hash === stored;
|
||||
}
|
||||
|
||||
export async function getBiometricCapability(): Promise<BiometricCapability> {
|
||||
if (!isElectronRenderer()) {
|
||||
return {
|
||||
available: false,
|
||||
provider: "Biometric",
|
||||
platform: "web",
|
||||
status: "Unsupported",
|
||||
};
|
||||
}
|
||||
|
||||
return await window
|
||||
.require("electron")
|
||||
.ipcRenderer.invoke("get-biometric-capability");
|
||||
}
|
||||
|
||||
export async function promptBiometricAuth(
|
||||
message: string
|
||||
): Promise<BiometricAuthResult> {
|
||||
if (!isElectronRenderer()) {
|
||||
return {
|
||||
success: false,
|
||||
code: "Unsupported",
|
||||
provider: "Biometric",
|
||||
};
|
||||
}
|
||||
|
||||
return await window.require("electron").ipcRenderer.invoke(
|
||||
"prompt-biometric-auth",
|
||||
{
|
||||
message,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function getBiometricErrorMessage(
|
||||
code: string,
|
||||
t: (title: string) => string
|
||||
): string {
|
||||
if (code === "Canceled" || code === "Cancelled") {
|
||||
return t("Authentication required to access the app");
|
||||
}
|
||||
|
||||
if (
|
||||
[
|
||||
"Unavailable",
|
||||
"Unsupported",
|
||||
"DeviceNotPresent",
|
||||
"NotConfiguredForUser",
|
||||
"DisabledByPolicy",
|
||||
].includes(code)
|
||||
) {
|
||||
return t("Biometric authentication is not available on this device");
|
||||
}
|
||||
|
||||
return t("Biometric authentication failed, please try again");
|
||||
}
|
||||
|
||||
export async function setProtectionPassword(password: string): Promise<void> {
|
||||
const hash = await sha256(password);
|
||||
await TokenService.deleteToken("protection_pin");
|
||||
@@ -42,6 +120,12 @@ export async function setProtectionPin(pin: string): Promise<void> {
|
||||
await TokenService.setToken("protection_method", "pin");
|
||||
}
|
||||
|
||||
export async function setProtectionBiometric(): Promise<void> {
|
||||
await TokenService.deleteToken("protection_password");
|
||||
await TokenService.deleteToken("protection_pin");
|
||||
await TokenService.setToken("protection_method", "biometric");
|
||||
}
|
||||
|
||||
export async function clearProtection(): Promise<void> {
|
||||
await TokenService.deleteToken("protection_method");
|
||||
await TokenService.deleteToken("protection_password");
|
||||
|
||||
Reference in New Issue
Block a user