소스 검색

Added option to save SCRAM keys

based-a-tron 2 년 전
부모
커밋
8ab0b718de
4개의 변경된 파일133개의 추가작업 그리고 2개의 파일을 삭제
  1. 41 0
      docs/source/configuration.rst
  2. 51 1
      src/headless/core.js
  3. 2 0
      src/headless/shared/settings/constants.js
  4. 39 1
      src/headless/utils/init.js

+ 41 - 0
docs/source/configuration.rst

@@ -407,6 +407,47 @@ in to their XMPP account.
   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.
 
+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
 ---------

+ 51 - 1
src/headless/core.js

@@ -14,6 +14,7 @@ import log from '@converse/headless/log.js';
 import pluggable from 'pluggable.js/src/pluggable.js';
 import sizzle from 'sizzle';
 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 { Collection } from "@converse/skeletor/src/collection";
 import { Connection, MockConnection } from '@converse/headless/shared/connection/index.js';
@@ -37,6 +38,7 @@ import {
     initClientConfig,
     initPlugins,
     initSessionStorage,
+    initScramStorage,
     registerGlobalEventHandlers,
     setUserJID,
 } from './utils/init.js';
@@ -471,7 +473,23 @@ export const api = _converse.api = {
         }
         api.trigger('send', stanza);
         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;
+    },
+
 };
 
 
@@ -675,6 +693,38 @@ 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
      * if you want to have access to the private API methods defined further down below.

+ 2 - 0
src/headless/shared/settings/constants.js

@@ -6,6 +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 } [save_scram_keys=false] - Save SCRAM keys after login to allow for future auto login
  * @property { Boolean } [auto_reconnect=true]
  * @property { Array<String>} [blacklisted_plugins]
  * @property { Boolean } [clear_cache_on_logout=false]
@@ -37,6 +38,7 @@ export const DEFAULT_SETTINGS = {
     assets_path: '/dist',
     authentication: 'login', // Available values are "login", "prebind", "anonymous" and "external".
     auto_login: false, // Currently only used in connection with anonymous login
+    save_scram_keys: false,
     auto_reconnect: true,
     blacklisted_plugins: [],
     clear_cache_on_logout: false,

+ 39 - 1
src/headless/utils/init.js

@@ -87,6 +87,17 @@ 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) {
     if (_converse.api.settings.get('persistent_store') === 'sessionStorage') {
         return;
@@ -355,6 +366,33 @@ function connect (credentials) {
             _converse.connection.reset();
             _converse.connection.service = getConnectionServiceURL();
         }
-        _converse.connection.connect(_converse.jid, password);
+
+        let callback;
+
+        if (api.settings.get("save_scram_keys") && !password.ck) {
+            // Don't save the SCRAM data if we already logged in with SCRAM
+            const login_info = await _converse.api.savedLoginInfo();
+
+            callback = async (status) => {
+                // Store scram keys in scram storage
+                if (!_converse?.storage?.scramStorage) {
+                    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.connect(_converse.jid, password, callback);
     }
 }