1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-11-29 16:43:09 +03:00

Change crypto store transaction API

To allow multiple things to be fetched/stored in a single
transaction.

Currently it is still just the account that's actually in
indexeddb though.
This commit is contained in:
David Baker
2017-11-29 16:22:54 +00:00
parent 0238ecebed
commit f11a58e2cb
5 changed files with 160 additions and 123 deletions

View File

@@ -14,7 +14,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
/**
* olm.js wrapper
@@ -144,21 +145,24 @@ OlmDevice.prototype.init = async function() {
async function _initialise_account(sessionStore, cryptoStore, pickleKey, account) {
let removeFromSessionStore = false;
await cryptoStore.endToEndAccountTransaction((pickledAccount, save) => {
if (pickledAccount !== null) {
account.unpickle(pickleKey, pickledAccount);
} else {
// Migrate from sessionStore
pickledAccount = sessionStore.getEndToEndAccount();
await cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
cryptoStore.getAccount(txn, (pickledAccount) => {
if (pickledAccount !== null) {
removeFromSessionStore = true;
account.unpickle(pickleKey, pickledAccount);
} else {
account.create();
pickledAccount = account.pickle(pickleKey);
// Migrate from sessionStore
pickledAccount = sessionStore.getEndToEndAccount();
if (pickledAccount !== null) {
removeFromSessionStore = true;
account.unpickle(pickleKey, pickledAccount);
} else {
account.create();
pickledAccount = account.pickle(pickleKey);
}
cryptoStore.storeAccount(txn, pickledAccount);
}
save(pickledAccount);
}
});
});
// only remove this once it's safely saved to the crypto store
@@ -177,36 +181,30 @@ OlmDevice.getOlmVersion = function() {
/**
* extract our OlmAccount from the crypto store and call the given function
* with the account object and a 'save' function which returns a promise.
* The `account` will be freed as soon as `func` returns - even if func returns
* a promise
* with the account object
* The `account` object is useable only within the callback passed to this
* function and will be freed as soon the callback returns. It is *not*
* useable for the rest of the lifetime of the transaction.
*
* @param {*} txn
* @param {function} func
* @return {object} result of func
* @private
*/
OlmDevice.prototype._getAccount = async function(func) {
let result;
await this._cryptoStore.endToEndAccountTransaction((pickledAccount, save) => {
// Olm has a limited heap size so we must tightly control the number of
// Olm account objects in existence at any given time: once created, it
// must be destroyed again before we await.
OlmDevice.prototype._getAccount = function(txn, func) {
this._cryptoStore.getAccount(txn, (pickledAccount) => {
const account = new Olm.Account();
try {
account.unpickle(this._pickleKey, pickledAccount);
result = func(account, () => {
const pickledAccount = account.pickle(this._pickleKey);
save(pickledAccount);
});
func(account);
} finally {
account.free();
}
});
return result;
};
OlmDevice.prototype._storeAccount = function(txn, account) {
this._cryptoStore.storeAccount(txn, account.pickle(this._pickleKey));
};
/**
* extract an OlmSession from the session store and call the given function
@@ -270,9 +268,13 @@ OlmDevice.prototype._getUtility = function(func) {
* @return {Promise<string>} base64-encoded signature
*/
OlmDevice.prototype.sign = async function(message) {
return await this._getAccount(function(account) {
return account.sign(message);
let result;
await this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this._getAccount(txn, (account) => {
result = account.sign(message);
});
});
return result;
};
/**
@@ -283,9 +285,14 @@ OlmDevice.prototype.sign = async function(message) {
* key.
*/
OlmDevice.prototype.getOneTimeKeys = async function() {
return await this._getAccount(function(account) {
return JSON.parse(account.one_time_keys());
let result;
await this._cryptoStore.doTxn('readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this._getAccount(txn, (account) => {
result = JSON.parse(account.one_time_keys())
});
});
return result;
};
@@ -302,9 +309,11 @@ OlmDevice.prototype.maxNumberOfOneTimeKeys = function() {
* Marks all of the one-time keys as published.
*/
OlmDevice.prototype.markKeysAsPublished = async function() {
await this._getAccount(function(account, save) {
account.mark_keys_as_published();
save();
await this._cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this._getAccount(txn, (account) => {
account.mark_keys_as_published();
this._storeAccount(txn, account);
});
});
};
@@ -315,9 +324,11 @@ OlmDevice.prototype.markKeysAsPublished = async function() {
* @return {Promise} Resolved once the account is saved back having generated the keys
*/
OlmDevice.prototype.generateOneTimeKeys = async function(numKeys) {
return this._getAccount(function(account, save) {
account.generate_one_time_keys(numKeys);
save();
await this._cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this._getAccount(txn, (account) => {
account.generate_one_time_keys(numKeys);
this._storeAccount(txn, account);
});
});
};
@@ -333,18 +344,22 @@ OlmDevice.prototype.generateOneTimeKeys = async function(numKeys) {
OlmDevice.prototype.createOutboundSession = async function(
theirIdentityKey, theirOneTimeKey,
) {
const self = this;
return await this._getAccount(function(account, save) {
const session = new Olm.Session();
try {
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
save();
self._saveSession(theirIdentityKey, session);
return session.session_id();
} finally {
session.free();
}
let newSessionId;
await this._cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this._getAccount(txn, (account) => {
const session = new Olm.Session();
try {
session.create_outbound(account, theirIdentityKey, theirOneTimeKey);
newSessionId = session.session_id();
this._storeAccount(txn, account);
this._saveSession(theirIdentityKey, session);
return session.session_id();
} finally {
session.free();
}
});
});
return newSessionId;
};
@@ -368,26 +383,30 @@ OlmDevice.prototype.createInboundSession = async function(
throw new Error("Need message_type == 0 to create inbound session");
}
const self = this;
return await this._getAccount(function(account, save) {
const session = new Olm.Session();
try {
session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext);
account.remove_one_time_keys(session);
save();
let result;
await this._cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
this._getAccount(txn, (account) => {
const session = new Olm.Session();
try {
session.create_inbound_from(account, theirDeviceIdentityKey, ciphertext);
account.remove_one_time_keys(session);
this._storeAccount(txn, account);
const payloadString = session.decrypt(message_type, ciphertext);
const payloadString = session.decrypt(message_type, ciphertext);
self._saveSession(theirDeviceIdentityKey, session);
this._saveSession(theirDeviceIdentityKey, session);
return {
payload: payloadString,
session_id: session.session_id(),
};
} finally {
session.free();
}
result = {
payload: payloadString,
session_id: session.session_id(),
};
} finally {
session.free();
}
});
});
return result;
};

View File

@@ -258,35 +258,23 @@ export class Backend {
return promiseifyTxn(txn);
}
/**
* Load the end to end account for the logged-in user. Once the account
* is retrieved, the given function is executed and passed the pickled
* account string and a method for saving the pickle
* back to the database. This allows the account to be read and writen
* atomically.
* @param {function(string, function())} func Function called with the
* picked account and a save function
* @return {Promise} Resolves with the return value of `func` once
* the transaction is complete (ie. once data is written back if the
* save function is called.)
*/
endToEndAccountTransaction(func) {
const txn = this._db.transaction("account", "readwrite");
getAccount(txn, func) {
const objectStore = txn.objectStore("account");
const txnPromise = promiseifyTxn(txn);
const getReq = objectStore.get("-");
let result;
getReq.onsuccess = function() {
result = func(
getReq.result || null,
(newData) => {
objectStore.put(newData, "-");
},
);
func(getReq.result || null);
};
return txnPromise.then(() => {
}
storeAccount(txn, newData) {
const objectStore = txn.objectStore("account");
objectStore.put(newData, "-");
}
doTxn(mode, stores, func) {
const txn = this._db.transaction(stores, mode);
const result = func(txn);
return promiseifyTxn(txn).then(() => {
return result;
});
}

View File

@@ -227,21 +227,50 @@ export default class IndexedDBCryptoStore {
});
}
/**
* Load the end to end account for the logged-in user. Once the account
* is retrieved, the given function is executed and passed the pickled
* account string and a method for saving the pickle
* back to the database. This allows the account to be read and writen
* atomically.
* @param {function(string, function())} func Function called with the
* account data and a save function
* @return {Promise} Resolves with the return value of `func` once
* the transaction is complete (ie. once data is written back if the
* save function is called.)
/*
* Get the account pickle from the store.
* This requires an active transaction. See doTxn().
*
* @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the account pickle
*/
endToEndAccountTransaction(func) {
getAccount(txn, func) {
this._backendPromise.value().getAccount(txn, func);
}
/*
* Write the account pickle to the store.
* This requires an active transaction. See doTxn().
*
* @param {*} txn An active transaction. See doTxn().
* @param {string} newData The new account pickle to store.
*/
storeAccount(txn, newData) {
this._backendPromise.value().storeAccount(txn, newData);
}
/**
* Perform a transaction on the crypto store. Any store methods
* that require a transaction (txn) object to be passed in may
* only be called within a callback of either this function or
* one of the store functions operating on the same transaction.
*
* @param {string} mode 'readwrite' if you need to call setter
* functions with this transaction. Otherwise, 'readonly'.
* @param {string[]} stores List IndexedDBCryptoStore.STORE_*
* options representing all types of object that will be
* accessed or written to with this transaction.
* @param {function(*)} func Function called with the
* transaction object: an opaque object that should be passed
* to store functions.
* @return {Promise} Promise that resolves with the result of the `func`
* when the transaction is complete
*/
doTxn(mode, stores, func) {
return this._connect().then((backend) => {
return backend.endToEndAccountTransaction(func);
return backend.doTxn(mode, stores, func);
});
}
}
IndexedDBCryptoStore.STORE_ACCOUNT = 'account';

View File

@@ -49,10 +49,16 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
return Promise.resolve();
}
endToEndAccountTransaction(func) {
getAccount(txn, func) {
const account = this.store.getItem(KEY_END_TO_END_ACCOUNT);
return Promise.resolve(func(account, (newData) => {
this.store.setItem(KEY_END_TO_END_ACCOUNT, newData);
}));
func(account);
}
storeAccount(txn, newData) {
this.store.setItem(KEY_END_TO_END_ACCOUNT, newData);
}
doTxn(mode, stores, func) {
return Promise.resolve(func(null));
}
}

View File

@@ -198,20 +198,15 @@ export default class MemoryCryptoStore {
return Promise.resolve(null);
}
/**
* Load the end to end account for the logged-in user. Once the account
* is retrieved, the given function is executed and passed the base64
* encoded account string and a method for saving the account string
* back to the database. This allows the account to be read and writen
* atomically.
* @param {func} func Function called with the account data and a save function
* @return {Promise} Resolves with the return value of the function once
* the transaction is complete (ie. once data is written back if the
* save function is called.
*/
endToEndAccountTransaction(func) {
return Promise.resolve(func(this._account, (newData) => {
this._account = newData;
}));
getAccount(txn, func) {
func(this._account);
}
storeAccount(txn, newData) {
this._account = newData;
}
doTxn(mode, stores, func) {
return Promise.resolve(func(null));
}
}