Skip to content
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

Add support for olm fallback keys #1467

Merged
merged 15 commits into from
Oct 7, 2020
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"jest-localstorage-mock": "^2.4.3",
"jsdoc": "^3.6.6",
"matrix-mock-request": "^1.2.3",
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
"rimraf": "^3.0.2",
"terser": "^4.8.0",
"tsify": "^4.0.2",
Expand Down
11 changes: 9 additions & 2 deletions spec/unit/crypto/CrossSigningInfo.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,16 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: () => testKey,
});
const [pubKey, ab] = await info.getCrossSigningKey("master", masterKeyPub);
const [pubKey, pkSigning] = await info.getCrossSigningKey("master", masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(ab).toEqual({a: 106712, b: 106712});
// check that the pkSigning object corresponds to the pubKey
const signature = pkSigning.sign("message");
const util = new global.Olm.Utility();
try {
util.ed25519_verify(pubKey, "message", signature);
} finally {
util.free();
}
});

it.each(types)("should request a key from the cache callback (if set)" +
Expand Down
7 changes: 2 additions & 5 deletions src/@types/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/

import * as Olm from "olm";
// this is needed to tell TS about global.Olm
import * as Olm from "olm"; // eslint-disable-line @typescript-eslint/no-unused-vars

export {};

declare global {
namespace NodeJS {
interface Global {
localStorage: Storage;
Olm: Olm;
}
}
interface Global {
Olm: Olm;
}
}
30 changes: 30 additions & 0 deletions src/crypto/OlmDevice.js
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,36 @@ OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
);
};

/**
* Generate a new fallback keys
*
* @return {Promise} Resolved once the account is saved back having generated the key
*/
OlmDevice.prototype.generateFallbackKey = async function() {
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._getAccount(txn, (account) => {
account.generate_fallback_key();
this._storeAccount(txn, account);
});
},
);
};

OlmDevice.prototype.getFallbackKey = async function() {
let result;
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._getAccount(txn, (account) => {
result = JSON.parse(account.fallback_key());
});
},
);
return result;
};

/**
* Generate a new outbound session
*
Expand Down
58 changes: 41 additions & 17 deletions src/crypto/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1859,6 +1859,14 @@ Crypto.prototype.updateOneTimeKeyCount = function(currentCount) {
}
};

Crypto.prototype.setNeedsNewFallback = function(needsNewFallback) {
this._needsNewFallback = !!needsNewFallback;
};

Crypto.prototype.getNeedsNewFallback = function() {
return this._needsNewFallback;
};

// check if it's time to upload one-time keys, and do so if so.
function _maybeUploadOneTimeKeys(crypto) {
// frequency with which to check & upload one-time keys
Expand Down Expand Up @@ -1906,27 +1914,31 @@ function _maybeUploadOneTimeKeys(crypto) {
// out stale private keys that won't receive a message.
const keyLimit = Math.floor(maxOneTimeKeys / 2);

function uploadLoop(keyCount) {
if (keyLimit <= keyCount) {
// If we don't need to generate any more keys then we are done.
return Promise.resolve();
}
async function uploadLoop(keyCount) {
while (keyLimit > keyCount || crypto.getNeedsNewFallback()) {
// Ask olm to generate new one time keys, then upload them to synapse.
if (keyLimit > keyCount) {
logger.info("generating oneTimeKeys");
const keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle);
await crypto._olmDevice.generateOneTimeKeys(keysThisLoop);
}

const keysThisLoop = Math.min(keyLimit - keyCount, maxKeysPerCycle);
if (crypto.getNeedsNewFallback()) {
logger.info("generating fallback key");
await crypto._olmDevice.generateFallbackKey();
}

// Ask olm to generate new one time keys, then upload them to synapse.
return crypto._olmDevice.generateOneTimeKeys(keysThisLoop).then(() => {
return _uploadOneTimeKeys(crypto);
}).then((res) => {
logger.info("calling _uploadOneTimeKeys");
const res = await _uploadOneTimeKeys(crypto);
if (res.one_time_key_counts && res.one_time_key_counts.signed_curve25519) {
// if the response contains a more up to date value use this
// for the next loop
return uploadLoop(res.one_time_key_counts.signed_curve25519);
keyCount = res.one_time_key_counts.signed_curve25519;
} else {
throw new Error("response for uploading keys does not contain "
+ "one_time_key_counts.signed_curve25519");
throw new Error("response for uploading keys does not contain " +
"one_time_key_counts.signed_curve25519");
}
});
}
}

crypto._oneTimeKeyCheckInProgress = true;
Expand Down Expand Up @@ -1958,11 +1970,22 @@ function _maybeUploadOneTimeKeys(crypto) {

// returns a promise which resolves to the response
async function _uploadOneTimeKeys(crypto) {
const promises = [];

const fallbackJson = {};
if (crypto.getNeedsNewFallback()) {
const fallbackKeys = await crypto._olmDevice.getFallbackKey();
for (const [keyId, key] of Object.entries(fallbackKeys.curve25519)) {
const k = { key, fallback: true };
fallbackJson["signed_curve25519:" + keyId] = k;
promises.push(crypto._signObject(k));
}
crypto.setNeedsNewFallback(false);
}

const oneTimeKeys = await crypto._olmDevice.getOneTimeKeys();
const oneTimeJson = {};

const promises = [];

for (const keyId in oneTimeKeys.curve25519) {
if (oneTimeKeys.curve25519.hasOwnProperty(keyId)) {
const k = {
Expand All @@ -1976,7 +1999,8 @@ async function _uploadOneTimeKeys(crypto) {
await Promise.all(promises);

const res = await crypto._baseApis.uploadKeysRequest({
one_time_keys: oneTimeJson,
"one_time_keys": oneTimeJson,
"org.matrix.msc2732.fallback_keys": fallbackJson,
});

await crypto._olmDevice.markKeysAsPublished();
Expand Down
10 changes: 10 additions & 0 deletions src/sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -1361,6 +1361,16 @@ SyncApi.prototype._processSyncResponse = async function(
const currentCount = data.device_one_time_keys_count.signed_curve25519 || 0;
this.opts.crypto.updateOneTimeKeyCount(currentCount);
}
if (this.opts.crypto && data["org.matrix.msc2732.device_unused_fallback_key_types"]) {
// The presence of device_unused_fallback_key_types indicates that the
// server supports fallback keys. If there's no unused
// signed_curve25519 fallback key we need a new one.
const unusedFallbackKeys = data["org.matrix.msc2732.device_unused_fallback_key_types"];
this.opts.crypto.setNeedsNewFallback(
unusedFallbackKeys instanceof Array &&
!unusedFallbackKeys.includes("signed_curve25519"),
);
}
};

/**
Expand Down
6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5411,9 +5411,9 @@ object.values@^1.1.1:
function-bind "^1.1.1"
has "^1.0.3"

"olm@https://packages.matrix.org/npm/olm/olm-3.1.4.tgz":
version "3.1.4"
resolved "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz#0f03128b7d3b2f614d2216409a1dfccca765fdb3"
"olm@https://packages.matrix.org/npm/olm/olm-3.2.1.tgz":
version "3.2.1"
resolved "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz#d623d76f99c3518dde68be8c86618d68bc7b004a"

once@^1.3.0, once@^1.3.1, once@^1.4.0:
version "1.4.0"
Expand Down