1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-28 05:03:59 +03:00

Merge pull request #587 from matrix-org/dbkr/inbound_sessions_to_cryptostore

Migrate inbound sessions to cryptostore
This commit is contained in:
David Baker
2018-01-09 10:41:38 +00:00
committed by GitHub
9 changed files with 522 additions and 260 deletions

View File

@@ -10,6 +10,7 @@ import Promise from 'bluebird';
import sdk from '../../../..'; import sdk from '../../../..';
import algorithms from '../../../../lib/crypto/algorithms'; import algorithms from '../../../../lib/crypto/algorithms';
import WebStorageSessionStore from '../../../../lib/store/session/webstorage'; import WebStorageSessionStore from '../../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../../MockStorageApi'; import MockStorageApi from '../../../MockStorageApi';
import testUtils from '../../../test-utils'; import testUtils from '../../../test-utils';
@@ -45,8 +46,9 @@ describe("MegolmDecryption", function() {
const mockStorage = new MockStorageApi(); const mockStorage = new MockStorageApi();
const sessionStore = new WebStorageSessionStore(mockStorage); const sessionStore = new WebStorageSessionStore(mockStorage);
const cryptoStore = new MemoryCryptoStore(mockStorage);
const olmDevice = new OlmDevice(sessionStore); const olmDevice = new OlmDevice(sessionStore, cryptoStore);
megolmDecryption = new MegolmDecryption({ megolmDecryption = new MegolmDecryption({
userId: '@user:id', userId: '@user:id',

View File

@@ -216,6 +216,41 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
this._sessionStore.removeAllEndToEndSessions(); this._sessionStore.removeAllEndToEndSessions();
} }
// inbound group sessions
const ibGroupSessions = this._sessionStore.getAllEndToEndInboundGroupSessionKeys();
if (Object.keys(ibGroupSessions).length > 0) {
let numIbSessions = 0;
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
// We always migrate inbound group sessions, even if we already have some
// in the new store. They should be be safe to migrate.
for (const s of ibGroupSessions) {
try {
this._cryptoStore.addEndToEndInboundGroupSession(
s.senderKey, s.sessionId,
JSON.parse(
this._sessionStore.getEndToEndInboundGroupSession(
s.senderKey, s.sessionId,
),
), txn,
);
} catch (e) {
console.warn(
"Failed to migrate session " + s.senderKey + "/" +
s.sessionId + ": " + e.stack || e,
);
}
++numIbSessions;
}
console.log(
"Migrated " + numIbSessions +
" inbound group sessions from session store",
);
},
);
this._sessionStore.removeAllEndToEndInboundGroupSessions();
}
}; };
/** /**
@@ -773,68 +808,62 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
*/ */
/** /**
* store an InboundGroupSession in the session store * Unpickle a session from a sessionData object and invoke the given function.
* The session is valid only until func returns.
* *
* @param {string} senderCurve25519Key * @param {Object} sessionData Object describing the session.
* @param {string} sessionId * @param {function(Olm.InboundGroupSession)} func Invoked with the unpickled session
* @param {InboundGroupSessionData} sessionData * @return {*} result of func
* @private
*/ */
OlmDevice.prototype._saveInboundGroupSession = function( OlmDevice.prototype._unpickleInboundGroupSession = function(sessionData, func) {
senderCurve25519Key, sessionId, sessionData,
) {
this._sessionStore.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, JSON.stringify(sessionData),
);
};
/**
* extract an InboundGroupSession from the session store and call the given function
*
* @param {string} roomId
* @param {string} senderKey
* @param {string} sessionId
* @param {function(Olm.InboundGroupSession, InboundGroupSessionData): T} func
* function to call.
*
* @return {null} the sessionId is unknown
*
* @return {T} result of func
*
* @private
* @template {T}
*/
OlmDevice.prototype._getInboundGroupSession = function(
roomId, senderKey, sessionId, func,
) {
let r = this._sessionStore.getEndToEndInboundGroupSession(
senderKey, sessionId,
);
if (r === null) {
return null;
}
r = JSON.parse(r);
// check that the room id matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
if (roomId !== r.room_id) {
throw new Error(
"Mismatched room_id for inbound group session (expected " + r.room_id +
", was " + roomId + ")",
);
}
const session = new Olm.InboundGroupSession(); const session = new Olm.InboundGroupSession();
try { try {
session.unpickle(this._pickleKey, r.session); session.unpickle(this._pickleKey, sessionData.session);
return func(session, r); return func(session);
} finally { } finally {
session.free(); session.free();
} }
}; };
/**
* extract an InboundGroupSession from the crypto store and call the given function
*
* @param {string} roomId The room ID to extract the session for, or null to fetch
* sessions for any room.
* @param {string} senderKey
* @param {string} sessionId
* @param {*} txn Opaque transaction object from cryptoStore.doTxn()
* @param {function(Olm.InboundGroupSession, InboundGroupSessionData)} func
* function to call.
*
* @private
*/
OlmDevice.prototype._getInboundGroupSession = function(
roomId, senderKey, sessionId, txn, func,
) {
this._cryptoStore.getEndToEndInboundGroupSession(
senderKey, sessionId, txn, (sessionData) => {
if (sessionData === null) {
func(null);
return;
}
// if we were given a room ID, check that the it matches the original one for the session. This stops
// the HS pretending a message was targeting a different room.
if (roomId !== null && roomId !== sessionData.room_id) {
throw new Error(
"Mismatched room_id for inbound group session (expected " +
sessionData.room_id + ", was " + roomId + ")",
);
}
this._unpickleInboundGroupSession(sessionData, (session) => {
func(session, sessionData);
});
},
);
};
/** /**
* Add an inbound group session to the session store * Add an inbound group session to the session store
* *
@@ -853,100 +882,52 @@ OlmDevice.prototype.addInboundGroupSession = async function(
sessionId, sessionKey, keysClaimed, sessionId, sessionKey, keysClaimed,
exportFormat, exportFormat,
) { ) {
const self = this; await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
/* if we already have this session, consider updating it */
this._getInboundGroupSession(
roomId, senderKey, sessionId, txn,
(existingSession, existingSessionData) => {
if (existingSession) {
console.log(
"Update for megolm session " + senderKey + "/" + sessionId,
);
// for now we just ignore updates. TODO: implement something here
return;
}
/* if we already have this session, consider updating it */ // new session.
function updateSession(session, sessionData) { const session = new Olm.InboundGroupSession();
console.log("Update for megolm session " + senderKey + "/" + sessionId); try {
// for now we just ignore updates. TODO: implement something here if (exportFormat) {
session.import_session(sessionKey);
} else {
session.create(sessionKey);
}
if (sessionId != session.session_id()) {
throw new Error(
"Mismatched group session ID from senderKey: " +
senderKey,
);
}
return true; const sessionData = {
} room_id: roomId,
session: session.pickle(this._pickleKey),
keysClaimed: keysClaimed,
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
};
const r = this._getInboundGroupSession( this._cryptoStore.addEndToEndInboundGroupSession(
roomId, senderKey, sessionId, updateSession, senderKey, sessionId, sessionData, txn,
); );
} finally {
if (r !== null) { session.free();
return; }
} },
// new session.
const session = new Olm.InboundGroupSession();
try {
if (exportFormat) {
session.import_session(sessionKey);
} else {
session.create(sessionKey);
}
if (sessionId != session.session_id()) {
throw new Error(
"Mismatched group session ID from senderKey: " + senderKey,
); );
} },
const sessionData = {
room_id: roomId,
session: session.pickle(this._pickleKey),
keysClaimed: keysClaimed,
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
};
self._saveInboundGroupSession(
senderKey, sessionId, sessionData,
);
} finally {
session.free();
}
};
/**
* Add a previously-exported inbound group session to the session store
*
* @param {module:crypto/OlmDevice.MegolmSessionData} data session data
*/
OlmDevice.prototype.importInboundGroupSession = async function(data) {
/* if we already have this session, consider updating it */
function updateSession(session, sessionData) {
console.log("Update for megolm session " + data.sender_key + "|" +
data.session_id);
// for now we just ignore updates. TODO: implement something here
return true;
}
const r = this._getInboundGroupSession(
data.room_id, data.sender_key, data.session_id, updateSession,
); );
if (r !== null) {
return;
}
// new session.
const session = new Olm.InboundGroupSession();
try {
session.import_session(data.session_key);
if (data.session_id != session.session_id()) {
throw new Error(
"Mismatched group session ID from senderKey: " + data.sender_key,
);
}
const sessionData = {
room_id: data.room_id,
session: session.pickle(this._pickleKey),
keysClaimed: data.sender_claimed_keys,
forwardingCurve25519KeyChain: data.forwarding_curve25519_key_chain,
};
this._saveInboundGroupSession(
data.sender_key, data.session_id, sessionData,
);
} finally {
session.free();
}
}; };
/** /**
@@ -968,51 +949,68 @@ OlmDevice.prototype.importInboundGroupSession = async function(data) {
OlmDevice.prototype.decryptGroupMessage = async function( OlmDevice.prototype.decryptGroupMessage = async function(
roomId, senderKey, sessionId, body, eventId, timestamp, roomId, senderKey, sessionId, body, eventId, timestamp,
) { ) {
const self = this; let result;
function decrypt(session, sessionData) { await this._cryptoStore.doTxn(
const res = session.decrypt(body); 'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
this._getInboundGroupSession(
roomId, senderKey, sessionId, txn, (session, sessionData) => {
if (session === null) {
result = null;
return;
}
const res = session.decrypt(body);
let plaintext = res.plaintext; let plaintext = res.plaintext;
if (plaintext === undefined) { if (plaintext === undefined) {
// Compatibility for older olm versions. // Compatibility for older olm versions.
plaintext = res; plaintext = res;
} else { } else {
// Check if we have seen this message index before to detect replay attacks. // Check if we have seen this message index before to detect replay attacks.
// If the event ID and timestamp are specified, and the match the event ID // If the event ID and timestamp are specified, and the match the event ID
// and timestamp from the last time we used this message index, then we // and timestamp from the last time we used this message index, then we
// don't consider it a replay attack. // don't consider it a replay attack.
const messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index; const messageIndexKey = (
if (messageIndexKey in self._inboundGroupSessionMessageIndexes) { senderKey + "|" + sessionId + "|" + res.message_index
const msgInfo = self._inboundGroupSessionMessageIndexes[messageIndexKey]; );
if (msgInfo.id !== eventId || msgInfo.timestamp !== timestamp) { if (messageIndexKey in this._inboundGroupSessionMessageIndexes) {
throw new Error( const msgInfo = (
"Duplicate message index, possible replay attack: " + this._inboundGroupSessionMessageIndexes[messageIndexKey]
messageIndexKey, );
if (
msgInfo.id !== eventId ||
msgInfo.timestamp !== timestamp
) {
throw new Error(
"Duplicate message index, possible replay attack: " +
messageIndexKey,
);
}
}
this._inboundGroupSessionMessageIndexes[messageIndexKey] = {
id: eventId,
timestamp: timestamp,
};
}
sessionData.session = session.pickle(this._pickleKey);
this._cryptoStore.storeEndToEndInboundGroupSession(
senderKey, sessionId, sessionData, txn,
); );
} result = {
} result: plaintext,
self._inboundGroupSessionMessageIndexes[messageIndexKey] = { keysClaimed: sessionData.keysClaimed || {},
id: eventId, senderKey: senderKey,
timestamp: timestamp, forwardingCurve25519KeyChain: (
}; sessionData.forwardingCurve25519KeyChain || []
} ),
};
sessionData.session = session.pickle(self._pickleKey); },
self._saveInboundGroupSession( );
senderKey, sessionId, sessionData, },
);
return {
result: plaintext,
keysClaimed: sessionData.keysClaimed || {},
senderKey: senderKey,
forwardingCurve25519KeyChain: sessionData.forwardingCurve25519KeyChain || [],
};
}
return this._getInboundGroupSession(
roomId, senderKey, sessionId, decrypt,
); );
return result;
}; };
/** /**
@@ -1025,25 +1023,33 @@ OlmDevice.prototype.decryptGroupMessage = async function(
* @returns {Promise<boolean>} true if we have the keys to this session * @returns {Promise<boolean>} true if we have the keys to this session
*/ */
OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, sessionId) { OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, sessionId) {
const s = this._sessionStore.getEndToEndInboundGroupSession( let result;
senderKey, sessionId, await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
this._cryptoStore.getEndToEndInboundGroupSession(
senderKey, sessionId, txn, (sessionData) => {
if (sessionData === null) {
result = false;
return;
}
if (roomId !== sessionData.room_id) {
console.warn(
`requested keys for inbound group session ${senderKey}|` +
`${sessionId}, with incorrect room_id ` +
`(expected ${sessionData.room_id}, ` +
`was ${roomId})`,
);
result = false;
} else {
result = true;
}
},
);
},
); );
if (s === null) { return result;
return false;
}
const r = JSON.parse(s);
if (roomId !== r.room_id) {
console.warn(
`requested keys for inbound group session ${senderKey}|` +
`${sessionId}, with incorrect room_id (expected ${r.room_id}, ` +
`was ${roomId})`,
);
return false;
}
return true;
}; };
/** /**
@@ -1063,24 +1069,33 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
OlmDevice.prototype.getInboundGroupSessionKey = async function( OlmDevice.prototype.getInboundGroupSessionKey = async function(
roomId, senderKey, sessionId, roomId, senderKey, sessionId,
) { ) {
function getKey(session, sessionData) { let result;
const messageIndex = session.first_known_index(); await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
this._getInboundGroupSession(
roomId, senderKey, sessionId, txn, (session, sessionData) => {
if (session === null) {
result = null;
return;
}
const messageIndex = session.first_known_index();
const claimedKeys = sessionData.keysClaimed || {}; const claimedKeys = sessionData.keysClaimed || {};
const senderEd25519Key = claimedKeys.ed25519 || null; const senderEd25519Key = claimedKeys.ed25519 || null;
return { result = {
"chain_index": messageIndex, "chain_index": messageIndex,
"key": session.export_session(messageIndex), "key": session.export_session(messageIndex),
"forwarding_curve25519_key_chain": "forwarding_curve25519_key_chain":
sessionData.forwardingCurve25519KeyChain || [], sessionData.forwardingCurve25519KeyChain || [],
"sender_claimed_ed25519_key": senderEd25519Key, "sender_claimed_ed25519_key": senderEd25519Key,
}; };
} },
);
return this._getInboundGroupSession( },
roomId, senderKey, sessionId, getKey,
); );
return result;
}; };
/** /**
@@ -1088,37 +1103,24 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
* *
* @param {string} senderKey base64-encoded curve25519 key of the sender * @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier * @param {string} sessionId session identifier
* @return {Promise<module:crypto/OlmDevice.MegolmSessionData>} exported session data * @param {string} sessionData The session object from the store
* @return {module:crypto/OlmDevice.MegolmSessionData} exported session data
*/ */
OlmDevice.prototype.exportInboundGroupSession = async function(senderKey, sessionId) { OlmDevice.prototype.exportInboundGroupSession = function(
const s = this._sessionStore.getEndToEndInboundGroupSession( senderKey, sessionId, sessionData,
senderKey, sessionId, ) {
); return this._unpickleInboundGroupSession(sessionData, (session) => {
if (s === null) {
throw new Error("Unknown inbound group session [" + senderKey + "," +
sessionId + "]");
}
const r = JSON.parse(s);
const session = new Olm.InboundGroupSession();
try {
session.unpickle(this._pickleKey, r.session);
const messageIndex = session.first_known_index(); const messageIndex = session.first_known_index();
return { return {
"sender_key": senderKey, "sender_key": senderKey,
"sender_claimed_keys": r.keysClaimed, "sender_claimed_keys": sessionData.keysClaimed,
"room_id": r.room_id, "room_id": sessionData.room_id,
"session_id": sessionId, "session_id": sessionId,
"session_key": session.export_session(messageIndex), "session_key": session.export_session(messageIndex),
"forwarding_curve25519_key_chain": "forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [],
session.forwardingCurve25519KeyChain || [],
}; };
} finally { });
session.free();
}
}; };
// Utilities // Utilities

