mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-18 16:54:25 +08:00
support use face id/touch id for application lock
This commit is contained in:
@@ -6,6 +6,7 @@ const serverSettingsCookieKey = 'ACP_SETTINGS';
|
||||
const defaultSettings = {
|
||||
lang: 'en',
|
||||
applicationLock: false,
|
||||
applicationLockWebAuthn: false,
|
||||
autoUpdateExchangeRatesData: true,
|
||||
thousandsSeparator: true,
|
||||
currencyDisplayMode: 'code', // or 'none' or 'name'
|
||||
@@ -76,6 +77,8 @@ export default {
|
||||
setLanguage: value => setOption('lang', value),
|
||||
isEnableApplicationLock: () => getOption('applicationLock'),
|
||||
setEnableApplicationLock: value => setOption('applicationLock', value),
|
||||
isEnableApplicationLockWebAuthn: () => getOption('applicationLockWebAuthn'),
|
||||
setEnableApplicationLockWebAuthn: value => setOption('applicationLockWebAuthn', value),
|
||||
isAutoUpdateExchangeRatesData: () => getOption('autoUpdateExchangeRatesData'),
|
||||
setAutoUpdateExchangeRatesData: value => setOption('autoUpdateExchangeRatesData', value),
|
||||
isEnableThousandsSeparator: () => getOption('thousandsSeparator'),
|
||||
|
||||
+49
-2
@@ -6,7 +6,9 @@ import utils from './utils.js';
|
||||
const APP_LOCK_SECRET_BASE_STRING_PREFIX = 'LAB_LOCK_SECRET_';
|
||||
|
||||
const tokenLocalStorageKey = 'lab_user_token';
|
||||
const webauthnConfigLocalStorageKey = 'lab_user_webauthn_config';
|
||||
const userInfoLocalStorageKey = 'lab_user_info';
|
||||
|
||||
const tokenSessionStorageKey = 'lab_user_session_token';
|
||||
const appLockSecretSessionStorageKey = 'lab_user_app_lock_secret';
|
||||
|
||||
@@ -36,6 +38,11 @@ function getUserInfo() {
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
function getUserAppLockSecret() {
|
||||
const currentSecret = sessionStorage.getItem(appLockSecretSessionStorageKey);
|
||||
return currentSecret;
|
||||
}
|
||||
|
||||
function isUserLogined() {
|
||||
return !!localStorage.getItem(tokenLocalStorageKey);
|
||||
}
|
||||
@@ -52,7 +59,42 @@ function isUserUnlocked() {
|
||||
return !!sessionStorage.getItem(appLockSecretSessionStorageKey) && !!sessionStorage.getItem(tokenSessionStorageKey);
|
||||
}
|
||||
|
||||
function unlockToken(pinCode) {
|
||||
function getWebAuthnCredentialId() {
|
||||
const webauthnConfigData = localStorage.getItem(webauthnConfigLocalStorageKey);
|
||||
const webauthnConfig = JSON.parse(webauthnConfigData);
|
||||
|
||||
return webauthnConfig.credentialId;
|
||||
}
|
||||
|
||||
function saveWebAuthnConfig(credentialId) {
|
||||
const webAuthnConfig = {
|
||||
credentialId: credentialId
|
||||
};
|
||||
|
||||
localStorage.setItem(webauthnConfigLocalStorageKey, JSON.stringify(webAuthnConfig));
|
||||
}
|
||||
|
||||
function clearWebAuthnConfig() {
|
||||
localStorage.removeItem(webauthnConfigLocalStorageKey);
|
||||
}
|
||||
|
||||
function unlockTokenByWebAuthn(credentialId, userSecret) {
|
||||
const webauthnConfigData = localStorage.getItem(webauthnConfigLocalStorageKey);
|
||||
const webauthnConfig = JSON.parse(webauthnConfigData);
|
||||
|
||||
if (webauthnConfig.credentialId !== credentialId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const encryptedToken = localStorage.getItem(tokenLocalStorageKey);
|
||||
const secret = userSecret;
|
||||
const token = getDecryptedToken(encryptedToken, secret);
|
||||
|
||||
sessionStorage.setItem(appLockSecretSessionStorageKey, secret);
|
||||
sessionStorage.setItem(tokenSessionStorageKey, token);
|
||||
}
|
||||
|
||||
function unlockTokenByPinCode(pinCode) {
|
||||
const encryptedToken = localStorage.getItem(tokenLocalStorageKey);
|
||||
const secret = getAppLockSecret(pinCode);
|
||||
const token = getDecryptedToken(encryptedToken, secret);
|
||||
@@ -127,9 +169,14 @@ function clearTokenAndUserInfo() {
|
||||
export default {
|
||||
getToken,
|
||||
getUserInfo,
|
||||
getUserAppLockSecret,
|
||||
isUserLogined,
|
||||
isUserUnlocked,
|
||||
unlockToken,
|
||||
getWebAuthnCredentialId,
|
||||
saveWebAuthnConfig,
|
||||
clearWebAuthnConfig,
|
||||
unlockTokenByWebAuthn,
|
||||
unlockTokenByPinCode,
|
||||
encryptToken,
|
||||
decryptToken,
|
||||
isCorrectPinCode,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import CryptoJS from "crypto-js";
|
||||
import uaParser from 'ua-parser-js';
|
||||
import accountConstants from '../consts/account.js';
|
||||
|
||||
@@ -29,6 +30,27 @@ function isBoolean(val) {
|
||||
return typeof(val) === 'boolean';
|
||||
}
|
||||
|
||||
function base64encode(arrayBuffer) {
|
||||
if (!arrayBuffer || arrayBuffer.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
|
||||
}
|
||||
|
||||
function arrayBufferToString(arrayBuffer) {
|
||||
return String.fromCharCode.apply(null, new Uint8Array(arrayBuffer));
|
||||
}
|
||||
|
||||
function stringToArrayBuffer(str){
|
||||
return Uint8Array.from(str, c => c.charCodeAt(0)).buffer;
|
||||
}
|
||||
|
||||
function generateRandomString() {
|
||||
const baseString = 'lab_' + Math.round(new Date().getTime() / 1000) + '_' + Math.random();
|
||||
return CryptoJS.SHA256(baseString).toString();
|
||||
}
|
||||
|
||||
function parseUserAgent(ua) {
|
||||
const uaParseRet = uaParser(ua);
|
||||
|
||||
@@ -129,6 +151,10 @@ export default {
|
||||
isString,
|
||||
isNumber,
|
||||
isBoolean,
|
||||
base64encode,
|
||||
arrayBufferToString,
|
||||
stringToArrayBuffer,
|
||||
generateRandomString,
|
||||
parseUserAgent,
|
||||
getCategorizedAccounts,
|
||||
getAccountByAccountId,
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import CBOR from 'cbor-js';
|
||||
import utils from './utils.js';
|
||||
|
||||
const PUBLIC_KEY_CREDENTIAL_CREATION_OPTIONS_TEMPLATE = {
|
||||
attestation: "none",
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment: 'platform',
|
||||
requireResidentKey: false,
|
||||
userVerification: "required"
|
||||
},
|
||||
pubKeyCredParams: [
|
||||
// https://www.iana.org/assignments/cose/cose.xhtml#algorithms
|
||||
{type: "public-key", alg: -7}, // ECDSA w/ SHA-256
|
||||
{type: "public-key", alg: -35}, // ECDSA w/ SHA-384
|
||||
{type: "public-key", alg: -36}, // ECDSA w/ SHA-512
|
||||
{type: "public-key", alg: -257}, // RSASSA-PKCS1-v1_5 using SHA-256
|
||||
{type: "public-key", alg: -258}, // RSASSA-PKCS1-v1_5 using SHA-384
|
||||
{type: "public-key", alg: -259}, // RSASSA-PKCS1-v1_5 using SHA-512
|
||||
{type: "public-key", alg: -37}, // RSASSA-PSS w/ SHA-256
|
||||
{type: "public-key", alg: -38}, // RSASSA-PSS w/ SHA-384
|
||||
{type: "public-key", alg: -39}, // RSASSA-PSS w/ SHA-512
|
||||
{type: "public-key", alg: -8} // EdDSA
|
||||
],
|
||||
timeout: 1800000
|
||||
};
|
||||
|
||||
const PUBLIC_KEY_CREDENTIAL_REQUEST_OPTIONS_TEMPLATE = {
|
||||
allowCredentials: [{
|
||||
type: 'public-key',
|
||||
transports: ['internal']
|
||||
}],
|
||||
userVerification: "required",
|
||||
timeout: 1800000
|
||||
}
|
||||
|
||||
function isSupported() {
|
||||
return !!window.PublicKeyCredential && !!navigator.credentials;
|
||||
}
|
||||
|
||||
function isCompletelySupported() {
|
||||
if (!isSupported()) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
||||
}
|
||||
|
||||
function registerCredential({ username, nickname }, userSecret) {
|
||||
if (!window.location || !window.location.hostname || !document.title) {
|
||||
return Promise.reject({
|
||||
notSupported: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!isSupported() || !navigator.credentials.create) {
|
||||
return Promise.reject({
|
||||
notSupported: true
|
||||
});
|
||||
}
|
||||
|
||||
const challenge = utils.generateRandomString();
|
||||
const publicKeyCredentialCreationOptions = Object.assign({}, PUBLIC_KEY_CREDENTIAL_CREATION_OPTIONS_TEMPLATE, {
|
||||
challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
|
||||
rp: {
|
||||
name: document.title,
|
||||
id: window.location.hostname,
|
||||
},
|
||||
user: {
|
||||
id: Uint8Array.from(userSecret, c => c.charCodeAt(0)),
|
||||
name: username,
|
||||
displayName: nickname,
|
||||
}
|
||||
});
|
||||
|
||||
return navigator.credentials.create({
|
||||
publicKey: publicKeyCredentialCreationOptions
|
||||
}).then(rawCredential => {
|
||||
const clientData = rawCredential ? parseClientData(rawCredential) : null;
|
||||
const publicKey = rawCredential ? parsePublicKeyFromAttestationData(rawCredential) : null;
|
||||
|
||||
const challengeFromClientData = clientData && clientData.challenge ? atob(clientData.challenge) : null;
|
||||
|
||||
if (rawCredential && rawCredential.rawId &&
|
||||
clientData && clientData.type === 'webauthn.create' && challengeFromClientData === challenge) {
|
||||
|
||||
return {
|
||||
id: utils.base64encode(rawCredential.rawId),
|
||||
clientData: clientData,
|
||||
publicKey: publicKey,
|
||||
rawCredential: rawCredential
|
||||
};
|
||||
} else {
|
||||
return Promise.reject({
|
||||
invalid: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function parseClientData(credential) {
|
||||
const utf8Decoder = new TextDecoder('utf-8');
|
||||
const decodedClientData = utf8Decoder.decode(credential.response.clientDataJSON);
|
||||
return JSON.parse(decodedClientData);
|
||||
}
|
||||
|
||||
function parsePublicKeyFromAttestationData(credential) {
|
||||
const decodedAttestationData = CBOR.decode(credential.response.attestationObject);
|
||||
const authData = decodedAttestationData.authData;
|
||||
|
||||
const dataView = new DataView(new ArrayBuffer(2));
|
||||
const idLenBytes = authData.slice(53, 55);
|
||||
idLenBytes.forEach((value, index) => dataView.setUint8(index, value));
|
||||
|
||||
const credentialIdLength = dataView.getUint16();
|
||||
const publicKeyBytes = authData.slice(55 + credentialIdLength);
|
||||
|
||||
return publicKeyBytes;
|
||||
}
|
||||
|
||||
function verifyCredential(credentialId) {
|
||||
if (!window.location || !window.location.hostname) {
|
||||
return Promise.reject({
|
||||
notSupported: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!isSupported() || !navigator.credentials.get) {
|
||||
return Promise.reject({
|
||||
notSupported: true
|
||||
});
|
||||
}
|
||||
|
||||
const challenge = utils.generateRandomString();
|
||||
const publicKeyCredentialRequestOptions = Object.assign({}, PUBLIC_KEY_CREDENTIAL_REQUEST_OPTIONS_TEMPLATE, {
|
||||
challenge: Uint8Array.from(challenge, c => c.charCodeAt(0)),
|
||||
rpId: window.location.hostname
|
||||
});
|
||||
publicKeyCredentialRequestOptions.allowCredentials[0].id = Uint8Array.from(atob(credentialId), c=>c.charCodeAt(0)).buffer;
|
||||
|
||||
return navigator.credentials.get({
|
||||
publicKey: publicKeyCredentialRequestOptions
|
||||
}).then(rawCredential => {
|
||||
const clientData = rawCredential ? parseClientData(rawCredential) : null;
|
||||
const challengeFromClientData = clientData && clientData.challenge ? atob(clientData.challenge) : null;
|
||||
|
||||
if (rawCredential && rawCredential.rawId &&
|
||||
rawCredential.response && rawCredential.response.userHandle &&
|
||||
clientData && clientData.type === 'webauthn.get' && challengeFromClientData === challenge) {
|
||||
|
||||
return {
|
||||
id: utils.base64encode(rawCredential.rawId),
|
||||
userSecret: utils.arrayBufferToString(rawCredential.response.userHandle),
|
||||
clientData: clientData,
|
||||
rawCredential: rawCredential
|
||||
};
|
||||
} else {
|
||||
return Promise.reject({
|
||||
invalid: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
isSupported,
|
||||
isCompletelySupported,
|
||||
registerCredential,
|
||||
verifyCredential
|
||||
}
|
||||
Reference in New Issue
Block a user