-
-
Notifications
You must be signed in to change notification settings - Fork 590
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Change crypto store transaction API #582
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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()); | ||
|
@@ -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 | ||
|
@@ -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); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should we not return the result here and at line 194? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Possibly, but as per other comment it could get quite hairy, so probably simpler to just never do it at all. |
||
} 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 | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. again, can't we pass the result up through the callbacks to save us having to fiddle with a local result? (perhaps it actually ends up being more confusing?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we can, but it could now be quite a few callbacks, and you could be doing more than one get in the transaction, at which point it's not clear which one the transaction promise would return, so I think I'd vote for just making you assign it to a captured variable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the transaction promise would return whatever is returned by the I don't think I agree with you, but I don't feel super strongly about it. |
||
await this._cryptoStore.doTxn( | ||
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT], | ||
(txn) => { | ||
this._getAccount(txn, (account) => { | ||
result = account.sign(message); | ||
}, | ||
); | ||
}); | ||
return result; | ||
}; | ||
|
||
/** | ||
|
@@ -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; | ||
}; | ||
|
||
|
||
|
@@ -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); | ||
}); | ||
}, | ||
); | ||
}; | ||
|
||
/** | ||
|
@@ -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); | ||
}); | ||
}, | ||
); | ||
}; | ||
|
||
/** | ||
|
@@ -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 | ||
|
@@ -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; | ||
}; | ||
|
||
|
||
|
@@ -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; | ||
|
@@ -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; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