From 8286b1143e1a1c8884aa3a2e5a8b555215ea3e0f Mon Sep 17 00:00:00 2001 From: kousu Date: Sat, 31 May 2025 17:37:25 -0400 Subject: [PATCH 1/2] Load/store FAST keys for auto_login. Also clean up FAST *and* SCRAM keys on log out; otherwise, the credentials are still in the browser, and could be stolen, or reused simply by someone who knows to redefined conversejs-session-jid in localStorage. Depends on https://github.com/strophe/strophejs/pull/839 TODO: * [ ] This *renames* reuse_scram_keys to reuse_keys to cover both FAST and SCRAM, so it should probably get a backwards-compatibility shim for the old name. * [ ] Drop my development environment edit to package.json (without there's no way to test because both repos need to be in sync) --- docs/source/configuration.rst | 15 ++++---- docs/source/session.rst | 8 +++-- package-lock.json | 3 +- package.json | 3 +- src/headless/shared/api/user.js | 11 ++++-- src/headless/shared/settings/constants.js | 4 +-- .../types/shared/settings/constants.d.ts | 8 ++--- src/headless/utils/init.js | 34 ++++++++++++++----- 8 files changed, 58 insertions(+), 28 deletions(-) diff --git a/docs/source/configuration.rst b/docs/source/configuration.rst index 7b8da9c292..4adf6d64c3 100644 --- a/docs/source/configuration.rst +++ b/docs/source/configuration.rst @@ -1887,25 +1887,26 @@ Based on the OGP metadata Converse will render a URL preview (also known as an the ``show_images_inline``, ``embed_audio`` and ``embed_videos`` settings. -reuse_scram_keys ----------------- +reuse_keys +---------- * Default: ``true`` Most XMPP servers enable the Salted Challenge Response Authentication Mechanism -or SCRAM for short. This allows the user and the server to mutually -authenticate *without* the need to transmit the user's password in plaintext. +or SCRAM for short. Newer servers also support Fast Authentication Streamlining Tokens. +These allow the user and the server to mutually authenticate *without* the need +to transmit the user's password in plaintext. Assuming the server does not alter the user's password or the -storage parameters, we can authenticate with the same SCRAM key multiple times. +storage parameters, we can authenticate with the same key multiple times. This opens an opportunity: we can store the user's login credentials in the browser without storing the sensitive plaintext password, or the need to set up complicated third party backends, like OAuth. -Enabling this option will let Converse save a user's SCRAM keys upon successful +Enabling this option will let Converse save a user's keys upon successful login, and next time Converse is loaded the user will be automatically logged in -with those SCRAM keys. +with those keys. .. _`roomconfig_whitelist`: diff --git a/docs/source/session.rst b/docs/source/session.rst index 8e8874ce56..28d617b6fd 100644 --- a/docs/source/session.rst +++ b/docs/source/session.rst @@ -196,10 +196,12 @@ We've purposefully not put this functionality in Converse.js due to the security implications of storing plaintext passwords in localStorage. -Storing the SASL SCRAM-SHA1 hash in IndexedDB ---------------------------------------------- +Storing the SASL SCRAM-SHA1 hash or FAST token in IndexedDB +----------------------------------------------------------- Another suggestion that's been suggested is to store the SCRAM-SHA1 computed ``clientKey`` in localStorage and to use that upon page reload to log the user in again. -This has been implemented since version 10, see documentation on `reuse_scram_keys `_ +In more modern terms, we can store the FAST token if supported by the server. + +This has been implemented since version 10, see documentation on `reuse_keys `_ diff --git a/package-lock.json b/package-lock.json index f204e9bf49..08a21c6488 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,8 @@ "pluggable.js": "^3.0.1", "prettier": "^3.2.5", "sizzle": "^2.3.5", - "sprintf-js": "^1.1.2" + "sprintf-js": "^1.1.2", + "strophe.js": "file:../strophejs" }, "devDependencies": { "@converse/headless": "file:src/headless", diff --git a/package.json b/package.json index 576241e47e..0c56d68cc5 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,8 @@ "pluggable.js": "^3.0.1", "prettier": "^3.2.5", "sizzle": "^2.3.5", - "sprintf-js": "^1.1.2" + "sprintf-js": "^1.1.2", + "strophe.js": "file:../strophejs" }, "resolutions": { "autoprefixer": "10.4.5" diff --git a/src/headless/shared/api/user.js b/src/headless/shared/api/user.js index ad4d38acea..3d786e5e7c 100644 --- a/src/headless/shared/api/user.js +++ b/src/headless/shared/api/user.js @@ -5,7 +5,7 @@ import _converse from '../_converse.js'; import presence_api from './presence.js'; import connection_api from '../connection/api.js'; import { replacePromise } from '../../utils/session.js'; -import { attemptNonPreboundSession, setUserJID } from '../../utils/init.js'; +import { attemptNonPreboundSession, setUserJID, savedLoginInfo } from '../../utils/init.js'; import { getOpenPromise } from '@converse/openpromise'; import { user_settings_api } from '../settings/user/api.js'; import { LOGOUT } from '../constants.js'; @@ -108,8 +108,15 @@ const api = { // Recreate all the promises Object.keys(_converse.promises).forEach((p) => replacePromise(_converse, p)); - // Remove the session JID, otherwise the user would just be logged + // Remove the session JID/keys, otherwise the user would just be logged // in again upon reload. See #2759 + const jid = localStorage.getItem("conversejs-session-jid"); + if (jid) { + savedLoginInfo(jid).then((login_info) => { + // XXX TODO: is there a cleaner call? .remove()? .clear()? + login_info.save({ fast: null, scram_keys: null }); + }) + } localStorage.removeItem('conversejs-session-jid'); /** diff --git a/src/headless/shared/settings/constants.js b/src/headless/shared/settings/constants.js index fcca140e2b..3a5cd8bdff 100644 --- a/src/headless/shared/settings/constants.js +++ b/src/headless/shared/settings/constants.js @@ -6,7 +6,7 @@ * @property {String} [assets_path='/dist'] * @property {('login'|'prebind'|'anonymous'|'external')} [authentication='login'] * @property {Boolean} [auto_login=false] - Currently only used in connection with anonymous login - * @property {Boolean} [reuse_scram_keys=true] - Save SCRAM keys after login to allow for future auto login + * @property {Boolean} [reuse_keys=true] - Save SCRAM/FAST keys after login to allow for future auto login * @property {Boolean} [auto_reconnect=true] * @property {Array} [blacklisted_plugins] * @property {Boolean} [clear_cache_on_logout=false] @@ -50,7 +50,7 @@ export const DEFAULT_SETTINGS = { geouri_replacement: 'https://www.openstreetmap.org/?mlat=$1&mlon=$2#map=18/$1/$2', i18n: undefined, jid: undefined, - reuse_scram_keys: true, + reuse_keys: true, keepalive: true, loglevel: 'info', locales: [ diff --git a/src/headless/types/shared/settings/constants.d.ts b/src/headless/types/shared/settings/constants.d.ts index 4b1f46e40f..c38baecef0 100644 --- a/src/headless/types/shared/settings/constants.d.ts +++ b/src/headless/types/shared/settings/constants.d.ts @@ -16,7 +16,7 @@ export namespace DEFAULT_SETTINGS { let geouri_replacement: string; let i18n: any; let jid: any; - let reuse_scram_keys: boolean; + let reuse_keys: boolean; let keepalive: boolean; let loglevel: string; let locales: string[]; @@ -46,9 +46,9 @@ export type ConfigurationSettings = { */ auto_login?: boolean; /** - * - Save SCRAM keys after login to allow for future auto login + * - Save keys after login to allow for future auto login */ - reuse_scram_keys?: boolean; + reuse_keys?: boolean; auto_reconnect?: boolean; blacklisted_plugins?: Array; clear_cache_on_logout?: boolean; @@ -77,4 +77,4 @@ export type ConfigurationSettings = { websocket_url?: string; whitelisted_plugins?: Array; }; -//# sourceMappingURL=constants.d.ts.map \ No newline at end of file +//# sourceMappingURL=constants.d.ts.map diff --git a/src/headless/utils/init.js b/src/headless/utils/init.js index b8426b23f5..cabffd1983 100644 --- a/src/headless/utils/init.js +++ b/src/headless/utils/init.js @@ -348,15 +348,25 @@ async function getLoginCredentialsFromBrowser() { } } -async function getLoginCredentialsFromSCRAMKeys() { +async function getLoginCredentialsFromStorage() { const jid = localStorage.getItem("conversejs-session-jid"); if (!jid) return null; await setUserJID(jid); const login_info = await savedLoginInfo(jid); + + const fast_credential = login_info.get("fast"); + if (fast_credential) { + return { jid, password: fast_credential } + } + const scram_keys = login_info.get("scram_keys"); - return scram_keys ? { jid, password: scram_keys } : null; + if (scram_keys) { + return { jid, password: scram_keys } + } + + return null } /** @@ -393,8 +403,13 @@ export async function attemptNonPreboundSession(credentials, automatic) { return connect(); } - if (api.settings.get("reuse_scram_keys")) { - const credentials = await getLoginCredentialsFromSCRAMKeys(); + if (api.settings.get("reuse_keys")) { + // XXX if connecting with FAST, we need to present the same user-agent and client-id as before + // This is currently implemented by storing the client ID as part of the fast keys + // and having Strophe generate it if unset. + // but it makes more sense for the client, Converse, to handle that. + // and it should probably coordinate with the OMEMO plugin. + const credentials = await getLoginCredentialsFromStorage(); if (credentials) return connect(credentials); } @@ -423,7 +438,7 @@ export async function attemptNonPreboundSession(credentials, automatic) { * used login keys. */ export async function savedLoginInfo(jid) { - const id = `converse.scram-keys-${Strophe.getBareJidFromJid(jid)}`; + const id = `converse.keys-${Strophe.getBareJidFromJid(jid)}`; if (_converse.state.login_info?.get("id") === id) { return _converse.state.login_info; } @@ -479,8 +494,9 @@ async function connect(credentials) { let callback; // Save the SCRAM data if we're not already logged in with SCRAM - if (_converse.state.config.get("trusted") && jid && api.settings.get("reuse_scram_keys") && !password?.ck) { - // Store scram keys in scram storage + if (_converse.state.config.get("trusted") && jid && api.settings.get("reuse_keys") && !password?.ck) { + // Store scram/fast keys in storage + // XXX seems like we could move the *loading* of keys from storage here, since login_info contains them const login_info = await savedLoginInfo(jid); callback = /** @@ -488,11 +504,13 @@ async function connect(credentials) { * @param {string} message */ (status, message) => { - const { scram_keys } = connection; + const { scram_keys, fast } = connection; if (scram_keys) login_info.save({ scram_keys }); + if (fast?.credential?.token) login_info.save('fast', fast.credential); connection.onConnectStatusChanged(status, message); }; } + connection.connect(jid, password, callback); } } From 765881c69b921a008ef37f489f61ebc2c3ede405 Mon Sep 17 00:00:00 2001 From: kousu Date: Tue, 10 Jun 2025 02:49:36 -0400 Subject: [PATCH 2/2] Fix CI --- package-lock.json | 7 +++---- package.json | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 08a21c6488..96a22f01ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "prettier": "^3.2.5", "sizzle": "^2.3.5", "sprintf-js": "^1.1.2", - "strophe.js": "file:../strophejs" + "strophe.js": "github:kousu/strophejs#sasl2_fast_2" }, "devDependencies": { "@converse/headless": "file:src/headless", @@ -8147,9 +8147,8 @@ } }, "node_modules/strophe.js": { - "version": "4.0.0-rc0", - "resolved": "https://registry.npmjs.org/strophe.js/-/strophe.js-4.0.0-rc0.tgz", - "integrity": "sha512-9j2hR/OsxFX1gmqcsxNOQySrUUju0blHAmGB5g5EcdlVjWn19u+xHKEoXt4Ft8VPBB9rQR0jvtQkAJPpqM9XTw==", + "version": "3.1.1", + "resolved": "git+ssh://git@github.com/kousu/strophejs.git#0d0c7fe3b3badfb52f190dd0b1d02e00986267b3", "license": "MIT", "peerDependencies": { "jsdom": ">=20.0.0", diff --git a/package.json b/package.json index 0c56d68cc5..89b9f9b43e 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ "prettier": "^3.2.5", "sizzle": "^2.3.5", "sprintf-js": "^1.1.2", - "strophe.js": "file:../strophejs" + "strophe.js": "github:kousu/strophejs#sasl2_fast_2" }, "resolutions": { "autoprefixer": "10.4.5"