diff --git a/extension/authenticator.js b/extension/authenticator.js index 4b06c78..fb16723 100644 --- a/extension/authenticator.js +++ b/extension/authenticator.js @@ -13,17 +13,15 @@ window.authnTools = window.authnTools || {}; const initAuthenticator = async () => { const authenticator = new window.AuthnDevice(); // Override storage handler to use chrome.storage.local - authenticator.handleStorage = function (data) { - if (data) { - // Save mode - return a promise or handle async - return new Promise((resolve, reject) => { - chrome.storage.local.set({ 'system_credentials': data }, () => { - console.log('Credentials saved to local storage'); - resolve(data); - }); - }); + authenticator.handleStorage = async function (data = null) { + if (data !== null) { + await chrome.storage.local.set({ 'system_credentials': data } + ); + console.log('Credentials saved to local storage'); + return data; } else { - return this.storage; + const result = await chrome.storage.local.get('system_credentials'); + return result.system_credentials || []; } }; const result = await chrome.storage.local.get(['system_credentials', 'option@debugLogging', 'extension_salt', 'extension_ca_key']); @@ -61,7 +59,7 @@ const initAuthenticator = async () => { authenticator.legacyMasterkeySalt = legacySalt.buffer; console.log('[Auth] Legacy salt fallback initialized (16 zeros).'); - if (result.system_credentials) authenticator.storage = result.system_credentials; + authenticator.storage = result.system_credentials || []; authenticator.debugLogging = result['option@debugLogging'] === true; return authenticator; }; @@ -142,9 +140,23 @@ const handleMessage = async (request, sender, sendResponse) => { // So we need to manually ensure we save "authenticator.storage" if it changed? // UNLESS we explicit save here. - debugLog('[Auth] Manually ensuring storage save...'); - await new Promise(r => chrome.storage.local.set({ 'system_credentials': deviceInstance.storage }, r)); - debugLog('[Auth] Manual save complete.'); + //debugLog('[Auth] Manually ensuring storage save...'); + //await new Promise(r => chrome.storage.local.set({ 'system_credentials': deviceInstance.storage }, r)); + //debugLog('[Auth] Manual save complete.'); + + + if (deviceInstance.storage) { + debugLog('[Auth] Saving credential storage...'); + await new Promise(r => + chrome.storage.local.set( + { 'system_credentials': deviceInstance.storage }, + r + ) + ); + debugLog('[Auth] Storage save complete.'); + } + + } else if (request.type === 'get' || request.authn === 'get') { debugLog('[Auth] Get Options (Raw):', request.options); diff --git a/extension/background.js b/extension/background.js index 7958042..efc159b 100644 --- a/extension/background.js +++ b/extension/background.js @@ -11,62 +11,147 @@ let contentScriptPort = null; // Listen for messages from content scripts (injected page) chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + //console.log("BACKGROUND MESSAGE:", message); + // + // Popup is ready - send pend8ing WebAuthn request + // if (message.type === 'authenticator_ready') { - // The popup is open and ready. Send the pending request. + + console.log("AUTHENTICATOR READY"); + if (pendingRequest) { chrome.runtime.sendMessage(pendingRequest); - // pendingRequest = null; // Keep it until completion? No, simple flow. } + return; } - // Message from the authenticator popup (completion) - if (message.status === 'completed' || message.status === 'error') { - // Forward to the tab that requested it + // + // Response from authenticator popup + // + if (message.status === "completed" || message.status === "error") { + + // console.log( + // "FULL COMPLETION MESSAGE:", + // message + // ); + + let credential = null; + + if (message.credential) { + try { + credential = JSON.parse(message.credential); + console.log("PARSED CREDENTIAL:", credential); + } catch (e) { + console.error("FAILED TO PARSE CREDENTIAL:", e); + } + } + + // + // Debug credential payload + // + if (message.credential) { + + try { + let credential = JSON.parse(message.credential); - // If we have a stored tab ID from the pending request, use it. + console.log( + "PARSED CREDENTIAL:", + credential + ); + + // rawId + if (credential.rawId?.data) { + let rawId = new Uint8Array(credential.rawId.data); + console.log("FORWARDED RAW ID:",rawId.byteLength,rawId); + } + + // authenticatorData + if ( + credential.response?.authenticatorData?.data + ) { + + let authData = new Uint8Array(credential.response.authenticatorData.data); + console.log("FORWARDED AUTHDATA:",authData.byteLength); + } + + // signature + if (credential.response?.signature?.data) { + let signature = + new Uint8Array(credential.response.signature.data + ); + + console.log("FORWARDED SIGNATURE:",signature.byteLength); + + console.log( + "SIGNATURE HEX:", + Array.from(signature) + .map( + b => + b.toString(16) + .padStart(2,"0") + ) + .join("") + ); + } + + // userHandle + if ( + credential.response?.userHandle?.data + ) { + + let userHandle = + new Uint8Array( + credential.response.userHandle.data + ); + + console.log("FORWARDED USER HANDLE:",userHandle.byteLength); + } + + + } + catch(e) {console.error("FAILED TO PARSE CREDENTIAL:",e); + } + } + + // Send response back to requesting tab if (pendingRequest && pendingRequest.requestingTabId) { console.log("WebAuthnLinux: Forwarding response to tab " + pendingRequest.requestingTabId); chrome.tabs.sendMessage(pendingRequest.requestingTabId, { - ...message, - extensionResponse: true - }); - // Clear pending request after completion? - // Maybe wait a bit or clear it now. Let's clear it to be clean. - // pendingRequest = null; + ...message, + extensionResponse: true + }); + // Clear request after completion + // + pendingRequest = null; } else { - // Fallback: Query active tab (less reliable if popup is focused or user switched tabs) - chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { - if (tabs.length > 0) { - chrome.tabs.sendMessage(tabs[0].id, { - ...message, - extensionResponse: true // Flag to identify it - }); - } - }); + console.warn("No requesting tab stored"); } return; } // Message from the content script (requesting auth) if (message.action === 'trigger_authenticator') { - pendingRequest = message.data; + //console.log("AUTH REQUEST RECEIVED",message); + pendingRequest = message.data; // Store the ID of the tab that requested this, so we can reply to the correct one later - // message.data might not have it, but 'sender' does. + // message.data might not have it, but 'sender' does. if (sender.tab) { pendingRequest.requestingTabId = sender.tab.id; } - // Open the popup + // Open authenticator popup chrome.windows.create({ - url: "authenticator.html", - type: "popup", - width: 400, - height: 600, - focused: true - }, (window) => { - popupWindowId = window.id; - }); + url: "authenticator.html", + type: "popup", + width: 400, + height: 600, + focused: true + }, (window) => { + if(window) + popupWindowId = window.id; + } + ); sendResponse({ started: true }); return true; // async response diff --git a/extension/js/webauthn-authenticator.js b/extension/js/webauthn-authenticator.js index cc1b09a..f374714 100644 --- a/extension/js/webauthn-authenticator.js +++ b/extension/js/webauthn-authenticator.js @@ -29,9 +29,13 @@ window.AuthnDevice = (function (localURL) { this.askmasterkey = false; // Default AAGUID - let guid = 'linux-authn-v1.0'; // 16 chars - guid = window.authnTools.stringToBase64url(guid); - this.aaguid = window.authnTools.base64urlToUint8Array(guid); + // 16-byte opaque authenticator identifier + this.aaguid = new Uint8Array([ + 0x8d, 0x4f, 0x3a, 0x21, + 0x72, 0x91, 0x4c, 0x55, + 0xa8, 0x93, 0x7e, 0x1b, + 0x62, 0x44, 0xf0, 0x9c + ]); // Initialize placeholders this.masterkey = null; @@ -356,45 +360,115 @@ window.AuthnDevice = (function (localURL) { AuthnDevice.prototype._saveResidentKey = async function (host) { // Get wrapped key and generate hash as id let wrappedKey = this.credential_id; - // Resident keys use a has of the credential id as a credential id + // Resident keys use a hash of the credential id as a credential id this.credential_id = await window.crypto.subtle.digest('SHA-256', this.credential_id); // Save key info - if (this.handleStorage) this.storage = this.handleStorage(); + // Load existing storage + if (this.handleStorage) this.storage = await this.handleStorage(); this.storage.push({ host: host, keyid: window.authnTools.uint8ArrayToBase64url(new Uint8Array(this.credential_id)), masterKeySalt: window.authnTools.uint8ArrayToBase64url(this.masterkeysalt), - wrappedKey: window.authnTools.uint8ArrayToBase64url(new Uint8Array(wrappedKey)) + wrappedKey: window.authnTools.uint8ArrayToBase64url(new Uint8Array(wrappedKey)), + + userHandle: this.user_handle + ? window.authnTools.uint8ArrayToBase64url( + new Uint8Array(this.user_handle) + ) + : null, + + // Credential management metadata + created: Date.now(), + lastUsed: null, + signCount: 0 + }); - if (this.handleStorage) this.handleStorage(this.storage); + // Persist + if (this.handleStorage) await this.handleStorage(this.storage); }; AuthnDevice.prototype._cred_init_with_ResidentKey = async function (rp_id, user_handle = null, key_id = null) { - if (this.handleStorage) this.storage = this.handleStorage(); - // Search storage + if (this.handleStorage) this.storage = await this.handleStorage(); + + // Normalise key_id for storage comparison + if (key_id instanceof ArrayBuffer) { + key_id = window.authnTools.uint8ArrayToBase64url(new Uint8Array(key_id)); + } + + console.log("RESIDENT LOOKUP:", { + rp_id, + key_id, + storageCount: this.storage.length + }); + + // Search storage //console.log('Storage', this.storage); for (let i = this.storage.length - 1; i >= 0; i--) { - //console.log('Checking', this.storage[i].host, rp_id, this.storage[i].keyid, key_id); + console.log("Checking:", { + index: i, + host: this.storage[i].host, + keyid: this.storage[i].keyid, + matchHost: this.storage[i].host === rp_id, + matchKey: key_id === null || this.storage[i].keyid === key_id + }); // If key found in storage if (this.storage[i].host === rp_id && (key_id === null || this.storage[i].keyid === key_id)) { + + console.log("RESIDENT KEY MATCH FOUND:", this.storage[i]); + let credentials = this.storage[i]; - //console.log('Webpage match', credentials); + + this.currentCredential = credentials; + + // Update last used time + credentials.lastUsed = Date.now(); + + // Ensure signCount exists for old credentials + if (!("signCount" in credentials)) { + credentials.signCount = 0; + } + + // Save updated metadata + if (this.handleStorage) { + await this.handleStorage(this.storage); + } + let wrappedKey = window.authnTools.base64urlToUint8Array(credentials.wrappedKey); - // Restore key salt this.masterkeysalt = window.authnTools.base64urlToUint8Array(credentials.masterKeySalt); - // Init credentials - let r = await this._cred_init(rp_id, user_handle, wrappedKey); - // Resident keys use a different credential id than the original credential id - if (r) { - if (key_id === null) { - //key_id = window.authnTools.base64urlToUint8Array(credentials.keyid); - key_id = new Uint8Array(await window.crypto.subtle.digest('SHA-256', wrappedKey)); - } - this.credential_id = key_id; + + let r = await this._cred_init(rp_id,user_handle,wrappedKey); + + if (r) { + if (key_id === null) { + key_id = new Uint8Array( + await window.crypto.subtle.digest('SHA-256', wrappedKey) + ); } - return r; + console.log("KEY ID BEFORE ASSIGN:", { + type: Object.prototype.toString.call(key_id), + length: key_id?.byteLength || key_id?.length, + value: key_id + }); + + + //this.credential_id = key_id; + if (typeof key_id === "string") { + this.credential_id = window.authnTools.base64urlToUint8Array(key_id); + } else { + this.credential_id = new Uint8Array(key_id); + } + + console.log("RESIDENT KEY INITIALISED:", { + credential_id: + window.authnTools.uint8ArrayToBase64url( + new Uint8Array(this.credential_id) + ) + }); + } + return r; } } + console.log("NO RESIDENT KEY FOUND"); return false; }; @@ -413,30 +487,34 @@ window.AuthnDevice = (function (localURL) { }; AuthnDevice.prototype._flags = function (flags) { - // Bit 0: User Present (UP) result. - // Bit 1: Reserved for future use (RFU1). - // Bit 2: User Verified (UV) result. - // Bit 3: Reserved for future use (RFU2). - // Bit 4: Reserved for future use (RFU2). - // Bit 5: Reserved for future use (RFU2). - // Bit 6: Attested credential data included (AT). - // Bit 7: Extension data included (ED). + /* + * WebAuthn authenticator data flags: + * + * Bit 0: User Present (UP) + * Bit 1: Reserved for future use (RFU1) + * Bit 2: User Verified (UV) + * Bit 3: Backup Eligible (BE) + * Bit 4: Backup State (BS) + * Bit 5: Reserved for future use (RFU2) + * Bit 6: Attested credential data included (AT) + * Bit 7: Extension data included (ED) + */ // Testing: Freeze User Verification if (Number.isInteger(this.testing.freezeUserVerificationFlag)) { - flags['uv'] = this.testing.freezeUserVerificationFlag > 0 ? true : false; + flags['uv'] = this.testing.freezeUserVerificationFlag > 0; } - let bits = [0, 0, 0, 0, 0, 0, 0, 0]; - if (flags.hasOwnProperty('up') && flags['up']) bits[0] = 1; - if (flags.hasOwnProperty('uv') && flags['uv']) bits[2] = 1; - if (flags.hasOwnProperty('at') && flags['at']) bits[6] = 1; - if (flags.hasOwnProperty('ed') && flags['ed']) bits[7] = 1; + let value = 0; - // Convert to string and reverse order - bits = bits.map(x => x + '').reduce((x, y) => y + x, ''); + if (flags.up) value |= (1 << 0); + if (flags.uv) value |= (1 << 2); + if (flags.be) value |= (1 << 3); + if (flags.bs) value |= (1 << 4); + if (flags.at) value |= (1 << 6); + if (flags.ed) value |= (1 << 7); - return parseInt(bits, 2); + return value; }; AuthnDevice.prototype.create = async function (options, _origin) { @@ -460,9 +538,30 @@ window.AuthnDevice = (function (localURL) { if (algs.length < 1) throw new Error('Requested pubKeyCredParams does not contain supported type'); let keyPairAlg = algs[0].alg; - // Check if required Resident Key aka Discoverable credential - let requestResidentKey = (options.publicKey.authenticatorSelection && options.publicKey.authenticatorSelection.requireResidentKey === true); - if (this.testing.forceResidentKey) requestResidentKey = true; + console.log( + JSON.stringify( + options.publicKey.authenticatorSelection, + null, + 2 + ) + ); + + // Check if required/preferred Resident Key aka Discoverable credential + let requestResidentKey = false; + + if (options.publicKey.authenticatorSelection) { + + let selection = options.publicKey.authenticatorSelection; + + requestResidentKey = + selection.requireResidentKey === true || + selection.residentKey === "required" || + selection.residentKey === "preferred"; + } + + if (this.testing.forceResidentKey) { + requestResidentKey = true; + } // Calcualte RP ID and origin's effective domain let origin = (() => { @@ -494,8 +593,25 @@ window.AuthnDevice = (function (localURL) { } let userId = options.publicKey.user.id; + + // Always work with a Uint8Array + if (userId instanceof ArrayBuffer) userId = new Uint8Array(userId); + + console.log("REGISTER USER ID TYPE:", userId.constructor.name); + console.log("REGISTER USER ID LENGTH:", userId.byteLength); + console.log( + "REGISTER USER ID HEX:", + Array.from(userId) + .map(b => b.toString(16).padStart(2, "0")) + .join("") + ); + + // Testing override if (this.testing.userId) { if (this.testing.userId instanceof ArrayBuffer) { + userId = new Uint8Array(this.testing.userId); + } + else if (this.testing.userId instanceof Uint8Array) { userId = this.testing.userId; } else if (typeof this.testing.userId === 'string') { @@ -503,10 +619,43 @@ window.AuthnDevice = (function (localURL) { } } + console.log("FINAL USER ID TYPE:", userId.constructor.name); + console.log("FINAL USER ID LENGTH:", userId.byteLength); + console.log( + "FINAL USER ID HEX:", + Array.from(userId) + .map(b => b.toString(16).padStart(2, "0")) + .join("") + ); + // Prepare new key await this._cred_init(rpid, userId, null, keyPairAlg); - // Save resident key - if (requestResidentKey) await this._saveResidentKey(rpid); + + console.log("CREATE KEY STATE:", { + credential_id: window.authnTools.uint8ArrayToBase64url(new Uint8Array(this.credential_id)), + rp_id: this.rp_id, + private_key: this.private_key, + public_key: this.public_key, + cose_key: this.cose_key + }); + + // Determine whether to save as a discoverable (resident) credential + const resident = options.publicKey.authenticatorSelection?.residentKey; + + const shouldSave = + requestResidentKey || + resident === "preferred" || + resident === "required"; + + console.log("CREATE:", { + rpid: rpid, + requireResidentKey: requestResidentKey, + residentKey: resident, + shouldSave: shouldSave, + selection: options.publicKey.authenticatorSelection + }); + + if (shouldSave) await this._saveResidentKey(rpid); // Get credential id let credential_id = new Uint8Array(this.credential_id); @@ -526,21 +675,35 @@ window.AuthnDevice = (function (localURL) { 'type': 'webauthn.create', 'challenge': window.authnTools.uint8ArrayToBase64url(challenge), 'origin': origin, - 'crossOrigin': false, - 'authenticator': 'WebAuthnLinux Fingerprint Bridge' + 'crossOrigin': false } //console.log(client_data); client_data = JSON.stringify(client_data); client_data = new TextEncoder().encode(client_data); + console.log("CREATED CREDENTIAL ID:", + window.authnTools.uint8ArrayToBase64url(this.credential_id) + ); + // hash the message let rp_id_hash = new Uint8Array(await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(this.rp_id))); - let flags = new Uint8Array([this._flags({ uv: true, up: true, at: true })]); + let flags = new Uint8Array([this._flags({ up: true, uv: true, at: true })]); let sign_count = window.authnTools.intToUint8Array(this._getSignCount(), 4, true); let credential_id_length = window.authnTools.intToUint8Array(credential_id.length, 2, true); let cose_key = new Uint8Array(window.CBOR.encode(this.cose_key)); let authData = this._concatUint8Arrays(rp_id_hash, flags, sign_count, this._getAAGUID(), credential_id_length, credential_id, cose_key); + console.log("CREATE AUTH DATA LENGTH:", authData.length); + console.log("CREATE AAGUID:", this._getAAGUID()); + console.log("CREATE FLAGS:", flags); + console.log("CREATE COSE KEY:", cose_key); + console.log( + "CREATE COSE KEY HEX:", + Array.from(cose_key) + .map(b => (b & 0xff).toString(16).padStart(2,'0')) + .join('') + ); + // Attestation object let attestation_object = { 'fmt': 'none', @@ -553,7 +716,7 @@ window.AuthnDevice = (function (localURL) { // Generate signature let client_data_hash = new Uint8Array(await crypto.subtle.digest('SHA-256', client_data)); let signatureData = this._concatUint8Arrays(authData, client_data_hash); - let signature = await Algorithms.Sign(this.private_key, signatureData); + let signature = await Algorithms.Sign(this.private_key, signatureData, this.public_key); //let signature = await crypto.subtle.sign({name: 'ECDSA', hash: 'SHA-256'}, this.private_key, signatureData); // Convert signature @@ -608,6 +771,11 @@ window.AuthnDevice = (function (localURL) { // Clear master key if not needed if (this.askmasterkey) this.clearMasterKey(); + + + + + // Return Credentials return new (VirtualPublicKeyCredential())({ 'id': window.authnTools.uint8ArrayToBase64url(credential_id), @@ -699,9 +867,63 @@ window.AuthnDevice = (function (localURL) { // Generate Authenticator Data let rp_id_hash = new Uint8Array(await window.crypto.subtle.digest('SHA-256', new TextEncoder().encode(this.rp_id))); let flags = new Uint8Array([this._flags({ uv: true, up: true })]); - let sign_count = window.authnTools.intToUint8Array(this._getSignCount(), 4, true); + let currentSignCount = this._getSignCount(); + let sign_count = window.authnTools.intToUint8Array(currentSignCount,4,true); + //let sign_count = window.authnTools.intToUint8Array(this._getSignCount(), 4, true); let authData = this._concatUint8Arrays(rp_id_hash, flags, sign_count); + console.log("AUTH DATA:", { + rpIdHash: window.authnTools.uint8ArrayToBase64url(rp_id_hash), + flags: flags[0].toString(16), + signCount: this._getSignCount(), + authData: window.authnTools.uint8ArrayToBase64url(authData) + }); + + // Update credential management metadata + if (this.currentCredential) { + + this.currentCredential.signCount = currentSignCount; + this.currentCredential.lastUsed = Date.now(); + + if (this.handleStorage) { + await this.handleStorage(this.storage); + } + } + + let expectedRpIdHash = await crypto.subtle.digest( + "SHA-256", + new TextEncoder().encode(rpid) + ); + + let actualRpIdHash = window.authnTools.uint8ArrayToBase64url( + authData.slice(0,32) + ); + + let expectedRpIdHash64 = window.authnTools.uint8ArrayToBase64url( + new Uint8Array(expectedRpIdHash) + ); + + console.log("RP ID HASH CHECK:", { + rpId: rpid, + credentialRpId: this.rp_id, + authDataRpIdHash: actualRpIdHash, + expectedRpIdHash: expectedRpIdHash64, + match: actualRpIdHash === expectedRpIdHash64 + }); + + console.log("ASSERTION STATE:", { + requestedRpId: rpid, + credentialRpId: this.rp_id, + userHandleLength: this.user_handle?.length, + signCount: currentSignCount + }); + + console.log("ASSERTION USER HANDLE:", + this.user_handle + ? window.authnTools.uint8ArrayToBase64url(new Uint8Array(this.user_handle)) + : null + ); + // Testing: Relay Party ID if (this.testing.relayPartyID) { origin = new URL('https://' + this.testing.relayPartyID).origin; @@ -723,16 +945,117 @@ window.AuthnDevice = (function (localURL) { 'type': 'webauthn.get', 'challenge': window.authnTools.uint8ArrayToBase64url(challenge), 'origin': origin, - 'crossOrigin': false, - 'authenticator': 'WebAuthnLinux Fingerprint Bridge' + 'crossOrigin': false } client_data = JSON.stringify(client_data); client_data = new TextEncoder().encode(client_data); + console.log("CLIENT DATA:", + new TextDecoder().decode(client_data) + ); + // Generate signature let client_data_hash = new Uint8Array(await crypto.subtle.digest('SHA-256', client_data)); let signatureData = this._concatUint8Arrays(authData, client_data_hash); - let signature = await Algorithms.Sign(this.private_key, signatureData); + + console.log("SIGNATURE DATA LENGTH:", signatureData.length); + + // --------------------------------------------------------------------- + // ASSERTION INPUTS + // --------------------------------------------------------------------- + + console.log("ASSERTION STATE:", { + rpId: rpid, + credentialId: window.authnTools.uint8ArrayToBase64url(credential_id), + userHandleLength: this.user_handle.length, + signCount: new DataView(authData.buffer).getUint32(33), + flags: authData[32] + }); + + // --------------------------------------------------------------------- + // KEY MATERIAL + // --------------------------------------------------------------------- + + let jwkPub = await crypto.subtle.exportKey("jwk", this.public_key); + + console.log("PUBLIC KEY (JWK):", jwkPub); + + console.log("COSE KEY:", { + x: window.authnTools.uint8ArrayToBase64url(this.cose_key.get(-2)), + y: window.authnTools.uint8ArrayToBase64url(this.cose_key.get(-3)) + }); + + let cose_key = new Uint8Array(window.CBOR.encode(this.cose_key)); + + console.log("COSE KEY HEX:", Array.from(cose_key) + .map(b => b.toString(16).padStart(2, '0')) + .join('')); + + console.log("KEY OBJECTS:", { + privateKeyType: this.private_key?.constructor?.name, + publicKeyType: this.public_key?.constructor?.name, + algorithm: this.private_key.algorithm, + extractable: this.private_key.extractable, + usages: this.private_key.usages + }); + + let restoredPublic = await Algorithms.CryptoKey2COSEKey(this.public_key); + + console.log("RESTORED PUBLIC COSE:", { + x: window.authnTools.uint8ArrayToBase64url(restoredPublic.get(-2)), + y: window.authnTools.uint8ArrayToBase64url(restoredPublic.get(-3)) + }); + + console.log("STORED COSE:", { + x: window.authnTools.uint8ArrayToBase64url(this.cose_key.get(-2)), + y: window.authnTools.uint8ArrayToBase64url(this.cose_key.get(-3)) + }); + + // --------------------------------------------------------------------- + // SIGNING + // --------------------------------------------------------------------- + + let signature = await Algorithms.Sign(this.private_key,signatureData,this.public_key); + + console.log("SIGNATURE:", { + length: signature.byteLength, + hex: Array.from(signature) + .map(b => b.toString(16).padStart(2, '0')) + .join('') + }); + + // --------------------------------------------------------------------- + // ASSERTION SUMMARY + // --------------------------------------------------------------------- + + console.log("ASSERTION CREATED:", { + credentialId: window.authnTools.uint8ArrayToBase64url(credential_id), + authDataLength: authData.byteLength, + clientDataLength: client_data.byteLength, + signatureLength: signature.byteLength + }); + + // --------------------------------------------------------------------- + // ASSERTION DATA + // --------------------------------------------------------------------- + + console.log("AUTHDATA HEX:", + Array.from(authData) + .map(b => b.toString(16).padStart(2,'0')) + .join('') + ); + + console.log("CLIENT HASH HEX:", + Array.from(client_data_hash) + .map(b => b.toString(16).padStart(2,'0')) + .join('') + ); + + console.log("SIGNATURE DATA HEX:", + Array.from(signatureData) + .map(b => b.toString(16).padStart(2,'0')) + .join('') + ); //let signature = await crypto.subtle.sign({name: 'ECDSA', hash: 'SHA-256'}, this.private_key, signatureData); // Convert signature //signature = ans1.parseSignatureECDSA(signature); @@ -740,15 +1063,84 @@ window.AuthnDevice = (function (localURL) { // Clear master key if not needed if (this.askmasterkey) this.clearMasterKey(); + + console.log("RETURNING CREDENTIAL"); + + console.log({ + id: credential_id.byteLength, + rawId: credential_id.byteLength, + authData: authData.buffer.slice(authData.byteOffset,authData.byteOffset + authData.byteLength).byteLength, + signature: signature.buffer.slice(signature.byteOffset,signature.byteOffset + signature.byteLength).byteLength, + userHandle: this.user_handle ? this.user_handle.byteLength : null + }); + + console.log("AUTH DATA TYPE:", authData.constructor.name); + + console.log( + "USER HANDLE TYPE:", + this.user_handle ? this.user_handle.constructor.name : "null" + ); + + if (this.user_handle) { + console.log( + "USER HANDLE HEX:", + Array.from(this.user_handle) + .map(b => b.toString(16).padStart(2,'0')) + .join('') + ); + } + + console.log( + "RAWID HEX:", + Array.from(credential_id) + .map(b => b.toString(16).padStart(2,'0')) + .join('') + ); + + console.log("RESPONSE TYPES:", { + authData: authData.constructor.name, + signature: signature.constructor.name, + userHandle: this.user_handle ? this.user_handle.constructor.name : "null" + }); + + console.log("ASSERTION CREATED:", { + credentialId: this.credential_id ? + window.authnTools.uint8ArrayToBase64url(this.credential_id) + : null, + authDataLength: authData.length, + signatureLength: signature.length, + clientDataLength: client_data.length + }); + + + console.log("FINAL ASSERTION CREDENTIAL CHECK", { + credential_id: this.credential_id ? + window.authnTools.uint8ArrayToBase64url(this.credential_id) + : null, + length: this.credential_id?.byteLength + }); + + console.log("FINAL credential_id DEBUG:", { + value: this.credential_id, + type: typeof this.credential_id, + constructor: this.credential_id?.constructor?.name, + hasBuffer: !!this.credential_id?.buffer + }); + // Return Credentials return new (VirtualPublicKeyCredential())({ - 'id': window.authnTools.uint8ArrayToBase64url(credential_id), - 'rawId': credential_id.buffer.slice(0), + 'id': window.authnTools.uint8ArrayToBase64url(this.credential_id), + 'rawId': this.credential_id instanceof ArrayBuffer + ? this.credential_id + : this.credential_id.buffer.slice( + this.credential_id.byteOffset, + this.credential_id.byteOffset + this.credential_id.byteLength + ), 'response': { - 'authenticatorData': authData.buffer, - 'userHandle': this.user_handle.buffer, - 'signature': signature.buffer, - 'clientDataJSON': client_data.buffer, + 'authenticatorData': authData.buffer.slice(authData.byteOffset,authData.byteOffset + authData.byteLength), + 'userHandle': this.user_handle.buffer.slice(this.user_handle.byteOffset,this.user_handle.byteOffset + this.user_handle.byteLength), + 'signature': signature.buffer.slice(signature.byteOffset,signature.byteOffset + signature.byteLength), + 'clientDataJSON': client_data.buffer.slice(client_data.byteOffset,client_data.byteOffset + client_data.byteLength), }, 'type': 'public-key', @@ -1011,14 +1403,105 @@ window.AuthnDevice = (function (localURL) { return params.filter(param => (param.type == 'public-key' && algs.includes(param.alg))); }, - Sign: async function (private_key, data) { + Sign: async function (private_key, data, public_key) { let type = private_key.algorithm; + console.log("SIGN INPUT:", { + type: Object.prototype.toString.call(data), + length: data?.byteLength, + isArrayBuffer: data instanceof ArrayBuffer, + isUint8Array: data instanceof Uint8Array, + hex: data ? Array.from(new Uint8Array(data)) + .map(b => b.toString(16).padStart(2, '0')) + .join('') + : null + }); + + console.log("SIGNING KEY:", { + type: private_key?.type, + algorithm: private_key?.algorithm, + usages: private_key?.usages, + extractable: private_key?.extractable, + constructor: private_key?.constructor?.name + }); + + console.log("PUBLIC KEY:", { + type: public_key?.type, + algorithm: public_key?.algorithm, + usages: public_key?.usages, + constructor: public_key?.constructor?.name + }); + if (type.name == 'ECDSA') { - let signature = await crypto.subtle.sign({ name: 'ECDSA', hash: 'SHA-256' }, private_key, data); - signature = ans1.parseSignatureECDSA(signature); - return signature; + + let rawSignature = await crypto.subtle.sign( + { + name: 'ECDSA', + hash: 'SHA-256' + }, + private_key, + data + ); + + console.log("SIGNING CONTEXT:", { + algorithm: type.name, + curve: type.namedCurve || "unknown", + hashUsedForSign: "SHA-256", + privateKeyType: private_key?.constructor?.name, + publicKeyType: public_key?.constructor?.name + }); + + // This is the native WebCrypto ECDSA format: + // 64 bytes = r (32) + s (32) + let sigBytes = new Uint8Array(rawSignature); + + console.log("RAW ECDSA SIGNATURE:", { + length: sigBytes.length, + format: "P1363 r||s", + hex: Array.from(sigBytes) + .map(b => b.toString(16).padStart(2,'0')) + .join('') + }); + + // Convert WebCrypto P1363 (r||s) signature to ASN.1 DER ECDSA signature + let derSignature; + + try { + derSignature = ans1.parseSignatureECDSA(sigBytes); + + } + catch(e) { + console.log("DER CONVERSION FAILED:", e); + throw e; + } + + console.log("DER CONVERSION OK:", { + length: derSignature.length, + firstByte: derSignature[0].toString(16) + }); + + + console.log("SIGNATURE FORMAT: Converted P1363 → DER"); + + + console.log("FINAL SIGNATURE:", { + length: derSignature.byteLength, + hex: Array.from(derSignature) + .map(b => b.toString(16).padStart(2,'0')) + .join('') + }); + + + console.log("SIGNATURE RETURN:", { + length: derSignature.length, + firstByte: derSignature[0].toString(16), + der: derSignature[0] === 0x30 + }); + + + return derSignature; } + else if (type.name == 'RSA-PSS') { // ToDo Check let signature = await crypto.subtle.sign({ name: 'RSA-PSS', saltLength: 128 }, private_key, data); diff --git a/extension/manager.css b/extension/manager.css new file mode 100644 index 0000000..0661e95 --- /dev/null +++ b/extension/manager.css @@ -0,0 +1,422 @@ +/* + * WebAuthnLinux Credential Manager + */ + +* { + box-sizing: border-box; +} + +body { + margin: 0; + padding: 0; + width: 650px; + min-height: 500px; + + font-family: + -apple-system, + BlinkMacSystemFont, + "Segoe UI", + Helvetica, + Arial, + sans-serif; + + background: #ffffff; + color: #212529; +} + + +/* + * Header + */ + +.header { + position: sticky; + top: 0; + z-index: 10; + + display: flex; + align-items: center; + justify-content: space-between; + + padding: 15px 20px; + + background: white; + border-bottom: 1px solid #dee2e6; +} + + +.header h1 { + margin: 0; + + font-size: 1.35rem; + font-weight: 600; +} + + +.header-actions button { + + border: none; + background: transparent; + + font-size: 1.5rem; + + cursor: pointer; + + color: #495057; +} + + +.header-actions button:hover { + + color: #007bff; +} + + + +/* + * Search toolbar + */ + +.toolbar { + + padding: 12px 20px; + + border-bottom: 1px solid #e9ecef; + + background: #fafafa; + +} + + +#searchBox { + +width: 100%; + +padding: 9px 12px; + +border: 1px solid #ced4da; + +border-radius: 6px; + +font-size: 0.95rem; + +outline: none; +} + + +#searchBox:focus { + +border-color: #007bff; + +box-shadow: +0 0 0 2px rgba(0,123,255,0.15); + +} + + + +/* + * Credential scrolling area + */ + +.credential-list { + + padding: 15px 20px; + + height: 390px; + + overflow-y: auto; + +} + + + +/* + * Credential cards + */ + +.credential-card { + + border: 1px solid #dee2e6; + + border-radius: 8px; + + padding: 14px; + + margin-bottom: 14px; + + background: white; + +} + + +.credential-card:hover { + + border-color: #adb5bd; + + box-shadow: + 0 2px 6px rgba(0,0,0,0.08); + +} + + +.credential-row { + + display: flex; + + margin-bottom: 6px; + + font-size: 0.85rem; + +} + + +.credential-label { + + width: 120px; + + font-weight: 600; + + color: #495057; + + flex-shrink: 0; + +} + + +.credential-value { + + word-break: break-all; + + color: #212529; + +} + + + +/* + * Credential actions + */ + +.credential-actions { + + display: flex; + + justify-content: flex-end; + + gap: 8px; + + margin-top: 12px; + +} + + +.action-button { + + width: 34px; + height: 30px; + + border: none; + + border-radius: 5px; + + cursor: pointer; + + background: #f1f3f5; + + font-size: 1rem; + +} + + +.action-button:hover { + + background: #dee2e6; + +} + + +.action-delete:hover { + + background: #ffd8d8; + + color: #b00020; + +} + + + +/* + * Empty state + */ + +.empty-message { + + text-align: center; + + padding: 40px; + + color: #6c757d; + + font-size: 0.95rem; + +} + + + +/* + * Footer + */ + +.footer { + + position: sticky; + + bottom: 0; + + padding: 10px; + + text-align: center; + + font-size: 0.75rem; + + color: #adb5bd; + + border-top: 1px solid #e9ecef; + + background: white; + +} + + + +/* + * Dialog overlay + */ + +.dialog { + + position: fixed; + + inset: 0; + + display: flex; + + align-items: center; + + justify-content: center; + + background: + rgba(0,0,0,0.35); + + z-index: 100; + +} + + +.dialog.hidden { + + display: none; + +} + + + +.dialog-box { + + width: 360px; + + padding: 20px; + + background: white; + + border-radius: 8px; + + box-shadow: + 0 5px 20px rgba(0,0,0,0.25); + +} + + +.dialog-box h2 { + + margin-top: 0; + + font-size: 1.1rem; + +} + + +.dialog-box input { + + width: 100%; + + padding: 9px; + + margin: 10px 0 15px; + + border: + + 1px solid #ced4da; + + border-radius: 5px; + + font-size: 0.95rem; + +} + + + +/* + * Dialog buttons + */ + +.dialog-buttons { + + display: flex; + + justify-content: flex-end; + + gap: 10px; + +} + + +.dialog-buttons button { + + padding: 8px 16px; + + border-radius: 5px; + + border: 1px solid #ced4da; + + background: white; + + cursor: pointer; + +} + + +.dialog-buttons button:hover { + + background: #f1f3f5; + +} + + +.dialog-buttons .danger { + + background: #dc3545; + + color: white; + + border-color: #dc3545; + +} + + +.dialog-buttons .danger:hover { + + background: #bb2d3b; + +} diff --git a/extension/manager.html b/extension/manager.html new file mode 100644 index 0000000..3843450 --- /dev/null +++ b/extension/manager.html @@ -0,0 +1,112 @@ + + + + + + + + Manage Credentials + + + + + + +
+

Manage Credentials

+ +
+ +
+
+ + +
+ + + +
+ + +
+ + + +
+ No credentials found. +
+ +
+ + + + + + + + + + + +
+ + + +

+ + + + + +
+ +
+ + + + + + diff --git a/extension/manager.js b/extension/manager.js new file mode 100644 index 0000000..0ae613e --- /dev/null +++ b/extension/manager.js @@ -0,0 +1,575 @@ +/* + * WebAuthnLinux Credential Manager + * + * Handles: + * - Display credentials + * - Delete credentials + * - Rename user handles + */ + + +const storage = +(typeof browser !== "undefined") +? browser.storage.local +: chrome.storage.local; + + +let credentials = []; + + + +/* + * Load credentials + */ + +async function loadCredentials() { + + const result = await storage.get("system_credentials"); + + credentials = result.system_credentials || []; + + credentials.forEach(credential => { + + if (!credential.label) { + + const handle = + credential.userHandle || + credential.user_handle || + ""; + + const decoded = + decodeUserHandle(handle); + + + // Only use as label if it looks like readable text + if (/^[\x20-\x7E]+$/.test(decoded)) { + + credential.label = decoded; + + } + else { + + credential.label = + credential.host || "Unnamed"; + + } + } + + }); + + renderCredentials(); +} + + +function decodeUserHandle(value) { + if (!value) return "(none)"; + + try { + const base64 = value + .replace(/-/g, '+') + .replace(/_/g, '/'); + + const bytes = Uint8Array.from( + atob(base64), + c => c.charCodeAt(0) + ); + + return new TextDecoder().decode(bytes); + + } catch (e) { + console.error("decodeUserHandle failed:", e, value); + return value; + } +} + +function encodeUserHandle(text) { + const bytes = new TextEncoder().encode(text); + + let binary = ""; + + for (const b of bytes) { + binary += String.fromCharCode(b); + } + + return btoa(binary) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=/g, ""); +} + +function askForRename(oldName) { + + return new Promise(resolve => { + + const dialog = + document.getElementById("renameDialog"); + + const input = + document.getElementById("renameInput"); + + + input.value = oldName; + + + dialog.showModal(); + + + dialog.addEventListener( + "close", + function handler() { + + dialog.removeEventListener( + "close", + handler + ); + + + if (dialog.returnValue === "ok") { + + resolve(input.value); + + } + else { + + resolve(null); + + } + + } + ); + + }); +} + + +function askForDelete(message) { + + return new Promise(resolve => { + + const dialog = + document.getElementById("deleteDialog"); + + const text = + document.getElementById("deleteMessage"); + + const confirmButton = + document.getElementById("confirmDelete"); + + const cancelButton = + document.getElementById("cancelDelete"); + + + text.textContent = message; + + + const cleanup = () => { + + dialog.classList.add("hidden"); + + confirmButton.removeEventListener( + "click", + confirmHandler + ); + + cancelButton.removeEventListener( + "click", + cancelHandler + ); + }; + + + const confirmHandler = () => { + + cleanup(); + resolve(true); + + }; + + + const cancelHandler = () => { + + cleanup(); + resolve(false); + + }; + + + confirmButton.addEventListener( + "click", + confirmHandler + ); + + + cancelButton.addEventListener( + "click", + cancelHandler + ); + + + dialog.classList.remove("hidden"); + + }); +} + +function displayUserHandle(value) { + + if (!value) + return "(none)"; + + const bytes = Uint8Array.from( + atob( + value.replace(/-/g, '+').replace(/_/g, '/') + ), + c => c.charCodeAt(0) + ); + + const text = + new TextDecoder().decode(bytes); + + + if (/^[\x20-\x7E]+$/.test(text)) { + return text; + } + + + return "(binary) " + + Array.from(bytes.slice(0,16)) + .map(b => b.toString(16).padStart(2,"0")) + .join("") + + "..."; +} + + +/* + * Render credential list + */ + +function renderCredentials() { + + const container = + document.getElementById("credentialList"); + + + container.innerHTML = ""; + + + if (credentials.length === 0) { + + container.innerHTML = + ` +
+ No credentials stored. +
+ `; + + return; + } + + + + credentials.forEach((credential, index) => { + + + const card = + document.createElement("div"); + + + card.className = + "credential-card"; + + + + card.innerHTML = + ` + +
+
+ RP +
+
+ ${escapeHtml(credential.host || "")} +
+
+ + +
+
+ Credential ID +
+
+ ${escapeHtml(credential.keyid || "")} +
+
+ + +
+
+ Label +
+ +
+ ${escapeHtml( + credential.label || "Unnamed" + )} +
+
+ + +
+
+ User Handle +
+ +
+ ${escapeHtml( + displayUserHandle( + credential.userHandle || credential.user_handle + ) + )} +
+
+ + +
+
+ Created +
+
+ ${formatDate(credential.created)} +
+
+ + +
+
+ Last Used +
+
+ ${formatDate(credential.lastUsed)} +
+
+ + +
+
+ Sign Count +
+
+ ${credential.signCount || 0} +
+
+ + + +
+ + + + + + +
+ + `; + + + + card.querySelector( + '[data-action="rename"]' + ) + .onclick = () => renameCredential(index); + + + + card.querySelector( + '[data-action="delete"]' + ) + .onclick = () => deleteCredential(index); + + + + container.appendChild(card); + + }); + +} + + + +/* + * Delete credential + */ + +async function deleteCredential(index) { + + + const credential = + credentials[index]; + + + const confirmDelete = + await askForDelete( + "Delete credential for " + + credential.host + + "?" + ); + + + if (!confirmDelete) + return; + + + + credentials.splice(index,1); + + + await storage.set({ + system_credentials: credentials + }); + + + renderCredentials(); + +} + + + +/* + * Rename Label + */ + +async function renameCredential(index) { + + const credential = + credentials[index]; + + + const oldName = + credential.label || ""; + + + const newName = + await askForRename(oldName); + + + if (newName === null) + return; + + + console.log("RENAME LABEL INPUT:", newName); + + + if (!newName || newName.trim() === "") { + alert("Label cannot be empty"); + return; + } + + + credential.label = + newName.trim(); + + + await storage.set({ + system_credentials: credentials + }); + + + await loadCredentials(); + +} + + + +/* + * Date formatting + */ + +function formatDate(value) { + + if (!value) + return "Unknown"; + + + try { + + return new Date(value) + .toLocaleString(); + + } + catch { + + return value; + + } + +} + + + +/* + * Basic HTML protection + */ + +function escapeHtml(value) { + + return String(value) + .replaceAll("&","&") + .replaceAll("<","<") + .replaceAll(">",">") + .replaceAll('"',""") + .replaceAll("'","'"); + +} + + + +/* + * Search + */ + +document +.getElementById("searchBox") +?.addEventListener( + "input", + function() { + + const text = + this.value + .toLowerCase(); + + + document + .querySelectorAll( + ".credential-card" + ) + .forEach(card => { + + card.style.display = + card.innerText + .toLowerCase() + .includes(text) + ? "" + : "none"; + + }); + + } +); + + + +/* + * Start + */ + +loadCredentials(); diff --git a/extension/manifest.chrome.json b/extension/manifest.chrome.json new file mode 100644 index 0000000..f7da799 --- /dev/null +++ b/extension/manifest.chrome.json @@ -0,0 +1,69 @@ +{ + "name": "WebAuthnLinux", + "description": "Biometric WebAuthn Credentials for Linux", + "author": "Samveen, building on the work of Grammatopoulos Athanasios Vasileios (GramThanos)", + "version": "0.9.10", + "homepage_url": "https://github.com/samveen/WebAuthnLinux", + "icons": { + "19": "icons/icon.png", + "48": "icons/icon_48.png", + "128": "icons/icon_128.png" + }, + "content_scripts": [ + { + "matches": [ + "http://*/*", + "https://*/*" + ], + "js": [ + "inject.js" + ], + "run_at": "document_start", + "all_frames": true + } + ], + "action": { + "default_popup": "popup.html" + }, + "background": { + "service_worker": "background.js" + }, + "host_permissions": [ + "" + ], + "permissions": [ + "storage", + "nativeMessaging" + ], + "web_accessible_resources": [ + { + "resources": [ + "webauthn-linux.js", + "js/webauthn-authenticator.js", + "js/webauthn-tools.js", + "js/cbor.js", + "js/falcon.js", + "js/falcon.wasm" + ], + "matches": [ + "https://*/*", + "http://*/*" + ] + } + ], + "manifest_version": 3, + "content_security_policy": { + "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self'" + }, + "browser_specific_settings": { + "gecko": { + "id": "webauthnlinux@samveen.github.io", + "data_collection_permissions": { + "required": [ + "none" + ], + "optional": [] + } + } + } +} diff --git a/extension/popup.html b/extension/popup.html index 6b5ca88..ed8aee0 100644 --- a/extension/popup.html +++ b/extension/popup.html @@ -138,6 +138,36 @@ text-decoration: underline; } + + + .actions { + margin-top: 18px; + } + + .action-button { + width: 100%; + padding: 10px 12px; + font-size: 0.95rem; + font-weight: 600; + background: #007bff; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color .2s; + } + + .action-button:hover { + background: #005ecb; + } + + .action-button:active { + background: #004ea8; + } + + + + .status-msg { margin-top: 10px; padding: 8px; @@ -147,6 +177,10 @@ color: #0056b3; display: none; } + + + + @@ -188,6 +222,12 @@

WebAuthnLinux

+
+ +
+
Changes saved automatically.