Files
ezbookkeeping/src/lib/webauthn.js
T
2020-11-22 12:22:43 +08:00

186 lines
6.2 KiB
JavaScript

import CBOR from 'cbor-js';
import logger from './logger.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) {
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: utils.stringToArrayBuffer(challenge),
rp: {
name: window.location.hostname,
id: window.location.hostname
},
user: {
id: utils.stringToArrayBuffer(userSecret),
name: username,
displayName: nickname
}
});
logger.debug('webauthn create options', publicKeyCredentialCreationOptions);
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;
logger.debug('webauthn create raw response', rawCredential);
if (rawCredential && rawCredential.rawId &&
clientData && clientData.type === 'webauthn.create' && challengeFromClientData === challenge) {
const ret = {
id: utils.base64encode(rawCredential.rawId),
clientData: clientData,
publicKey: publicKey,
rawCredential: rawCredential
};
logger.debug('webauthn create response', ret);
return ret;
} 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: utils.stringToArrayBuffer(challenge),
rpId: window.location.hostname
});
publicKeyCredentialRequestOptions.allowCredentials[0].id = utils.stringToArrayBuffer(atob(credentialId));
logger.debug('webauthn get options', publicKeyCredentialRequestOptions);
return navigator.credentials.get({
publicKey: publicKeyCredentialRequestOptions
}).then(rawCredential => {
const clientData = rawCredential ? parseClientData(rawCredential) : null;
const challengeFromClientData = clientData && clientData.challenge ? atob(clientData.challenge) : null;
logger.debug('webauthn get raw response', rawCredential);
if (rawCredential && rawCredential.rawId &&
rawCredential.response && rawCredential.response.userHandle &&
clientData && clientData.type === 'webauthn.get' && challengeFromClientData === challenge) {
const ret = {
id: utils.base64encode(rawCredential.rawId),
userSecret: utils.arrayBufferToString(rawCredential.response.userHandle),
clientData: clientData,
rawCredential: rawCredential
};
logger.debug('webauthn get response', ret);
return ret;
} else {
return Promise.reject({
invalid: true
});
}
});
}
export default {
isSupported,
isCompletelySupported,
registerCredential,
verifyCredential
}