/* * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). * * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics) * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published * by the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ 'use strict' class WebauthnTFA { _b64UrlSafeEncode = (str) => { const b64 = btoa(str); return b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); } // Decodes a Base64Url string _b64UrlSafeDecode = (input) => { input = input .replace(/-/g, '+') .replace(/_/g, '/'); const pad = input.length % 4; if (pad) { if (pad === 1) { throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding'); } input += new Array(5-pad).join('='); } return window.atob(input); }; // Converts an array of bytes into a Base64Url string _arrayToBase64String = (a) => { const str = String.fromCharCode(...a); return this._b64UrlSafeEncode(str); } // Prepares the public key options object returned by the Webauthn Framework _preparePublicKeyOptions = publicKey => { //Convert challenge from Base64Url string to Uint8Array publicKey.challenge = Uint8Array.from( this._b64UrlSafeDecode(publicKey.challenge), c => c.charCodeAt(0) ); //Convert the user ID from Base64 string to Uint8Array if (publicKey.user !== undefined) { publicKey.user = { ...publicKey.user, id: Uint8Array.from( window.atob(publicKey.user.id), c => c.charCodeAt(0) ), }; } //If excludeCredentials is defined, we convert all IDs to Uint8Array if (publicKey.excludeCredentials !== undefined) { publicKey.excludeCredentials = publicKey.excludeCredentials.map( data => { return { ...data, id: Uint8Array.from( this._b64UrlSafeDecode(data.id), c => c.charCodeAt(0) ), }; } ); } if (publicKey.allowCredentials !== undefined) { publicKey.allowCredentials = publicKey.allowCredentials.map( data => { return { ...data, id: Uint8Array.from( this._b64UrlSafeDecode(data.id), c => c.charCodeAt(0) ), }; } ); } return publicKey; }; // Prepares the public key credentials object returned by the authenticator _preparePublicKeyCredentials = data => { const publicKeyCredential = { id: data.id, type: data.type, rawId: this._arrayToBase64String(new Uint8Array(data.rawId)), response: { clientDataJSON: this._arrayToBase64String( new Uint8Array(data.response.clientDataJSON) ), }, }; if (data.response.attestationObject !== undefined) { publicKeyCredential.response.attestationObject = this._arrayToBase64String( new Uint8Array(data.response.attestationObject) ); } if (data.response.authenticatorData !== undefined) { publicKeyCredential.response.authenticatorData = this._arrayToBase64String( new Uint8Array(data.response.authenticatorData) ); } if (data.response.signature !== undefined) { publicKeyCredential.response.signature = this._arrayToBase64String( new Uint8Array(data.response.signature) ); } if (data.response.userHandle !== undefined) { publicKeyCredential.response.userHandle = this._arrayToBase64String( new Uint8Array(data.response.userHandle) ); } return publicKeyCredential; }; constructor() { const register_dom_ready = (fn) => { document.addEventListener('turbo:load', fn) } register_dom_ready(() => { this.registerForms(); }); } registerForms() { //Find all forms which have an data-webauthn-tfa-action attribute const forms = document.querySelectorAll('form[data-webauthn-tfa-action]'); forms.forEach((form) => { console.debug('Found webauthn TFA form with action: ' + form.getAttribute('data-webauthn-tfa-action'), form); //Ensure that the form has webauthn data const dataString = form.getAttribute('data-webauthn-tfa-data') const action = form.getAttribute('data-webauthn-tfa-action'); if (!dataString) { console.error('Form does not have webauthn data, can not continue!', form); return; } //Convert dataString to the needed dataObject const dataObject = JSON.parse(dataString); const options = this._preparePublicKeyOptions(dataObject); if(action === 'authenticate'){ this.authenticate(form, {publicKey: options}); } if(action === 'register'){ //Register submit action, so we can do the registration on submit form.addEventListener('submit', (e) => { e.preventDefault(); this.register(form, {publicKey: options}); }); } }); } /** * Submit the form with the given result data * @param form * @param data * @private */ _submit(form, data) { const resultField = document.getElementById('_auth_code'); resultField.value = JSON.stringify(data) form.submit(); } authenticate(form, authData) { navigator.credentials.get(authData) .then((credential) => { //Convert our credential to a form which can be JSON encoded let data = this._preparePublicKeyCredentials(credential); this._submit(form, data) }) .catch((error) => { console.error("WebAuthn Authentication error: ", error); alert("Error: " + error) }); } register(form, authData) { navigator.credentials.create(authData) .then((credential) => { //Convert our credential to a form which can be JSON encoded let data = this._preparePublicKeyCredentials(credential); this._submit(form, data) }) .catch((error) => { console.error("WebAuthn Registration error: ", error); alert("Error: " + error) }); } } window.webauthnTFA = new WebauthnTFA();