Skip to content

Commit

Permalink
Merge pull request #582 from matrix-org/dbkr/crypto_store_txn_api
Browse files Browse the repository at this point in the history
Change crypto store transaction API
  • Loading branch information
dbkr authored Nov 30, 2017
2 parents 7f590af + ecc1c86 commit 082683b
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 141 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && uglifyjs -c -m -o dist/browser-matrix.min.js --source-map dist/browser-matrix.min.js.map --in-source-map dist/browser-matrix.js.map dist/browser-matrix.js",
"dist": "npm run build",
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
"lint": "eslint --max-warnings 109 src spec",
"lint": "eslint --max-warnings 102 src spec",
"prepublish": "npm run clean && npm run build && git rev-parse HEAD > git-revision.txt"
},
"repository": {
Expand Down
218 changes: 134 additions & 84 deletions src/crypto/OlmDevice.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -127,7 +128,7 @@ OlmDevice.prototype.init = async function() {
let e2eKeys;
const account = new Olm.Account();
try {
await _initialise_account(
await _initialiseAccount(
this._sessionStore, this._cryptoStore, this._pickleKey, account,
);
e2eKeys = JSON.parse(account.identity_keys());
Expand All @@ -142,23 +143,26 @@ OlmDevice.prototype.init = async function() {
};


async function _initialise_account(sessionStore, cryptoStore, pickleKey, account) {
async function _initialiseAccount(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
Expand All @@ -177,36 +181,41 @@ 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.
* This function requires a live transaction object from cryptoStore.doTxn()
* and therefore may only be called in a doTxn() callback.
*
* @param {*} txn Opaque transaction object from cryptoStore.doTxn()
* @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;
};

/*
* Saves an account to the crypto store.
* This function requires a live transaction object from cryptoStore.doTxn()
* and therefore may only be called in a doTxn() callback.
*
* @param {*} txn Opaque transaction object from cryptoStore.doTxn()
* @param {object} Olm.Account object
* @private
*/
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
Expand Down Expand Up @@ -270,9 +279,16 @@ 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;
};

/**
Expand All @@ -283,9 +299,17 @@ 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;
};


Expand All @@ -302,10 +326,15 @@ 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);
});
},
);
};

/**
Expand All @@ -314,11 +343,16 @@ OlmDevice.prototype.markKeysAsPublished = async function() {
* @param {number} numKeys number of keys to generate
* @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();
});
OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
return this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._getAccount(txn, (account) => {
account.generate_one_time_keys(numKeys);
this._storeAccount(txn, account);
});
},
);
};

/**
Expand All @@ -333,26 +367,33 @@ 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;
};


/**
* Generate a new inbound session, given an incoming message
*
* @param {string} theirDeviceIdentityKey remote user's Curve25519 identity key
* @param {number} message_type message_type field from the received message (must be 0)
* @param {number} messageType messageType field from the received message (must be 0)
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {{payload: string, session_id: string}} decrypted payload, and
Expand All @@ -362,32 +403,41 @@ OlmDevice.prototype.createOutboundSession = async function(
* didn't use a valid one-time key).
*/
OlmDevice.prototype.createInboundSession = async function(
theirDeviceIdentityKey, message_type, ciphertext,
theirDeviceIdentityKey, messageType, ciphertext,
) {
if (message_type !== 0) {
throw new Error("Need message_type == 0 to create inbound session");
if (messageType !== 0) {
throw new Error("Need messageType == 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(messageType, 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;
};


Expand Down Expand Up @@ -484,18 +534,18 @@ OlmDevice.prototype.encryptMessage = async function(
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {number} message_type message_type field from the received message
* @param {number} messageType messageType field from the received message
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {Promise<string>} decrypted payload.
*/
OlmDevice.prototype.decryptMessage = async function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext,
theirDeviceIdentityKey, sessionId, messageType, ciphertext,
) {
const self = this;

return this._getSession(theirDeviceIdentityKey, sessionId, function(session) {
const payloadString = session.decrypt(message_type, ciphertext);
const payloadString = session.decrypt(messageType, ciphertext);
self._saveSession(theirDeviceIdentityKey, session);

return payloadString;
Expand All @@ -508,16 +558,16 @@ OlmDevice.prototype.decryptMessage = async function(
* @param {string} theirDeviceIdentityKey Curve25519 identity key for the
* remote device
* @param {string} sessionId the id of the active session
* @param {number} message_type message_type field from the received message
* @param {number} messageType messageType field from the received message
* @param {string} ciphertext base64-encoded body from the received message
*
* @return {Promise<boolean>} true if the received message is a prekey message which matches
* the given session.
*/
OlmDevice.prototype.matchesSession = async function(
theirDeviceIdentityKey, sessionId, message_type, ciphertext,
theirDeviceIdentityKey, sessionId, messageType, ciphertext,
) {
if (message_type !== 0) {
if (messageType !== 0) {
return false;
}

Expand Down
Loading

0 comments on commit 082683b

Please sign in to comment.