Add support for Related Origin Requests with passkeys (#2828)

This commit is contained in:
Sami Vänttinen 2026-02-16 18:08:26 +02:00 committed by GitHub
parent 8d4e46882d
commit bf0969aefe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 74 additions and 18 deletions

View file

@ -3,7 +3,7 @@
"name": "KeePassXC-Browser", "name": "KeePassXC-Browser",
"version": "1.9.11", "version": "1.9.11",
"version_name": "1.9.11", "version_name": "1.9.11",
"minimum_chrome_version": "93", "minimum_chrome_version": "124",
"description": "__MSG_extensionDescription__", "description": "__MSG_extensionDescription__",
"author": "KeePassXC Team", "author": "KeePassXC Team",
"icons": { "icons": {

View file

@ -185,7 +185,7 @@
"applications": { "applications": {
"gecko": { "gecko": {
"id": "keepassxc-browser@keepassxc.org", "id": "keepassxc-browser@keepassxc.org",
"strict_min_version": "96.0" "strict_min_version": "100.0"
} }
}, },
"storage": { "storage": {

View file

@ -10,20 +10,23 @@ keepass.featuresList = {
passkeysDefaultGroup: false, passkeysDefaultGroup: false,
requiredKeePassXCVersionFound: false, requiredKeePassXCVersionFound: false,
}; };
keepass.keyPair = { publicKey: null, secretKey: null }; keepass.cacheTimeout = 30 * 1000; // Milliseconds
keepass.serverPublicKey = '';
keepass.clientID = ''; keepass.clientID = '';
keepass.currentKeePassXC = '';
keepass.databaseHash = '';
keepass.isConnected = false; keepass.isConnected = false;
keepass.isDatabaseClosed = true; keepass.isDatabaseClosed = true;
keepass.isKeePassXCAvailable = false;
keepass.isEncryptionKeyUnrecognized = false; keepass.isEncryptionKeyUnrecognized = false;
keepass.currentKeePassXC = ''; keepass.isKeePassXCAvailable = false;
keepass.requiredKeePassXC = '2.6.0'; keepass.keyPair = { publicKey: null, secretKey: null };
keepass.latestVersionUrl = 'https://api.github.com/repos/keepassxreboot/keepassxc/releases/latest'; keepass.latestVersionUrl = 'https://api.github.com/repos/keepassxreboot/keepassxc/releases/latest';
keepass.cacheTimeout = 30 * 1000; // Milliseconds
keepass.databaseHash = '';
keepass.previousDatabaseHash = ''; keepass.previousDatabaseHash = '';
keepass.reconnectLoop = null; keepass.reconnectLoop = null;
keepass.requiredKeePassXC = '2.6.0';
keepass.serverPublicKey = '';
const DEFAULT_FETCH_TIMEOUT = 5000; // ms
const MAX_RELATED_ORIGIN_LABELS = 60;
const kpActions = { const kpActions = {
SET_LOGIN: 'set-login', SET_LOGIN: 'set-login',
@ -341,7 +344,13 @@ keepass.getDatabaseHash = async function(tab, args = []) {
} }
try { try {
const request = keepassClient.buildRequest(kpAction, keepassClient.encrypt(messageData, nonce), nonce, keepass.clientID, triggerUnlock); const request = keepassClient.buildRequest(
kpAction,
keepassClient.encrypt(messageData, nonce),
nonce,
keepass.clientID,
triggerUnlock,
);
const response = await keepassClient.sendNativeMessage(request, enableTimeout); const response = await keepassClient.sendNativeMessage(request, enableTimeout);
if (response.message && response.nonce) { if (response.message && response.nonce) {
const res = keepassClient.decrypt(response.message, response.nonce); const res = keepassClient.decrypt(response.message, response.nonce);
@ -616,11 +625,14 @@ keepass.passkeysRegister = async function(tab, args = []) {
const kpAction = kpActions.PASSKEYS_REGISTER; const kpAction = kpActions.PASSKEYS_REGISTER;
const nonce = keepassClient.getNonce(); const nonce = keepassClient.getNonce();
const [ publicKey, origin ] = args; const [ publicKey, origin ] = args;
const passkeyPublicKey = JSON.parse(JSON.stringify(publicKey));
const relatedOrigins = await keepass.getPasskeysRelatedOrigins(passkeyPublicKey?.rp?.id);
const messageData = { const messageData = {
action: kpAction, action: kpAction,
publicKey: JSON.parse(JSON.stringify(publicKey)), publicKey: passkeyPublicKey,
origin: origin, origin: origin,
relatedOrigins: relatedOrigins,
groupName: page?.settings?.defaultPasskeyGroup, groupName: page?.settings?.defaultPasskeyGroup,
keys: keepass.getCryptoKeys() keys: keepass.getCryptoKeys()
}; };
@ -648,13 +660,15 @@ keepass.passkeysGet = async function(tab, args = []) {
const kpAction = kpActions.PASSKEYS_GET; const kpAction = kpActions.PASSKEYS_GET;
const nonce = keepassClient.getNonce(); const nonce = keepassClient.getNonce();
const publicKey = args[0]; const [ publicKey, origin ] = args;
const origin = args[1]; const passkeyPublicKey = JSON.parse(JSON.stringify(publicKey));
const relatedOrigins = await keepass.getPasskeysRelatedOrigins(passkeyPublicKey?.rp?.id);
const messageData = { const messageData = {
action: kpAction, action: kpAction,
publicKey: JSON.parse(JSON.stringify(publicKey)), publicKey: passkeyPublicKey,
origin: origin, origin: origin,
relatedOrigins: relatedOrigins,
keys: keepass.getCryptoKeys() keys: keepass.getCryptoKeys()
}; };
@ -810,7 +824,9 @@ keepass.disableAutomaticReconnect = function() {
keepass.reconnect = async function(tab = null, connectionTimeout = 1500) { keepass.reconnect = async function(tab = null, connectionTimeout = 1500) {
keepassClient.connectToNative(); keepassClient.connectToNative();
keepass.generateNewKeyPair(); keepass.generateNewKeyPair();
const keyChangeResult = await keepass.changePublicKeys(tab, !!connectionTimeout, connectionTimeout).catch(() => false); const keyChangeResult = await keepass
.changePublicKeys(tab, !!connectionTimeout, connectionTimeout)
.catch(() => false);
// Change public keys timeout // Change public keys timeout
if (!keyChangeResult) { if (!keyChangeResult) {
@ -866,7 +882,9 @@ keepass.setcurrentKeePassXCVersion = function(version) {
keepass.keePassXCUpdateAvailable = async function() { keepass.keePassXCUpdateAvailable = async function() {
const checkUpdate = Number(page.settings.checkUpdateKeePassXC); const checkUpdate = Number(page.settings.checkUpdateKeePassXC);
if (checkUpdate !== CHECK_UPDATE_NEVER) { if (checkUpdate !== CHECK_UPDATE_NEVER) {
const lastChecked = (keepass.latestKeePassXC.lastChecked) ? new Date(keepass.latestKeePassXC.lastChecked) : new Date(1986, 11, 21); const lastChecked = keepass.latestKeePassXC.lastChecked
? new Date(keepass.latestKeePassXC.lastChecked)
: new Date(1986, 11, 21);
const daysSinceLastCheck = Math.floor(((new Date()).getTime() - lastChecked.getTime()) / 86400000); const daysSinceLastCheck = Math.floor(((new Date()).getTime() - lastChecked.getTime()) / 86400000);
if (daysSinceLastCheck >= checkUpdate) { if (daysSinceLastCheck >= checkUpdate) {
await keepass.checkForNewKeePassXCVersion(); await keepass.checkForNewKeePassXCVersion();
@ -882,7 +900,7 @@ keepass.checkForNewKeePassXCVersion = async function() {
let version = -1; let version = -1;
try { try {
const response = await fetch(keepass.latestVersionUrl); const response = await fetch(keepass.latestVersionUrl, { signal: AbortSignal.timeout(DEFAULT_FETCH_TIMEOUT) });
const jsonData = await response.json(); const jsonData = await response.json();
if (jsonData?.tag_name && jsonData?.prerelease === false) { if (jsonData?.tag_name && jsonData?.prerelease === false) {
version = jsonData.tag_name; version = jsonData.tag_name;
@ -894,6 +912,44 @@ keepass.checkForNewKeePassXCVersion = async function() {
keepass.latestKeePassXC.lastChecked = new Date().valueOf(); keepass.latestKeePassXC.lastChecked = new Date().valueOf();
}; };
// Implements retrieval of Related Origin Requests for passkeys
// https://www.w3.org/TR/webauthn-3/#sctn-related-origins
keepass.getPasskeysRelatedOrigins = async function(rpId) {
if (!rpId) {
return [];
}
try {
const response = await fetch(`https://${rpId}/.well-known/webauthn`, {
signal: AbortSignal.timeout(DEFAULT_FETCH_TIMEOUT),
});
// Basic reply validation, see: https://www.w3.org/TR/webauthn-3/#sctn-validating-relation-origin
const isJson = response?.headers?.get('content-type')?.includes('application/json');
if (!isJson) {
logError('getRelatedOrigins error: Content-Type is not JSON');
return [];
}
const jsonData = await response.json();
if (!Array.isArray(jsonData?.origins)
|| jsonData?.origins?.length === 0
|| jsonData?.origins?.length > MAX_RELATED_ORIGIN_LABELS
|| !jsonData?.origins?.every((origin) => typeof origin === 'string')) {
logError(
`getRelatedOrigins error: origins is not a list of strings, or it exceeds the maximum count of ${MAX_RELATED_ORIGIN_LABELS}`,
);
return [];
}
return jsonData.origins;
} catch (ex) {
logError(`getRelatedOrigins error: ${ex}`);
}
return [];
};
keepass.clearErrorMessage = function(tab) { keepass.clearErrorMessage = function(tab) {
if (tab && page.tabs[tab.id]) { if (tab && page.tabs[tab.id]) {
page.tabs[tab.id].errorMessage = undefined; page.tabs[tab.id].errorMessage = undefined;

View file

@ -3,7 +3,7 @@
"name": "KeePassXC-Browser", "name": "KeePassXC-Browser",
"version": "1.9.11", "version": "1.9.11",
"version_name": "1.9.11", "version_name": "1.9.11",
"minimum_chrome_version": "93", "minimum_chrome_version": "124",
"description": "__MSG_extensionDescription__", "description": "__MSG_extensionDescription__",
"author": "KeePassXC Team", "author": "KeePassXC Team",
"icons": { "icons": {