/* * Copyright 2017 Google Inc. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ const $ = q => { return document.querySelector(q); }; const show = q => { $(q).style.display = 'block'; }; const hide = q => { $(q).style.display = 'none'; }; const isChecked = q => { return $(q).checked; }; const onClick = (q, func) => { $(q).addEventListener('click', func); }; function addErrorMsg(msg) { $('#error-text').innerHTML = msg; show('#error'); }; function addSuccessMsg(msg) { $('#success-text').innerHTML = msg; show('#success'); }; function removeMsgs() { hide('#error'); hide('#success'); }; function _fetch(url, obj) { let headers = new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' }); let body; if (typeof URLSearchParams === "function") { body = new URLSearchParams(); for (let key in obj) { body.append(key, obj[key]); } // Set body to string value to handle an Edge case body = body.toString(); } else { // Add parameters to body manually if browser doesn't support // URLSearchParams body = ""; for (let key in obj) { body += encodeURIComponent(key) + "=" + encodeURIComponent(obj[key]) + "&"; } } return fetch(url, { method: 'POST', headers: headers, credentials: 'include', body: body }).then(response => { if (response.status === 200) { return response.json(); } else { throw response.statusText; } }); }; function fetchCredentials() { _fetch('/RegisteredKeys').then(response => { let credentials = ''; for (let i in response) { let { handle, publicKey, name, date, id } = response[i]; let hasCable = response[i].hasCable ? "Yes" : "No"; let buttonId = `delete${i}`; credentials += `
${name}
Enrolled ${date}
Public Key
${publicKey}
Key Handle
${handle}
Supports caBLE
${hasCable}
`; } $('#credentials').innerHTML = credentials; for (let i in response) { let { handle, publicKey, name, date, id } = response[i]; onClick(`#delete${i}`, removeCredential(id)); } }); } function removeCredential(id) { return () => { _fetch('/RemoveCredential', { credentialId : id }).then(() => { fetchCredentials(); }).catch(err => { addErrorMsg(`An error occurred during removal [${err.toString()}]`); }); } } function credentialListConversion(list) { return list.map(item => { const cred = { type: item.type, id: strToBin(item.id) }; if (item.transports) { cred.transports = list.transports; } return cred; }); } function hexToBuffer(hex) { let array = new Uint8Array(hex.toUpperCase().match(/[\dA-F]{2}/gi).map(function (b) { return parseInt(b, 16) })); return array.buffer; } function addCredential() { removeMsgs(); show('#active'); let _options; const advancedOptions = {}; if (isChecked('#switch-advanced')) { if (isChecked('#switch-rk')) { advancedOptions.requireResidentKey = isChecked('#switch-rk'); } if (isChecked('#switch-rr')) { advancedOptions.excludeCredentials = isChecked('#switch-rr'); } if ($('#userVerification').value != "none") { advancedOptions.userVerification = $('#userVerification').value; } if ($('#attachment').value != "none") { advancedOptions.authenticatorAttachment = $('#attachment').value; } if ($('#conveyance').value != "NA") { advancedOptions.attestationConveyancePreference = $('#conveyance').value; } } return _fetch('/BeginMakeCredential', { advanced: isChecked('#switch-advanced'), advancedOptions: JSON.stringify(advancedOptions) }).then(options => { const makeCredentialOptions = {}; _options = options; makeCredentialOptions.rp = options.rp; makeCredentialOptions.user = options.user; makeCredentialOptions.user.id = strToBin(options.user.id); makeCredentialOptions.challenge = strToBin(options.challenge); makeCredentialOptions.pubKeyCredParams = options.pubKeyCredParams; // Optional parameters if ('timeout' in options) { makeCredentialOptions.timeout = options.timeout; } if ('excludeCredentials' in options) { makeCredentialOptions.excludeCredentials = credentialListConversion(options.excludeCredentials); } if ('authenticatorSelection' in options) { makeCredentialOptions.authenticatorSelection = options.authenticatorSelection; } if ('attestation' in options) { makeCredentialOptions.attestation = options.attestation; } if ('extensions' in options) { makeCredentialOptions.extensions = options.extensions; } console.log(makeCredentialOptions); return navigator.credentials.create({ "publicKey": makeCredentialOptions }); }).then(attestation => { hide('#active'); const publicKeyCredential = {}; if ('id' in attestation) { publicKeyCredential.id = attestation.id; } if ('type' in attestation) { publicKeyCredential.type = attestation.type; } if ('rawId' in attestation) { publicKeyCredential.rawId = binToStr(attestation.rawId); } if (!attestation.response) { addErrorMsg("Make Credential response lacking 'response' attribute"); } const response = {}; response.clientDataJSON = binToStr(attestation.response.clientDataJSON); response.attestationObject = binToStr(attestation.response.attestationObject); publicKeyCredential.response = response; return _fetch('/FinishMakeCredential', { data: JSON.stringify(publicKeyCredential), session: _options.session.id }); }).then(parameters => { console.log(parameters); if (parameters && parameters.success) { addSuccessMsg(parameters.message); fetchCredentials(); } else { throw 'Unexpected response received.'; } }).catch(err => { hide('#active'); console.log(err.toString()); addErrorMsg(`An error occurred during Make Credential operation [${err.toString()}]`); }); } function getAssertion() { removeMsgs(); show('#active'); let _parameters; _fetch('/BeginGetAssertion').then(parameters => { const requestOptions = {}; _parameters = parameters; requestOptions.challenge = strToBin(parameters.challenge); if ('timeout' in parameters) { requestOptions.timeout = parameters.timeout; } if ('rpId' in parameters) { requestOptions.rpId = parameters.rpId; } if ('allowCredentials' in parameters) { requestOptions.allowCredentials = credentialListConversion(parameters.allowCredentials); } if ('extensions' in parameters) { requestOptions.extensions = parameters.extensions; // Convert ByteArrays if ('cableAuthentication' in requestOptions.extensions) { var cableSessionDatas = requestOptions.extensions.cableAuthentication; cableSessionDatas.forEach(sd => { sd.clientEid = hexToBuffer(sd.clientEid); sd.authenticatorEid = hexToBuffer(sd.authenticatorEid); sd.sessionPreKey = hexToBuffer(sd.sessionPreKey); }); } } // Add options if selected if (isChecked('#switch-advanced-auth')) { let extensions = {}; if ($('#cable-client-eid').value !== '' || $('#cable-authenticator-eid').value !== '' || $('#cable-session-pre-key').value !== '') { let cable = {}; cable.version = 1; cable.clientEid = hexToBuffer($('#cable-client-eid').value); cable.authenticatorEid = hexToBuffer($('#cable-authenticator-eid').value); cable.sessionPreKey = hexToBuffer($('#cable-session-pre-key').value); extensions.cableAuthentication = [cable]; } if (Object.keys(extensions).length !== 0) { requestOptions.extensions = extensions; } } console.log(requestOptions); return navigator.credentials.get({ "publicKey": requestOptions }); }).then(assertion => { hide('#active'); const publicKeyCredential = {}; if ('id' in assertion) { publicKeyCredential.id = assertion.id; } if ('type' in assertion) { publicKeyCredential.type = assertion.type; } if ('rawId' in assertion) { publicKeyCredential.rawId = binToStr(assertion.rawId); } if (!assertion.response) { throw "Get assertion response lacking 'response' attribute"; } const _response = assertion.response; publicKeyCredential.response = { clientDataJSON: binToStr(_response.clientDataJSON), authenticatorData: binToStr(_response.authenticatorData), signature: binToStr(_response.signature), userHandle: binToStr(_response.userHandle) }; return _fetch('/FinishGetAssertion', { data: JSON.stringify(publicKeyCredential), session: _parameters.session.id }); }).then(result => { console.log(result); if (result && result.success) { addSuccessMsg(result.message); if ('handle' in result) { let card = document.getElementById(result.handle); card.animate([{ backgroundColor: '#009688' },{ backgroundColor: 'white' }], { duration: 2000, easing: 'ease-out' }); } } }).catch(err => { hide('#active'); console.log(err.toString()); addErrorMsg(`An error occurred during Assertion request [${err.toString()}]`); }); } function strToBin(str) { return Uint8Array.from(atob(str), c => c.charCodeAt(0)); } function binToStr(bin) { return btoa(new Uint8Array(bin).reduce( (s, byte) => s + String.fromCharCode(byte), '' )); } document.addEventListener('DOMContentLoaded', () => { let hiddens = Array.from(document.querySelectorAll('.hidden')); for (let hidden of hiddens) { hidden.style.display = 'none'; hidden.classList.remove('hidden'); } if (navigator.credentials && navigator.credentials.create) { fetchCredentials(); } else { addErrorMsg('Your browser doesn\'t support WebAuthn'); fetchCredentials(); } }); window.addEventListener('load', () => { if (isChecked('#switch-advanced')) { show('#advanced'); } else { hide('#advanced'); } if (isChecked('#switch-advanced-auth')) { show('#advanced-auth'); } else { hide('#advanced-auth'); } onClick('#credential-button', addCredential); onClick('#authenticate-button', getAssertion); onClick('#switch-advanced', () => { if (isChecked('#switch-advanced')) { show('#advanced'); } else { hide('#advanced'); } }); onClick('#switch-advanced-auth', () => { if (isChecked('#switch-advanced-auth')) { show('#advanced-auth'); } else { hide('#advanced-auth'); } }); });