View File

@@ -927,10 +927,18 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
* @param {module:crypto/OlmDevice.MegolmSessionData} session * @param {module:crypto/OlmDevice.MegolmSessionData} session
*/ */
MegolmDecryption.prototype.importRoomKey = function(session) { MegolmDecryption.prototype.importRoomKey = function(session) {
this._olmDevice.importInboundGroupSession(session); return this._olmDevice.addInboundGroupSession(
session.room_id,
// have another go at decrypting events sent with this session. session.sender_key,
this._retryDecryption(session.sender_key, session.session_id); session.forwarding_curve25519_key_chain,
session.session_id,
session.session_key,
session.sender_claimed_keys,
true,
).then(() => {
// have another go at decrypting events sent with this session.
this._retryDecryption(session.sender_key, session.session_id);
});
}; };
/** /**

View File

@@ -700,21 +700,25 @@ Crypto.prototype.isRoomEncrypted = function(roomId) {
/** /**
* Get a list containing all of the room keys * Get a list containing all of the room keys
* *
* @return {module:client.Promise} a promise which resolves to a list of * @return {module:crypto/OlmDevice.MegolmSessionData[]} a list of session export objects
* session export objects
*/ */
Crypto.prototype.exportRoomKeys = function() { Crypto.prototype.exportRoomKeys = async function() {
return Promise.map( const exportedSessions = [];
this._sessionStore.getAllEndToEndInboundGroupSessionKeys(), await this._cryptoStore.doTxn(
(s) => { 'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
return this._olmDevice.exportInboundGroupSession( this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (s) => {
s.senderKey, s.sessionId, if (s === null) return;
).then((sess) => {
const sess = this._olmDevice.exportInboundGroupSession(
s.senderKey, s.sessionId, s.sessionData,
);
sess.algorithm = olmlib.MEGOLM_ALGORITHM; sess.algorithm = olmlib.MEGOLM_ALGORITHM;
return sess; exportedSessions.push(sess);
}); });
}, },
); );
return exportedSessions;
}; };
/** /**

View File

@@ -1,7 +1,7 @@
import Promise from 'bluebird'; import Promise from 'bluebird';
import utils from '../../utils'; import utils from '../../utils';
export const VERSION = 3; export const VERSION = 4;
/** /**
* Implementation of a CryptoStore which is backed by an existing * Implementation of a CryptoStore which is backed by an existing
@@ -258,6 +258,8 @@ export class Backend {
return promiseifyTxn(txn); return promiseifyTxn(txn);
} }
// Olm Account
getAccount(txn, func) { getAccount(txn, func) {
const objectStore = txn.objectStore("account"); const objectStore = txn.objectStore("account");
const getReq = objectStore.get("-"); const getReq = objectStore.get("-");
@@ -275,6 +277,8 @@ export class Backend {
objectStore.put(newData, "-"); objectStore.put(newData, "-");
} }
// Olm Sessions
countEndToEndSessions(txn, func) { countEndToEndSessions(txn, func) {
const objectStore = txn.objectStore("sessions"); const objectStore = txn.objectStore("sessions");
const countReq = objectStore.count(); const countReq = objectStore.count();
@@ -324,6 +328,69 @@ export class Backend {
objectStore.put({deviceKey, sessionId, session}); objectStore.put({deviceKey, sessionId, session});
} }
// Inbound group sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.get([senderCurve25519Key, sessionId]);
getReq.onsuccess = function() {
try {
if (getReq.result) {
func(getReq.result.session);
} else {
func(null);
}
} catch (e) {
abortWithException(txn, e);
}
};
}
getAllEndToEndInboundGroupSessions(txn, func) {
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
try {
func({
senderKey: cursor.value.senderCurve25519Key,
sessionId: cursor.value.sessionId,
sessionData: cursor.value.session,
});
} catch (e) {
abortWithException(txn, e);
}
cursor.continue();
} else {
try {
func(null);
} catch (e) {
abortWithException(txn, e);
}
}
};
}
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
const objectStore = txn.objectStore("inbound_group_sessions");
const addReq = objectStore.add({
senderCurve25519Key, sessionId, session: sessionData,
});
addReq.onerror = () => {
abortWithException(txn, new Error(
"Failed to add inbound group session - session may already exist",
));
};
}
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
const objectStore = txn.objectStore("inbound_group_sessions");
objectStore.put({
senderCurve25519Key, sessionId, session: sessionData,
});
}
doTxn(mode, stores, func) { doTxn(mode, stores, func) {
const txn = this._db.transaction(stores, mode); const txn = this._db.transaction(stores, mode);
const promise = promiseifyTxn(txn); const promise = promiseifyTxn(txn);
@@ -351,6 +418,11 @@ export function upgradeDatabase(db, oldVersion) {
}); });
sessionsStore.createIndex("deviceKey", "deviceKey"); sessionsStore.createIndex("deviceKey", "deviceKey");
} }
if (oldVersion < 4) {
db.createObjectStore("inbound_group_sessions", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
// Expand as needed. // Expand as needed.
} }
@@ -376,13 +448,28 @@ function abortWithException(txn, e) {
// We could alternatively make the thing we pass back to the app // We could alternatively make the thing we pass back to the app
// an object containing the transaction and exception. // an object containing the transaction and exception.
txn._mx_abortexception = e; txn._mx_abortexception = e;
txn.abort(); try {
txn.abort();
} catch (e) {
// sometimes we won't be able to abort the transaction
// (ie. if it's aborted or completed)
}
} }
function promiseifyTxn(txn) { function promiseifyTxn(txn) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
txn.oncomplete = resolve; txn.oncomplete = () => {
txn.onerror = reject; if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
}
resolve();
};
txn.onerror = () => {
if (txn._mx_abortexception !== undefined) {
reject(txn._mx_abortexception);
}
reject();
};
txn.onabort = () => reject(txn._mx_abortexception); txn.onabort = () => reject(txn._mx_abortexception);
}); });
} }

View File

@@ -227,6 +227,8 @@ export default class IndexedDBCryptoStore {
}); });
} }
// Olm Account
/* /*
* Get the account pickle from the store. * Get the account pickle from the store.
* This requires an active transaction. See doTxn(). * This requires an active transaction. See doTxn().
@@ -249,6 +251,8 @@ export default class IndexedDBCryptoStore {
this._backendPromise.value().storeAccount(txn, newData); this._backendPromise.value().storeAccount(txn, newData);
} }
// Olm sessions
/** /**
* Returns the number of end-to-end sessions in the store * Returns the number of end-to-end sessions in the store
* @param {*} txn An active transaction. See doTxn(). * @param {*} txn An active transaction. See doTxn().
@@ -296,6 +300,64 @@ export default class IndexedDBCryptoStore {
); );
} }
// Inbound group saessions
/**
* Retrieve the end-to-end inbound group session for a given
* server key and session ID
* @param {string} senderCurve25519Key The sender's curve 25519 key
* @param {string} sessionId The ID of the session
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called with A map from sessionId
* to Base64 end-to-end session.
*/
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
this._backendPromise.value().getEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, txn, func,
);
}
/**
* Fetches all inbound group sessions in the store
* @param {*} txn An active transaction. See doTxn().
* @param {function(object)} func Called once for each group session
* in the store with an object having keys {senderKey, sessionId,
* sessionData}, then once with null to indicate the end of the list.
*/
getAllEndToEndInboundGroupSessions(txn, func) {
this._backendPromise.value().getAllEndToEndInboundGroupSessions(txn, func);
}
/**
* Adds an end-to-end inbound group session to the store.
* If there already exists an inbound group session with the same
* senderCurve25519Key and sessionID, the session will not be added.
* @param {string} senderCurve25519Key The sender's curve 25519 key
* @param {string} sessionId The ID of the session
* @param {object} sessionData The session data structure
* @param {*} txn An active transaction. See doTxn().
*/
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backendPromise.value().addEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
/**
* Writes an end-to-end inbound group session to the store.
* If there already exists an inbound group session with the same
* senderCurve25519Key and sessionID, it will be overwritten.
* @param {string} senderCurve25519Key The sender's curve 25519 key
* @param {string} sessionId The ID of the session
* @param {object} sessionData The session data structure
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backendPromise.value().storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
/** /**
* Perform a transaction on the crypto store. Any store methods * Perform a transaction on the crypto store. Any store methods
* that require a transaction (txn) object to be passed in may * that require a transaction (txn) object to be passed in may
@@ -326,3 +388,4 @@ export default class IndexedDBCryptoStore {
IndexedDBCryptoStore.STORE_ACCOUNT = 'account'; IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions'; IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';

View File

@@ -29,11 +29,16 @@ import MemoryCryptoStore from './memory-crypto-store.js';
const E2E_PREFIX = "crypto."; const E2E_PREFIX = "crypto.";
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account"; const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
function keyEndToEndSessions(deviceKey) { function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey; return E2E_PREFIX + "sessions/" + deviceKey;
} }
function keyEndToEndInboundGroupSession(senderKey, sessionId) {
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
}
/** /**
* @implements {module:crypto/store/base~CryptoStore} * @implements {module:crypto/store/base~CryptoStore}
*/ */
@@ -43,6 +48,8 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
this.store = global.localStorage; this.store = global.localStorage;
} }
// Olm Sessions
countEndToEndSessions(txn, func) { countEndToEndSessions(txn, func) {
let count = 0; let count = 0;
for (let i = 0; i < this.store.length; ++i) { for (let i = 0; i < this.store.length; ++i) {
@@ -72,6 +79,54 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
); );
} }
// Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
func(getJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
));
}
getAllEndToEndInboundGroupSessions(txn, func) {
for (let i = 0; i < this.store.length; ++i) {
const key = this.store.key(i);
if (key.startsWith(KEY_INBOUND_SESSION_PREFIX)) {
// we can't use split, as the components we are trying to split out
// might themselves contain '/' characters. We rely on the
// senderKey being a (32-byte) curve25519 key, base64-encoded
// (hence 43 characters long).
func({
senderKey: key.substr(KEY_INBOUND_SESSION_PREFIX.length, 43),
sessionId: key.substr(KEY_INBOUND_SESSION_PREFIX.length + 44),
sessionData: getJsonItem(this.store, key),
});
}
}
func(null);
}
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
const existing = getJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
);
if (!existing) {
this.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
}
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
setJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
sessionData,
);
}
/** /**
* Delete all data from this store. * Delete all data from this store.
* *
@@ -82,6 +137,8 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
return Promise.resolve(); return Promise.resolve();
} }
// Olm account
getAccount(txn, func) { getAccount(txn, func) {
const account = this.store.getItem(KEY_END_TO_END_ACCOUNT); const account = this.store.getItem(KEY_END_TO_END_ACCOUNT);
func(account); func(account);

View File

@@ -34,6 +34,8 @@ export default class MemoryCryptoStore {
// Map of {devicekey -> {sessionId -> session pickle}} // Map of {devicekey -> {sessionId -> session pickle}}
this._sessions = {}; this._sessions = {};
// Map of {senderCurve25519Key+'/'+sessionId -> session data object}
this._inboundGroupSessions = {};
} }
/** /**
@@ -201,6 +203,8 @@ export default class MemoryCryptoStore {
return Promise.resolve(null); return Promise.resolve(null);
} }
// Olm Account
getAccount(txn, func) { getAccount(txn, func) {
func(this._account); func(this._account);
} }
@@ -209,6 +213,8 @@ export default class MemoryCryptoStore {
this._account = newData; this._account = newData;
} }
// Olm Sessions
countEndToEndSessions(txn, func) { countEndToEndSessions(txn, func) {
return Object.keys(this._sessions).length; return Object.keys(this._sessions).length;
} }
@@ -231,6 +237,40 @@ export default class MemoryCryptoStore {
deviceSessions[sessionId] = session; deviceSessions[sessionId] = session;
} }
// Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
func(this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] || null);
}
getAllEndToEndInboundGroupSessions(txn, func) {
for (const key of Object.keys(this._inboundGroupSessions)) {
// we can't use split, as the components we are trying to split out
// might themselves contain '/' characters. We rely on the
// senderKey being a (32-byte) curve25519 key, base64-encoded
// (hence 43 characters long).
func({
senderKey: key.substr(0, 43),
sessionId: key.substr(44),
sessionData: this._inboundGroupSessions[key],
});
}
func(null);
}
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
const k = senderCurve25519Key+'/'+sessionId;
if (this._inboundGroupSessions[k] === undefined) {
this._inboundGroupSessions[k] = sessionData;
}
}
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData;
}
doTxn(mode, stores, func) { doTxn(mode, stores, func) {
return Promise.resolve(func(null)); return Promise.resolve(func(null));
} }

View File

@@ -178,9 +178,8 @@ WebStorageSessionStore.prototype = {
return this.store.getItem(key); return this.store.getItem(key);
}, },
storeEndToEndInboundGroupSession: function(senderKey, sessionId, pickledSession) { removeAllEndToEndInboundGroupSessions: function() {
const key = keyEndToEndInboundGroupSession(senderKey, sessionId); removeByPrefix(this.store, E2E_PREFIX + 'inboundgroupsessions/');
return this.store.setItem(key, pickledSession);
}, },
/** /**