Refactor the code related to storing SCRAM keys
- No need to create a new storage mechanism, just use `persistent`. - Store SCRAM keys per JID - Upon succesfull login, store the current session JID, so that we know who to fetch SCRAM keys for - Only store SCRAM keys when the device is trusted Fixes #3001
This commit is contained in:
parent
8ab0b718de
commit
447fe8ba08
|
@ -5,8 +5,11 @@
|
||||||
- #326: Add the ability to reset your password
|
- #326: Add the ability to reset your password
|
||||||
- #2816: Chat highlight behaves odd
|
- #2816: Chat highlight behaves odd
|
||||||
- #2925: File upload is not always enabled
|
- #2925: File upload is not always enabled
|
||||||
|
- #3001: Add option to save SCRAM details and to use them to stay logged in upon reload
|
||||||
- Add a "Add to Contacts" button in MUC occupant modals
|
- Add a "Add to Contacts" button in MUC occupant modals
|
||||||
|
|
||||||
|
- New config option [save_scram_keys](https://conversejs.org/docs/html/configuration.html#save-scram-keys)
|
||||||
|
|
||||||
## 10.0.0 (2022-10-30)
|
## 10.0.0 (2022-10-30)
|
||||||
|
|
||||||
- Update to Strophe.js 1.6.0 which adds support for SCRAM-SHA-256 and SCRAM-SHA-512
|
- Update to Strophe.js 1.6.0 which adds support for SCRAM-SHA-256 and SCRAM-SHA-512
|
||||||
|
|
1
dev.html
1
dev.html
|
@ -32,6 +32,7 @@
|
||||||
auto_away: 300,
|
auto_away: 300,
|
||||||
enable_smacks: true,
|
enable_smacks: true,
|
||||||
loglevel: 'debug',
|
loglevel: 'debug',
|
||||||
|
reuse_scram_keys: true,
|
||||||
prune_messages_above: 100,
|
prune_messages_above: 100,
|
||||||
message_archiving: 'always',
|
message_archiving: 'always',
|
||||||
muc_respect_autojoin: true,
|
muc_respect_autojoin: true,
|
||||||
|
|
|
@ -407,48 +407,6 @@ in to their XMPP account.
|
||||||
So currently if EITHER ``keepalive`` or ``auto_login`` is ``true`` and
|
So currently if EITHER ``keepalive`` or ``auto_login`` is ``true`` and
|
||||||
`authentication`_ is set to ``login``, then Converse will try to log the user in.
|
`authentication`_ is set to ``login``, then Converse will try to log the user in.
|
||||||
|
|
||||||
save_scram_keys
|
|
||||||
---------------
|
|
||||||
* Default: ``false``
|
|
||||||
|
|
||||||
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.
|
|
||||||
Coincidentally, assuming the server does not alter the user's password or the
|
|
||||||
storage parameters, we can authenticate with the same SCRAM key multiple times.
|
|
||||||
This opens an opportunity: we can store the user's login credentials in the
|
|
||||||
browser without the need to store their sensitive plaintext password, or the
|
|
||||||
need to set up complicated third party backends, like oauth.
|
|
||||||
|
|
||||||
Enabling this option will cause converse to save the SCRAM keys on successful
|
|
||||||
login into browser storage. This information can be recovered from the public
|
|
||||||
API method ``converse.savedLoginInfo()``, which returns on success a Promise
|
|
||||||
which resolves to an object whose ``attributes`` object contains the following
|
|
||||||
information:
|
|
||||||
|
|
||||||
::
|
|
||||||
{ 'id': 'converse.savedLoginInfo',
|
|
||||||
'users': Usermap Object
|
|
||||||
}
|
|
||||||
|
|
||||||
Where the ``Usermap`` Object has keys corresponding to users and values
|
|
||||||
which are valid login credentials (which can be passed in as the
|
|
||||||
``password`` field on login), like so:
|
|
||||||
|
|
||||||
::
|
|
||||||
{ 'user1@xmpp.org': Credentials,
|
|
||||||
'user2@opkode.com': Credentials,
|
|
||||||
...
|
|
||||||
}
|
|
||||||
|
|
||||||
From here, one may configure their client to simply choose one of the logins,
|
|
||||||
depending on their needs, and pass the username and credentials into the
|
|
||||||
settings.
|
|
||||||
Note well that this method will only work once converse has been loaded.
|
|
||||||
If you need the utilities provided here before login, call
|
|
||||||
`window.converse.load()`.
|
|
||||||
|
|
||||||
|
|
||||||
auto_away
|
auto_away
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
@ -1861,6 +1819,27 @@ 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.
|
the ``show_images_inline``, ``embed_audio`` and ``embed_videos`` settings.
|
||||||
|
|
||||||
|
|
||||||
|
reuse_scram_keys
|
||||||
|
----------------
|
||||||
|
|
||||||
|
* Default: ``false``
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Assuming the server does not alter the user's password or the
|
||||||
|
storage parameters, we can authenticate with the same SCRAM 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
|
||||||
|
login, and next time Converse is loaded the user will be automatically logged in
|
||||||
|
with those SCRAM keys.
|
||||||
|
|
||||||
|
|
||||||
.. _`roomconfig_whitelist`:
|
.. _`roomconfig_whitelist`:
|
||||||
|
|
||||||
roomconfig_whitelist
|
roomconfig_whitelist
|
||||||
|
|
1
package-lock.json
generated
1
package-lock.json
generated
|
@ -18098,6 +18098,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"src/headless": {
|
"src/headless": {
|
||||||
|
"name": "@converse/headless",
|
||||||
"version": "10.0.0",
|
"version": "10.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
|
|
|
@ -14,7 +14,6 @@ import log from '@converse/headless/log.js';
|
||||||
import pluggable from 'pluggable.js/src/pluggable.js';
|
import pluggable from 'pluggable.js/src/pluggable.js';
|
||||||
import sizzle from 'sizzle';
|
import sizzle from 'sizzle';
|
||||||
import u, { setUnloadEvent, replacePromise } from '@converse/headless/utils/core.js';
|
import u, { setUnloadEvent, replacePromise } from '@converse/headless/utils/core.js';
|
||||||
import { initStorage } from './utils/storage.js';
|
|
||||||
import { CHAT_STATES, KEYCODES } from './shared/constants.js';
|
import { CHAT_STATES, KEYCODES } from './shared/constants.js';
|
||||||
import { Collection } from "@converse/skeletor/src/collection";
|
import { Collection } from "@converse/skeletor/src/collection";
|
||||||
import { Connection, MockConnection } from '@converse/headless/shared/connection/index.js';
|
import { Connection, MockConnection } from '@converse/headless/shared/connection/index.js';
|
||||||
|
@ -38,7 +37,6 @@ import {
|
||||||
initClientConfig,
|
initClientConfig,
|
||||||
initPlugins,
|
initPlugins,
|
||||||
initSessionStorage,
|
initSessionStorage,
|
||||||
initScramStorage,
|
|
||||||
registerGlobalEventHandlers,
|
registerGlobalEventHandlers,
|
||||||
setUserJID,
|
setUserJID,
|
||||||
} from './utils/init.js';
|
} from './utils/init.js';
|
||||||
|
@ -473,23 +471,7 @@ export const api = _converse.api = {
|
||||||
}
|
}
|
||||||
api.trigger('send', stanza);
|
api.trigger('send', stanza);
|
||||||
return promise;
|
return promise;
|
||||||
},
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch previously used login information, username and SCRAM keys if available
|
|
||||||
* @method _converse.api.savedLoginInfo
|
|
||||||
* @returns {Promise} A promise which resolves (or potentially rejects) once we
|
|
||||||
* fetch the previously used login keys.
|
|
||||||
*/
|
|
||||||
async savedLoginInfo () {
|
|
||||||
const id = "converse.savedLoginInfo";
|
|
||||||
const login_info = new Model({id});
|
|
||||||
initStorage(login_info, id, 'scramStorage');
|
|
||||||
await new Promise(f => login_info.fetch({'success': f, 'error': f}));
|
|
||||||
|
|
||||||
return login_info;
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
@ -693,38 +675,6 @@ Object.assign(converse, {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch previously used login information, username and SCRAM keys if available
|
|
||||||
* @method _converse.api.getSavedLoginInfo
|
|
||||||
* @returns {Promise} A promise which resolves (or potentially rejects) once
|
|
||||||
* we fetch the previously used login keys. The object returned on success
|
|
||||||
* has an attributes object of the following form:
|
|
||||||
* { 'id': 'converse.savedLoginInfo',
|
|
||||||
* 'users': Usermap Object
|
|
||||||
* }
|
|
||||||
* Where the Usermap Object has keys corresponding to users and values
|
|
||||||
* which are valid login credentials (which can be passed in as the
|
|
||||||
* password field on login), like so:
|
|
||||||
* { 'user1@xmpp.org': Credentials,
|
|
||||||
* 'user2@opkode.com': Credentials,
|
|
||||||
* ...
|
|
||||||
* }
|
|
||||||
* It should be noted that these Credentials will *NEVER* store the user's
|
|
||||||
* plaintext password, nor any material from which the user's plaintext
|
|
||||||
* password could be recovered. It uses SASL SCRAM internally, which
|
|
||||||
* secures the user's login information and ensures* the authenticating
|
|
||||||
* server is the server which was supplied the credentials initially.
|
|
||||||
*
|
|
||||||
* *With some caveats, we don't yet actively protect against active MITM
|
|
||||||
* attacks.
|
|
||||||
*/
|
|
||||||
savedLoginInfo: async () => {
|
|
||||||
if (!_converse.storage) {
|
|
||||||
await initScramStorage(_converse);
|
|
||||||
}
|
|
||||||
return _converse.api.savedLoginInfo()
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exposes methods for adding and removing plugins. You'll need to write a plugin
|
* Exposes methods for adding and removing plugins. You'll need to write a plugin
|
||||||
* if you want to have access to the private API methods defined further down below.
|
* if you want to have access to the private API methods defined further down below.
|
||||||
|
|
|
@ -181,6 +181,12 @@ export class Connection extends Strophe.Connection {
|
||||||
this.flush(); // Solves problem of returned PubSub BOSH response not received by browser
|
this.flush(); // Solves problem of returned PubSub BOSH response not received by browser
|
||||||
await setUserJID(this.jid);
|
await setUserJID(this.jid);
|
||||||
|
|
||||||
|
// Save the current JID in persistent storage so that we can attempt to
|
||||||
|
// recreate the session from SCRAM keys
|
||||||
|
if (_converse.config.get('trusted')) {
|
||||||
|
localStorage.setItem('conversejs-session-jid', _converse.bare_jid);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Synchronous event triggered after we've sent an IQ to bind the
|
* Synchronous event triggered after we've sent an IQ to bind the
|
||||||
* user's JID resource for this session.
|
* user's JID resource for this session.
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
* @property { String } [assets_path='/dist']
|
* @property { String } [assets_path='/dist']
|
||||||
* @property { ('login'|'prebind'|'anonymous'|'external') } [authentication='login']
|
* @property { ('login'|'prebind'|'anonymous'|'external') } [authentication='login']
|
||||||
* @property { Boolean } [auto_login=false] - Currently only used in connection with anonymous login
|
* @property { Boolean } [auto_login=false] - Currently only used in connection with anonymous login
|
||||||
* @property { Boolean } [save_scram_keys=false] - Save SCRAM keys after login to allow for future auto login
|
* @property { Boolean } [reuse_scram_keys=false] - Save SCRAM keys after login to allow for future auto login
|
||||||
* @property { Boolean } [auto_reconnect=true]
|
* @property { Boolean } [auto_reconnect=true]
|
||||||
* @property { Array<String>} [blacklisted_plugins]
|
* @property { Array<String>} [blacklisted_plugins]
|
||||||
* @property { Boolean } [clear_cache_on_logout=false]
|
* @property { Boolean } [clear_cache_on_logout=false]
|
||||||
|
@ -38,7 +38,7 @@ export const DEFAULT_SETTINGS = {
|
||||||
assets_path: '/dist',
|
assets_path: '/dist',
|
||||||
authentication: 'login', // Available values are "login", "prebind", "anonymous" and "external".
|
authentication: 'login', // Available values are "login", "prebind", "anonymous" and "external".
|
||||||
auto_login: false, // Currently only used in connection with anonymous login
|
auto_login: false, // Currently only used in connection with anonymous login
|
||||||
save_scram_keys: false,
|
reuse_scram_keys: false,
|
||||||
auto_reconnect: true,
|
auto_reconnect: true,
|
||||||
blacklisted_plugins: [],
|
blacklisted_plugins: [],
|
||||||
clear_cache_on_logout: false,
|
clear_cache_on_logout: false,
|
||||||
|
|
|
@ -14,7 +14,6 @@ import sizzle from "sizzle";
|
||||||
import { Model } from '@converse/skeletor/src/model.js';
|
import { Model } from '@converse/skeletor/src/model.js';
|
||||||
import { Strophe } from 'strophe.js/src/strophe.js';
|
import { Strophe } from 'strophe.js/src/strophe.js';
|
||||||
import { getOpenPromise } from '@converse/openpromise';
|
import { getOpenPromise } from '@converse/openpromise';
|
||||||
import { setUserJID, } from '@converse/headless/utils/init.js';
|
|
||||||
import { settings_api } from '@converse/headless/shared/settings/api.js';
|
import { settings_api } from '@converse/headless/shared/settings/api.js';
|
||||||
import { stx , toStanza } from './stanza.js';
|
import { stx , toStanza } from './stanza.js';
|
||||||
|
|
||||||
|
@ -125,12 +124,12 @@ u.getLongestSubstring = function (string, candidates) {
|
||||||
return candidates.reduce(reducer, '');
|
return candidates.reduce(reducer, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
u.isValidJID = function (jid) {
|
export function isValidJID (jid) {
|
||||||
if (typeof jid === 'string') {
|
if (typeof jid === 'string') {
|
||||||
return compact(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
|
return compact(jid.split('@')).length === 2 && !jid.startsWith('@') && !jid.endsWith('@');
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
}
|
||||||
|
|
||||||
u.isValidMUCJID = function (jid) {
|
u.isValidMUCJID = function (jid) {
|
||||||
return !jid.startsWith('@') && !jid.endsWith('@');
|
return !jid.startsWith('@') && !jid.endsWith('@');
|
||||||
|
@ -553,18 +552,6 @@ export function setUnloadEvent () {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLoginCredentialsFromBrowser () {
|
|
||||||
try {
|
|
||||||
const creds = await navigator.credentials.get({'password': true});
|
|
||||||
if (creds && creds.type == 'password' && u.isValidJID(creds.id)) {
|
|
||||||
await setUserJID(creds.id);
|
|
||||||
return {'jid': creds.id, 'password': creds.password};
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
log.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function replacePromise (name) {
|
export function replacePromise (name) {
|
||||||
const existing_promise = _converse.promises[name];
|
const existing_promise = _converse.promises[name];
|
||||||
if (!existing_promise) {
|
if (!existing_promise) {
|
||||||
|
@ -591,9 +578,10 @@ export function decodeHTMLEntities (str) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Object.assign({
|
export default Object.assign({
|
||||||
prefixMentions,
|
|
||||||
isEmptyMessage,
|
|
||||||
getUniqueId,
|
getUniqueId,
|
||||||
toStanza,
|
isEmptyMessage,
|
||||||
|
isValidJID,
|
||||||
|
prefixMentions,
|
||||||
stx,
|
stx,
|
||||||
|
toStanza,
|
||||||
}, u);
|
}, u);
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { Connection } from '@converse/headless/shared/connection/index.js';
|
||||||
import { Model } from '@converse/skeletor/src/model.js';
|
import { Model } from '@converse/skeletor/src/model.js';
|
||||||
import { Strophe } from 'strophe.js/src/strophe';
|
import { Strophe } from 'strophe.js/src/strophe';
|
||||||
import { createStore, initStorage } from '@converse/headless/utils/storage.js';
|
import { createStore, initStorage } from '@converse/headless/utils/storage.js';
|
||||||
import { getLoginCredentialsFromBrowser } from '@converse/headless/utils/core.js';
|
import { isValidJID } from './core.js';
|
||||||
|
|
||||||
|
|
||||||
export function initPlugins (_converse) {
|
export function initPlugins (_converse) {
|
||||||
|
@ -87,17 +87,6 @@ export async function initSessionStorage (_converse) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function initScramStorage (_converse) {
|
|
||||||
_converse.storage = {
|
|
||||||
..._converse.storage,
|
|
||||||
'scramStorage': Storage.localForage.createInstance({
|
|
||||||
'name': 'converse-scram',
|
|
||||||
'description': 'SCRAM storage driver',
|
|
||||||
'driver': Storage.localForage.INDEXEDDB
|
|
||||||
})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function initPersistentStorage (_converse, store_name) {
|
function initPersistentStorage (_converse, store_name) {
|
||||||
if (_converse.api.settings.get('persistent_store') === 'sessionStorage') {
|
if (_converse.api.settings.get('persistent_store') === 'sessionStorage') {
|
||||||
return;
|
return;
|
||||||
|
@ -130,6 +119,7 @@ function initPersistentStorage (_converse, store_name) {
|
||||||
_converse.storage['persistent'] = Storage.localForage.createInstance(config);
|
_converse.storage['persistent'] = Storage.localForage.createInstance(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function saveJIDtoSession (_converse, jid) {
|
function saveJIDtoSession (_converse, jid) {
|
||||||
jid = _converse.session.get('jid') || jid;
|
jid = _converse.session.get('jid') || jid;
|
||||||
if (_converse.api.settings.get("authentication") !== _converse.ANONYMOUS && !Strophe.getResourceFromJid(jid)) {
|
if (_converse.api.settings.get("authentication") !== _converse.ANONYMOUS && !Strophe.getResourceFromJid(jid)) {
|
||||||
|
@ -169,6 +159,7 @@ function saveJIDtoSession (_converse, jid) {
|
||||||
*/
|
*/
|
||||||
export async function setUserJID (jid) {
|
export async function setUserJID (jid) {
|
||||||
await initSession(_converse, jid);
|
await initSession(_converse, jid);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggered whenever the user's JID has been updated
|
* Triggered whenever the user's JID has been updated
|
||||||
* @event _converse#setUserJID
|
* @event _converse#setUserJID
|
||||||
|
@ -177,6 +168,7 @@ export async function setUserJID (jid) {
|
||||||
return jid;
|
return jid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function initSession (_converse, jid) {
|
export async function initSession (_converse, jid) {
|
||||||
const is_shared_session = _converse.api.settings.get('connection_options').worker;
|
const is_shared_session = _converse.api.settings.get('connection_options').worker;
|
||||||
|
|
||||||
|
@ -212,6 +204,7 @@ export async function initSession (_converse, jid) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function registerGlobalEventHandlers (_converse) {
|
export function registerGlobalEventHandlers (_converse) {
|
||||||
document.addEventListener("visibilitychange", _converse.saveWindowState);
|
document.addEventListener("visibilitychange", _converse.saveWindowState);
|
||||||
_converse.saveWindowState({'type': document.hidden ? "blur" : "focus"}); // Set initial state
|
_converse.saveWindowState({'type': document.hidden ? "blur" : "focus"}); // Set initial state
|
||||||
|
@ -233,6 +226,7 @@ function unregisterGlobalEventHandlers (_converse) {
|
||||||
api.trigger('unregisteredGlobalEventHandlers');
|
api.trigger('unregisteredGlobalEventHandlers');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Make sure everything is reset in case this is a subsequent call to
|
// Make sure everything is reset in case this is a subsequent call to
|
||||||
// converse.initialize (happens during tests).
|
// converse.initialize (happens during tests).
|
||||||
export async function cleanup (_converse) {
|
export async function cleanup (_converse) {
|
||||||
|
@ -248,23 +242,6 @@ export async function cleanup (_converse) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLoginCredentials () {
|
|
||||||
let credentials;
|
|
||||||
let wait = 0;
|
|
||||||
while (!credentials) {
|
|
||||||
try {
|
|
||||||
credentials = await fetchLoginCredentials(wait); // eslint-disable-line no-await-in-loop
|
|
||||||
} catch (e) {
|
|
||||||
log.error('Could not fetch login credentials');
|
|
||||||
log.error(e);
|
|
||||||
}
|
|
||||||
// If unsuccessful, we wait 2 seconds between subsequent attempts to
|
|
||||||
// fetch the credentials.
|
|
||||||
wait = 2000;
|
|
||||||
}
|
|
||||||
return credentials;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function fetchLoginCredentials (wait=0) {
|
function fetchLoginCredentials (wait=0) {
|
||||||
return new Promise(
|
return new Promise(
|
||||||
|
@ -296,6 +273,50 @@ function fetchLoginCredentials (wait=0) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getLoginCredentialsFromURL () {
|
||||||
|
let credentials;
|
||||||
|
let wait = 0;
|
||||||
|
while (!credentials) {
|
||||||
|
try {
|
||||||
|
credentials = await fetchLoginCredentials(wait); // eslint-disable-line no-await-in-loop
|
||||||
|
} catch (e) {
|
||||||
|
log.error('Could not fetch login credentials');
|
||||||
|
log.error(e);
|
||||||
|
}
|
||||||
|
// If unsuccessful, we wait 2 seconds between subsequent attempts to
|
||||||
|
// fetch the credentials.
|
||||||
|
wait = 2000;
|
||||||
|
}
|
||||||
|
return credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getLoginCredentialsFromBrowser () {
|
||||||
|
try {
|
||||||
|
const creds = await navigator.credentials.get({'password': true});
|
||||||
|
if (creds && creds.type == 'password' && isValidJID(creds.id)) {
|
||||||
|
await setUserJID(creds.id);
|
||||||
|
return {'jid': creds.id, 'password': creds.password};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
log.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function getLoginCredentialsFromSCRAMKeys () {
|
||||||
|
const jid = localStorage.getItem('conversejs-session-jid');
|
||||||
|
if (!jid) return null;
|
||||||
|
|
||||||
|
await setUserJID(jid);
|
||||||
|
|
||||||
|
const login_info = await savedLoginInfo(jid);
|
||||||
|
const scram_keys = login_info.get('scram_keys');
|
||||||
|
return scram_keys ? { jid , 'password': scram_keys } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function attemptNonPreboundSession (credentials, automatic) {
|
export async function attemptNonPreboundSession (credentials, automatic) {
|
||||||
const { api } = _converse;
|
const { api } = _converse;
|
||||||
if (api.settings.get("authentication") === _converse.LOGIN) {
|
if (api.settings.get("authentication") === _converse.LOGIN) {
|
||||||
|
@ -306,18 +327,24 @@ export async function attemptNonPreboundSession (credentials, automatic) {
|
||||||
// automatically setting up a new session (``auto_login``).
|
// automatically setting up a new session (``auto_login``).
|
||||||
// So we can't do the check (!automatic || _converse.api.settings.get("auto_login")) here.
|
// So we can't do the check (!automatic || _converse.api.settings.get("auto_login")) here.
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
connect(credentials);
|
return connect(credentials);
|
||||||
} else if (api.settings.get("credentials_url")) {
|
} else if (api.settings.get("credentials_url")) {
|
||||||
// We give credentials_url preference, because
|
// We give credentials_url preference, because
|
||||||
// _converse.connection.pass might be an expired token.
|
// _converse.connection.pass might be an expired token.
|
||||||
connect(await getLoginCredentials());
|
return connect(await getLoginCredentialsFromURL());
|
||||||
} else if (_converse.jid && (api.settings.get("password") || _converse.connection.pass)) {
|
} else if (_converse.jid && (api.settings.get("password") || _converse.connection.pass)) {
|
||||||
connect();
|
return connect();
|
||||||
} else if (!_converse.isTestEnv() && 'credentials' in navigator) {
|
|
||||||
connect(await getLoginCredentialsFromBrowser());
|
|
||||||
} else {
|
|
||||||
!_converse.isTestEnv() && log.warn("attemptNonPreboundSession: Couldn't find credentials to log in with");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (api.settings.get('reuse_scram_keys')) {
|
||||||
|
const credentials = await getLoginCredentialsFromSCRAMKeys();
|
||||||
|
if (credentials) return connect(credentials);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_converse.isTestEnv() && 'credentials' in navigator) {
|
||||||
|
return connect(await getLoginCredentialsFromBrowser());
|
||||||
|
}
|
||||||
|
!_converse.isTestEnv() && log.warn("attemptNonPreboundSession: Couldn't find credentials to log in with");
|
||||||
} else if (
|
} else if (
|
||||||
[_converse.ANONYMOUS, _converse.EXTERNAL].includes(api.settings.get("authentication")) &&
|
[_converse.ANONYMOUS, _converse.EXTERNAL].includes(api.settings.get("authentication")) &&
|
||||||
(!automatic || api.settings.get("auto_login"))
|
(!automatic || api.settings.get("auto_login"))
|
||||||
|
@ -338,7 +365,26 @@ export function getConnectionServiceURL () {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function connect (credentials) {
|
/**
|
||||||
|
* Fetch the stored SCRAM keys for the given JID, if available.
|
||||||
|
*
|
||||||
|
* The user's plaintext password is not stored, nor any material from which
|
||||||
|
* the user's plaintext password could be recovered.
|
||||||
|
*
|
||||||
|
* @param { String } JID - The XMPP address for which to fetch the SCRAM keys
|
||||||
|
* @returns { Promise } A promise which resolves once we've fetched the previously
|
||||||
|
* used login keys.
|
||||||
|
*/
|
||||||
|
export async function savedLoginInfo (jid) {
|
||||||
|
const id = `converse.scram-keys-${Strophe.getBareJidFromJid(jid)}`;
|
||||||
|
const login_info = new Model({ id });
|
||||||
|
initStorage(login_info, id, 'persistent');
|
||||||
|
await new Promise(f => login_info.fetch({'success': f, 'error': f}));
|
||||||
|
return login_info;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function connect (credentials) {
|
||||||
const { api } = _converse;
|
const { api } = _converse;
|
||||||
if ([_converse.ANONYMOUS, _converse.EXTERNAL].includes(api.settings.get("authentication"))) {
|
if ([_converse.ANONYMOUS, _converse.EXTERNAL].includes(api.settings.get("authentication"))) {
|
||||||
if (!_converse.jid) {
|
if (!_converse.jid) {
|
||||||
|
@ -369,26 +415,19 @@ function connect (credentials) {
|
||||||
|
|
||||||
let callback;
|
let callback;
|
||||||
|
|
||||||
if (api.settings.get("save_scram_keys") && !password.ck) {
|
// Save the SCRAM data if we're not already logged in with SCRAM
|
||||||
// Don't save the SCRAM data if we already logged in with SCRAM
|
if (
|
||||||
const login_info = await _converse.api.savedLoginInfo();
|
_converse.config.get('trusted') &&
|
||||||
|
_converse.jid &&
|
||||||
|
api.settings.get("reuse_scram_keys") &&
|
||||||
|
!password?.ck
|
||||||
|
) {
|
||||||
|
// Store scram keys in scram storage
|
||||||
|
const login_info = await savedLoginInfo(_converse.jid);
|
||||||
|
|
||||||
callback = async (status) => {
|
callback = (status) => {
|
||||||
// Store scram keys in scram storage
|
const { scram_keys } = _converse.connection;
|
||||||
if (!_converse?.storage?.scramStorage) {
|
if (scram_keys) login_info.save({ scram_keys });
|
||||||
await initScramStorage(_converse);
|
|
||||||
}
|
|
||||||
|
|
||||||
const newScramKeys = _converse.connection.scramKeys;
|
|
||||||
if (newScramKeys) {
|
|
||||||
try {
|
|
||||||
const new_users_info = login_info.users ?? { };
|
|
||||||
new_users_info[_converse.connection.authzid] = newScramKeys;
|
|
||||||
login_info.save({'users': new_users_info });
|
|
||||||
} catch (e) { // Could not find local storage }
|
|
||||||
log.error("No storage method found: ", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_converse.connection.onConnectStatusChanged(status);
|
_converse.connection.onConnectStatusChanged(status);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user