mirror of
https://github.com/mayswind/ezbookkeeping.git
synced 2026-05-15 07:27:33 +08:00
migrate webauthn.js to typescript
This commit is contained in:
+1
-1
@@ -237,7 +237,7 @@ export function base64encode(arrayBuffer: ArrayBuffer): string | null {
|
||||
return btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(arrayBuffer))));
|
||||
}
|
||||
|
||||
export function base64decode(str: string): string | null {
|
||||
export function base64decode(str: string): string {
|
||||
if (!str) {
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import CBOR from 'cbor-js';
|
||||
import logger from './logger.ts';
|
||||
|
||||
import type { ApplicationLockState } from '@/core/setting.ts';
|
||||
import type { UserBasicInfo } from '@/models/user.ts';
|
||||
|
||||
import {
|
||||
isFunction,
|
||||
stringToArrayBuffer,
|
||||
@@ -10,8 +13,36 @@ import {
|
||||
import {
|
||||
generateRandomString
|
||||
} from './misc.ts';
|
||||
import logger from './logger.ts';
|
||||
|
||||
const publicKeyCredentialCreationOptionsBaseTemplate = {
|
||||
interface ClientData {
|
||||
challenge: string;
|
||||
crossOrigin: boolean;
|
||||
origin: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface AttestationData {
|
||||
authData: Uint8Array;
|
||||
fmt: string;
|
||||
}
|
||||
|
||||
interface WebAuthnRegisterResponse {
|
||||
id: string | null;
|
||||
clientData: ClientData;
|
||||
publicKey: Uint8Array | null;
|
||||
rawCredential: Credential;
|
||||
}
|
||||
|
||||
interface WebAuthnVerifyResponse {
|
||||
id: string | null;
|
||||
userName: string;
|
||||
userSecret: string;
|
||||
clientData: ClientData;
|
||||
rawCredential: Credential;
|
||||
}
|
||||
|
||||
const PUBLIC_KEY_CREDENTIAL_CREATION_OPTIONS_BASE_TEMPLATE = {
|
||||
attestation: "none",
|
||||
authenticatorSelection: {
|
||||
authenticatorAttachment: 'platform',
|
||||
@@ -26,7 +57,7 @@ const publicKeyCredentialCreationOptionsBaseTemplate = {
|
||||
timeout: 120000
|
||||
};
|
||||
|
||||
const publicKeyCredentialRequestOptionsBaseTemplate = {
|
||||
const PUBLIC_KEY_CREDENTIAL_REQUEST_OPTIONS_BASE_TEMPLATE = {
|
||||
allowCredentials: [{
|
||||
type: 'public-key'
|
||||
}],
|
||||
@@ -34,37 +65,57 @@ const publicKeyCredentialRequestOptionsBaseTemplate = {
|
||||
timeout: 120000
|
||||
};
|
||||
|
||||
function isSupported() {
|
||||
function parseClientData(credential: Credential): ClientData | null {
|
||||
const utf8Decoder = new TextDecoder('utf-8');
|
||||
const decodedClientData = utf8Decoder.decode(credential.response.clientDataJSON);
|
||||
return JSON.parse(decodedClientData) as ClientData;
|
||||
}
|
||||
|
||||
function parsePublicKeyFromAttestationData(credential: Credential): Uint8Array {
|
||||
const decodedAttestationData = CBOR.decode(credential.response.attestationObject) as AttestationData;
|
||||
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(0);
|
||||
const publicKeyBytes = authData.slice(55 + credentialIdLength);
|
||||
|
||||
return publicKeyBytes;
|
||||
}
|
||||
|
||||
export function isWebAuthnSupported(): boolean {
|
||||
return !!window.PublicKeyCredential
|
||||
&& !!navigator.credentials
|
||||
&& isFunction(window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable);
|
||||
}
|
||||
|
||||
function isCompletelySupported() {
|
||||
if (!isSupported()) {
|
||||
export function isWebAuthnCompletelySupported(): Promise<boolean> {
|
||||
if (!isWebAuthnSupported()) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
||||
}
|
||||
|
||||
function registerCredential({ username, secret }, { nickname }) {
|
||||
export function registerWebAuthnCredential(lockState: ApplicationLockState, userInfo: UserBasicInfo): Promise<WebAuthnRegisterResponse> {
|
||||
if (!window.location || !window.location.hostname) {
|
||||
return Promise.reject({
|
||||
notSupported: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!isSupported() || !navigator.credentials.create) {
|
||||
if (!isWebAuthnSupported() || !navigator.credentials.create) {
|
||||
return Promise.reject({
|
||||
notSupported: true
|
||||
});
|
||||
}
|
||||
|
||||
const challenge = generateRandomString();
|
||||
const userId = `${username}|${secret}`; // username 32bytes(max) + secret 24bytes = 56bytes(max)
|
||||
const userId = `${lockState.username}|${lockState.secret}`; // username 32bytes(max) + secret 24bytes = 56bytes(max)
|
||||
|
||||
const publicKeyCredentialCreationOptions = Object.assign({}, publicKeyCredentialCreationOptionsBaseTemplate, {
|
||||
const publicKeyCredentialCreationOptions: PublicKeyCredentialCreationOptions = Object.assign({}, PUBLIC_KEY_CREDENTIAL_CREATION_OPTIONS_BASE_TEMPLATE, {
|
||||
challenge: stringToArrayBuffer(challenge),
|
||||
rp: {
|
||||
name: window.location.hostname,
|
||||
@@ -72,10 +123,10 @@ function registerCredential({ username, secret }, { nickname }) {
|
||||
},
|
||||
user: {
|
||||
id: stringToArrayBuffer(userId),
|
||||
name: username,
|
||||
displayName: nickname
|
||||
name: lockState.username,
|
||||
displayName: userInfo.nickname
|
||||
}
|
||||
});
|
||||
}) as PublicKeyCredentialCreationOptions;
|
||||
|
||||
logger.debug('webauthn create options', publicKeyCredentialCreationOptions);
|
||||
|
||||
@@ -91,7 +142,7 @@ function registerCredential({ username, secret }, { nickname }) {
|
||||
|
||||
if (rawCredential && rawCredential.rawId &&
|
||||
clientData && clientData.type === 'webauthn.create' && challengeFromClientData === challenge) {
|
||||
const ret = {
|
||||
const ret: WebAuthnRegisterResponse = {
|
||||
id: base64encode(rawCredential.rawId),
|
||||
clientData: clientData,
|
||||
publicKey: publicKey,
|
||||
@@ -109,45 +160,28 @@ function registerCredential({ username, secret }, { nickname }) {
|
||||
});
|
||||
}
|
||||
|
||||
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({ username }, credentialId) {
|
||||
export function verifyWebAuthnCredential(userInfo: UserBasicInfo, credentialId: string): Promise<WebAuthnVerifyResponse> {
|
||||
if (!window.location || !window.location.hostname) {
|
||||
return Promise.reject({
|
||||
notSupported: true
|
||||
});
|
||||
}
|
||||
|
||||
if (!isSupported() || !navigator.credentials.get) {
|
||||
if (!isWebAuthnSupported() || !navigator.credentials.get) {
|
||||
return Promise.reject({
|
||||
notSupported: true
|
||||
});
|
||||
}
|
||||
|
||||
const challenge = generateRandomString();
|
||||
const publicKeyCredentialRequestOptions = Object.assign({}, publicKeyCredentialRequestOptionsBaseTemplate, {
|
||||
const publicKeyCredentialRequestOptions: PublicKeyCredentialRequestOptions = Object.assign({}, PUBLIC_KEY_CREDENTIAL_REQUEST_OPTIONS_BASE_TEMPLATE, {
|
||||
challenge: stringToArrayBuffer(challenge),
|
||||
rpId: window.location.hostname
|
||||
});
|
||||
publicKeyCredentialRequestOptions.allowCredentials[0].id = stringToArrayBuffer(base64decode(credentialId));
|
||||
}) as PublicKeyCredentialRequestOptions;
|
||||
|
||||
if (publicKeyCredentialRequestOptions.allowCredentials && publicKeyCredentialRequestOptions.allowCredentials.length > 0) {
|
||||
publicKeyCredentialRequestOptions.allowCredentials[0].id = stringToArrayBuffer(base64decode(credentialId));
|
||||
}
|
||||
|
||||
logger.debug('webauthn get options', publicKeyCredentialRequestOptions);
|
||||
|
||||
@@ -162,8 +196,8 @@ function verifyCredential({ username }, credentialId) {
|
||||
|
||||
if (rawCredential && rawCredential.rawId &&
|
||||
clientData && clientData.type === 'webauthn.get' && challengeFromClientData === challenge &&
|
||||
userIdParts && userIdParts.length === 2 && userIdParts[0] === username) {
|
||||
const ret = {
|
||||
userIdParts && userIdParts.length === 2 && userIdParts[0] === userInfo.username) {
|
||||
const ret: WebAuthnVerifyResponse = {
|
||||
id: base64encode(rawCredential.rawId),
|
||||
userName: userIdParts[0],
|
||||
userSecret: userIdParts[1],
|
||||
@@ -181,10 +215,3 @@ function verifyCredential({ username }, credentialId) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
isSupported,
|
||||
isCompletelySupported,
|
||||
registerCredential,
|
||||
verifyCredential
|
||||
}
|
||||
Reference in New Issue
Block a user