You've already forked matrix-js-sdk
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:
@@ -10,6 +10,7 @@ import Promise from 'bluebird';
|
||||
import sdk from '../../../..';
|
||||
import algorithms from '../../../../lib/crypto/algorithms';
|
||||
import WebStorageSessionStore from '../../../../lib/store/session/webstorage';
|
||||
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
|
||||
import MockStorageApi from '../../../MockStorageApi';
|
||||
import testUtils from '../../../test-utils';
|
||||
|
||||
@@ -45,8 +46,9 @@ describe("MegolmDecryption", function() {
|
||||
|
||||
const mockStorage = new MockStorageApi();
|
||||
const sessionStore = new WebStorageSessionStore(mockStorage);
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
const olmDevice = new OlmDevice(sessionStore);
|
||||
const olmDevice = new OlmDevice(sessionStore, cryptoStore);
|
||||
|
||||
megolmDecryption = new MegolmDecryption({
|
||||
userId: '@user:id',
|
||||
|
||||
@@ -216,6 +216,41 @@ OlmDevice.prototype._migrateFromSessionStore = async function() {
|
||||
|
||||
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 {string} sessionId
|
||||
* @param {InboundGroupSessionData} sessionData
|
||||
* @private
|
||||
* @param {Object} sessionData Object describing the session.
|
||||
* @param {function(Olm.InboundGroupSession)} func Invoked with the unpickled session
|
||||
* @return {*} result of func
|
||||
*/
|
||||
OlmDevice.prototype._saveInboundGroupSession = function(
|
||||
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 + ")",
|
||||
);
|
||||
}
|
||||
|
||||
OlmDevice.prototype._unpickleInboundGroupSession = function(sessionData, func) {
|
||||
const session = new Olm.InboundGroupSession();
|
||||
try {
|
||||
session.unpickle(this._pickleKey, r.session);
|
||||
return func(session, r);
|
||||
session.unpickle(this._pickleKey, sessionData.session);
|
||||
return func(session);
|
||||
} finally {
|
||||
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
|
||||
*
|
||||
@@ -853,21 +882,17 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
sessionId, sessionKey, keysClaimed,
|
||||
exportFormat,
|
||||
) {
|
||||
const self = this;
|
||||
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
|
||||
/* if we already have this session, consider updating it */
|
||||
function updateSession(session, sessionData) {
|
||||
console.log("Update for megolm session " + senderKey + "/" + sessionId);
|
||||
// for now we just ignore updates. TODO: implement something here
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const r = this._getInboundGroupSession(
|
||||
roomId, senderKey, sessionId, updateSession,
|
||||
this._getInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn,
|
||||
(existingSession, existingSessionData) => {
|
||||
if (existingSession) {
|
||||
console.log(
|
||||
"Update for megolm session " + senderKey + "/" + sessionId,
|
||||
);
|
||||
|
||||
if (r !== null) {
|
||||
// for now we just ignore updates. TODO: implement something here
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -881,7 +906,8 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
}
|
||||
if (sessionId != session.session_id()) {
|
||||
throw new Error(
|
||||
"Mismatched group session ID from senderKey: " + senderKey,
|
||||
"Mismatched group session ID from senderKey: " +
|
||||
senderKey,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -892,61 +918,16 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
|
||||
};
|
||||
|
||||
self._saveInboundGroupSession(
|
||||
senderKey, sessionId, sessionData,
|
||||
this._cryptoStore.addEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, sessionData, txn,
|
||||
);
|
||||
} 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,9 +949,16 @@ OlmDevice.prototype.importInboundGroupSession = async function(data) {
|
||||
OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
roomId, senderKey, sessionId, body, eventId, timestamp,
|
||||
) {
|
||||
const self = this;
|
||||
let result;
|
||||
|
||||
function decrypt(session, sessionData) {
|
||||
await this._cryptoStore.doTxn(
|
||||
'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;
|
||||
@@ -982,37 +970,47 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
// 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
|
||||
// don't consider it a replay attack.
|
||||
const messageIndexKey = senderKey + "|" + sessionId + "|" + res.message_index;
|
||||
if (messageIndexKey in self._inboundGroupSessionMessageIndexes) {
|
||||
const msgInfo = self._inboundGroupSessionMessageIndexes[messageIndexKey];
|
||||
if (msgInfo.id !== eventId || msgInfo.timestamp !== timestamp) {
|
||||
const messageIndexKey = (
|
||||
senderKey + "|" + sessionId + "|" + res.message_index
|
||||
);
|
||||
if (messageIndexKey in this._inboundGroupSessionMessageIndexes) {
|
||||
const msgInfo = (
|
||||
this._inboundGroupSessionMessageIndexes[messageIndexKey]
|
||||
);
|
||||
if (
|
||||
msgInfo.id !== eventId ||
|
||||
msgInfo.timestamp !== timestamp
|
||||
) {
|
||||
throw new Error(
|
||||
"Duplicate message index, possible replay attack: " +
|
||||
messageIndexKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
self._inboundGroupSessionMessageIndexes[messageIndexKey] = {
|
||||
this._inboundGroupSessionMessageIndexes[messageIndexKey] = {
|
||||
id: eventId,
|
||||
timestamp: timestamp,
|
||||
};
|
||||
}
|
||||
|
||||
sessionData.session = session.pickle(self._pickleKey);
|
||||
self._saveInboundGroupSession(
|
||||
senderKey, sessionId, sessionData,
|
||||
sessionData.session = session.pickle(this._pickleKey);
|
||||
this._cryptoStore.storeEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, sessionData, txn,
|
||||
);
|
||||
return {
|
||||
result = {
|
||||
result: plaintext,
|
||||
keysClaimed: sessionData.keysClaimed || {},
|
||||
senderKey: senderKey,
|
||||
forwardingCurve25519KeyChain: sessionData.forwardingCurve25519KeyChain || [],
|
||||
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
|
||||
*/
|
||||
OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, sessionId) {
|
||||
const s = this._sessionStore.getEndToEndInboundGroupSession(
|
||||
senderKey, sessionId,
|
||||
);
|
||||
|
||||
if (s === null) {
|
||||
return false;
|
||||
let result;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
|
||||
this._cryptoStore.getEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, txn, (sessionData) => {
|
||||
if (sessionData === null) {
|
||||
result = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const r = JSON.parse(s);
|
||||
if (roomId !== r.room_id) {
|
||||
if (roomId !== sessionData.room_id) {
|
||||
console.warn(
|
||||
`requested keys for inbound group session ${senderKey}|` +
|
||||
`${sessionId}, with incorrect room_id (expected ${r.room_id}, ` +
|
||||
`${sessionId}, with incorrect room_id ` +
|
||||
`(expected ${sessionData.room_id}, ` +
|
||||
`was ${roomId})`,
|
||||
);
|
||||
return false;
|
||||
result = false;
|
||||
} else {
|
||||
result = true;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return true;
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1063,24 +1069,33 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
|
||||
OlmDevice.prototype.getInboundGroupSessionKey = async function(
|
||||
roomId, senderKey, sessionId,
|
||||
) {
|
||||
function getKey(session, sessionData) {
|
||||
let result;
|
||||
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 senderEd25519Key = claimedKeys.ed25519 || null;
|
||||
|
||||
return {
|
||||
result = {
|
||||
"chain_index": messageIndex,
|
||||
"key": session.export_session(messageIndex),
|
||||
"forwarding_curve25519_key_chain":
|
||||
sessionData.forwardingCurve25519KeyChain || [],
|
||||
"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} 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) {
|
||||
const s = this._sessionStore.getEndToEndInboundGroupSession(
|
||||
senderKey, sessionId,
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
OlmDevice.prototype.exportInboundGroupSession = function(
|
||||
senderKey, sessionId, sessionData,
|
||||
) {
|
||||
return this._unpickleInboundGroupSession(sessionData, (session) => {
|
||||
const messageIndex = session.first_known_index();
|
||||
|
||||
return {
|
||||
"sender_key": senderKey,
|
||||
"sender_claimed_keys": r.keysClaimed,
|
||||
"room_id": r.room_id,
|
||||
"sender_claimed_keys": sessionData.keysClaimed,
|
||||
"room_id": sessionData.room_id,
|
||||
"session_id": sessionId,
|
||||
"session_key": session.export_session(messageIndex),
|
||||
"forwarding_curve25519_key_chain":
|
||||
session.forwardingCurve25519KeyChain || [],
|
||||
"forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [],
|
||||
};
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Utilities
|
||||
|
||||
@@ -927,10 +927,18 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
|
||||
* @param {module:crypto/OlmDevice.MegolmSessionData} session
|
||||
*/
|
||||
MegolmDecryption.prototype.importRoomKey = function(session) {
|
||||
this._olmDevice.importInboundGroupSession(session);
|
||||
|
||||
return this._olmDevice.addInboundGroupSession(
|
||||
session.room_id,
|
||||
session.sender_key,
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -700,21 +700,25 @@ Crypto.prototype.isRoomEncrypted = function(roomId) {
|
||||
/**
|
||||
* Get a list containing all of the room keys
|
||||
*
|
||||
* @return {module:client.Promise} a promise which resolves to a list of
|
||||
* session export objects
|
||||
* @return {module:crypto/OlmDevice.MegolmSessionData[]} a list of session export objects
|
||||
*/
|
||||
Crypto.prototype.exportRoomKeys = function() {
|
||||
return Promise.map(
|
||||
this._sessionStore.getAllEndToEndInboundGroupSessionKeys(),
|
||||
(s) => {
|
||||
return this._olmDevice.exportInboundGroupSession(
|
||||
s.senderKey, s.sessionId,
|
||||
).then((sess) => {
|
||||
Crypto.prototype.exportRoomKeys = async function() {
|
||||
const exportedSessions = [];
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
|
||||
this._cryptoStore.getAllEndToEndInboundGroupSessions(txn, (s) => {
|
||||
if (s === null) return;
|
||||
|
||||
const sess = this._olmDevice.exportInboundGroupSession(
|
||||
s.senderKey, s.sessionId, s.sessionData,
|
||||
);
|
||||
sess.algorithm = olmlib.MEGOLM_ALGORITHM;
|
||||
return sess;
|
||||
exportedSessions.push(sess);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
return exportedSessions;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Promise from 'bluebird';
|
||||
import utils from '../../utils';
|
||||
|
||||
export const VERSION = 3;
|
||||
export const VERSION = 4;
|
||||
|
||||
/**
|
||||
* Implementation of a CryptoStore which is backed by an existing
|
||||
@@ -258,6 +258,8 @@ export class Backend {
|
||||
return promiseifyTxn(txn);
|
||||
}
|
||||
|
||||
// Olm Account
|
||||
|
||||
getAccount(txn, func) {
|
||||
const objectStore = txn.objectStore("account");
|
||||
const getReq = objectStore.get("-");
|
||||
@@ -275,6 +277,8 @@ export class Backend {
|
||||
objectStore.put(newData, "-");
|
||||
}
|
||||
|
||||
// Olm Sessions
|
||||
|
||||
countEndToEndSessions(txn, func) {
|
||||
const objectStore = txn.objectStore("sessions");
|
||||
const countReq = objectStore.count();
|
||||
@@ -324,6 +328,69 @@ export class Backend {
|
||||
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) {
|
||||
const txn = this._db.transaction(stores, mode);
|
||||
const promise = promiseifyTxn(txn);
|
||||
@@ -351,6 +418,11 @@ export function upgradeDatabase(db, oldVersion) {
|
||||
});
|
||||
sessionsStore.createIndex("deviceKey", "deviceKey");
|
||||
}
|
||||
if (oldVersion < 4) {
|
||||
db.createObjectStore("inbound_group_sessions", {
|
||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||
});
|
||||
}
|
||||
// Expand as needed.
|
||||
}
|
||||
|
||||
@@ -376,13 +448,28 @@ function abortWithException(txn, e) {
|
||||
// We could alternatively make the thing we pass back to the app
|
||||
// an object containing the transaction and exception.
|
||||
txn._mx_abortexception = e;
|
||||
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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
txn.oncomplete = resolve;
|
||||
txn.onerror = reject;
|
||||
txn.oncomplete = () => {
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -227,6 +227,8 @@ export default class IndexedDBCryptoStore {
|
||||
});
|
||||
}
|
||||
|
||||
// Olm Account
|
||||
|
||||
/*
|
||||
* Get the account pickle from the store.
|
||||
* This requires an active transaction. See doTxn().
|
||||
@@ -249,6 +251,8 @@ export default class IndexedDBCryptoStore {
|
||||
this._backendPromise.value().storeAccount(txn, newData);
|
||||
}
|
||||
|
||||
// Olm sessions
|
||||
|
||||
/**
|
||||
* Returns the number of end-to-end sessions in the store
|
||||
* @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
|
||||
* 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_SESSIONS = 'sessions';
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
|
||||
|
||||
@@ -29,11 +29,16 @@ import MemoryCryptoStore from './memory-crypto-store.js';
|
||||
|
||||
const E2E_PREFIX = "crypto.";
|
||||
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||||
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
|
||||
|
||||
function keyEndToEndSessions(deviceKey) {
|
||||
return E2E_PREFIX + "sessions/" + deviceKey;
|
||||
}
|
||||
|
||||
function keyEndToEndInboundGroupSession(senderKey, sessionId) {
|
||||
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @implements {module:crypto/store/base~CryptoStore}
|
||||
*/
|
||||
@@ -43,6 +48,8 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
this.store = global.localStorage;
|
||||
}
|
||||
|
||||
// Olm Sessions
|
||||
|
||||
countEndToEndSessions(txn, func) {
|
||||
let count = 0;
|
||||
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.
|
||||
*
|
||||
@@ -82,6 +137,8 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// Olm account
|
||||
|
||||
getAccount(txn, func) {
|
||||
const account = this.store.getItem(KEY_END_TO_END_ACCOUNT);
|
||||
func(account);
|
||||
|
||||
@@ -34,6 +34,8 @@ export default class MemoryCryptoStore {
|
||||
|
||||
// Map of {devicekey -> {sessionId -> session pickle}}
|
||||
this._sessions = {};
|
||||
// Map of {senderCurve25519Key+'/'+sessionId -> session data object}
|
||||
this._inboundGroupSessions = {};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,6 +203,8 @@ export default class MemoryCryptoStore {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// Olm Account
|
||||
|
||||
getAccount(txn, func) {
|
||||
func(this._account);
|
||||
}
|
||||
@@ -209,6 +213,8 @@ export default class MemoryCryptoStore {
|
||||
this._account = newData;
|
||||
}
|
||||
|
||||
// Olm Sessions
|
||||
|
||||
countEndToEndSessions(txn, func) {
|
||||
return Object.keys(this._sessions).length;
|
||||
}
|
||||
@@ -231,6 +237,40 @@ export default class MemoryCryptoStore {
|
||||
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) {
|
||||
return Promise.resolve(func(null));
|
||||
}
|
||||
|
||||
@@ -178,9 +178,8 @@ WebStorageSessionStore.prototype = {
|
||||
return this.store.getItem(key);
|
||||
},
|
||||
|
||||
storeEndToEndInboundGroupSession: function(senderKey, sessionId, pickledSession) {
|
||||
const key = keyEndToEndInboundGroupSession(senderKey, sessionId);
|
||||
return this.store.setItem(key, pickledSession);
|
||||
removeAllEndToEndInboundGroupSessions: function() {
|
||||
removeByPrefix(this.store, E2E_PREFIX + 'inboundgroupsessions/');
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user