support use face id/touch id for application lock

This commit is contained in:
MaysWind
2020-11-22 00:01:50 +08:00
parent fcf9069c80
commit 7f7c58132e
12 changed files with 359 additions and 4 deletions
+3
View File
@@ -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
View File
@@ -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,
+26
View File
@@ -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,
+169
View File
@@ -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
}