Compare commits

...

105 Commits

Author SHA1 Message Date
RiotRobot 3d20388ca0 v5.2.0 2020-03-30 13:20:20 +01:00
RiotRobot 198c9d934e Prepare changelog for v5.2.0 2020-03-30 13:20:20 +01:00
J. Ryan Stinnett d43005d91e Merge pull request #1290 from matrix-org/dbkr/send_is_verified_rel
Fix isVerified returning false
2020-03-30 10:28:51 +01:00
David Baker adbef16b9d Also pass the parameter in 2020-03-27 14:26:58 +00:00
David Baker 157ea49328 Fix isVerified returning false
which would cause key backups uploads to be missing is_verified
because it was set to `undefined` which would cause the backup to
fail

Fixes https://github.com/vector-im/riot-web/issues/12901
2020-03-27 14:26:53 +00:00
RiotRobot 5a3cc314be v5.2.0-rc.1 2020-03-26 12:55:17 +00:00
RiotRobot 3dfaafd177 Prepare changelog for v5.2.0-rc.1 2020-03-26 12:55:16 +00:00
David Baker bdba61975b Merge pull request #1285 from matrix-org/dbkr/trust_cross_signing_flag
Add a flag for whether cross signing signatures are trusted
2020-03-26 12:19:08 +00:00
David Baker 3b9023ec2b add comment 2020-03-26 12:04:16 +00:00
David Baker 4dfc7958b6 lint 2020-03-26 10:07:17 +00:00
David Baker 2fad318726 Make the flag only affect trust of other people's devices 2020-03-26 09:58:05 +00:00
David Baker 480b0e64a6 lint 2020-03-25 18:44:55 +00:00
David Baker 6ec7b5d404 Add a flag for whether cross signing signatures are trusted 2020-03-25 18:36:08 +00:00
J. Ryan Stinnett 0781d78da8 Merge pull request #1282 from matrix-org/jryans/robust-secret-share
Cache user and self signing keys during bootstrap
2020-03-25 17:50:58 +00:00
Zoe 513a256ec1 Merge pull request #1283 from matrix-org/foldleft/remove-extra-promise
remove unnecessary promise
2020-03-25 12:52:42 +00:00
Zoe 9372790666 remove unnecessary promise 2020-03-25 11:47:59 +00:00
J. Ryan Stinnett a6532b7881 Fix logging lints 2020-03-24 18:34:05 +00:00
J. Ryan Stinnett cea3582ed1 Always attempt caching via bootstrap 2020-03-24 18:28:31 +00:00
J. Ryan Stinnett 6bd22a3e9c Add logging to secret request side 2020-03-24 17:44:44 +00:00
J. Ryan Stinnett 7b93b99054 Cache USK and SSK private key during bootstrap 2020-03-24 17:35:59 +00:00
J. Ryan Stinnett a4b8ba0bb3 Add logging when replying to secret requests 2020-03-24 15:51:35 +00:00
Zoe 02216b15e5 Merge pull request #1281 from matrix-org/foldleft/12704-key-requests
Functions to cache session backups key automatically
2020-03-24 15:32:09 +00:00
David Baker 42efdf1e0a Merge pull request #1279 from matrix-org/dbkr/unify_cross_signing_checks
Add function for checking cross-signing is ready
2020-03-24 13:34:19 +00:00
David Baker 465f9e634e Merge pull request #1272 from matrix-org/dbkr/symmetric-ssss-migrate
Migration to symmetric SSSS
2020-03-24 13:12:17 +00:00
David Baker 7e92f0e5c8 OK, that really is all the comment formatting 2020-03-24 13:08:49 +00:00
David Baker 859a0d8db2 More comment formatting 2020-03-24 13:08:12 +00:00
David Baker 71740cabb5 comment formatting 2020-03-24 13:06:08 +00:00
David Baker 8f77680750 Typo
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-03-24 13:05:15 +00:00
David Baker 509e4b337d Update for new name 2020-03-24 13:01:46 +00:00
David Baker 942ff0c9fd Better name
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-03-24 13:00:53 +00:00
David Baker 24c3dd1f1a Merge pull request #1280 from matrix-org/uhoreg/reduce_olm_creation
reduce number of one-time-key requests
2020-03-24 10:30:23 +00:00
Hubert Chathi 4f58e9945b factor out failed device notif to a function, and record all failed devices
instead of filtering out already-notified devices
2020-03-24 00:15:04 -04:00
Hubert Chathi 547ded9155 handle failed devices that we aren't going to retry 2020-03-23 23:14:36 -04:00
Hubert Chathi 4f112e8379 only re-try creating olm sessions for servers that failed to respond
If the server responded, then retrying likely won't help.  Retrying is mainly
to help with slow servers.
2020-03-23 22:36:10 -04:00
Hubert Chathi 4d63f8ed04 don't always do second phase of olm creation
don't need to do the shorter timeout when doing preparation to encrypt, and
skip the second phase if the first phase already took longer than a normal
otk claim
2020-03-23 21:26:56 -04:00
Hubert Chathi 944d39c836 add some comments 2020-03-23 16:51:44 -04:00
Bruno Windels 433977b918 Merge pull request #1275 from matrix-org/bwindels/assumemethodswhentodevice
Fix: assume the requested method is supported by other party with to_device
2020-03-23 19:39:27 +00:00
David Baker d9796e3bec Fix indenting 2020-03-23 19:00:02 +00:00
David Baker 0a7b9109f0 Move aes functions to their own file 2020-03-23 18:56:32 +00:00
David Baker 89bf9ff65b doc style fix 2020-03-23 18:40:53 +00:00
David Baker 7f6e223c0c Add function for checking cross-signing is ready
Aggregate function that checks the various things are in place for
cross-signing to work.
2020-03-23 18:34:16 +00:00
David Baker c696e5238b Merge pull request #1278 from matrix-org/dbkr/blacklist_use_device_trust
Use checkDeviceTrust when computing untrusted devices
2020-03-23 14:58:06 +00:00
David Baker d303fd0c7c Fix test 2020-03-23 14:53:55 +00:00
David Baker e1ad2f8a21 Use checkDeviceTrust when computing untrusted devices
Apparently we missed using cross-signing trust in the js-sdk itself
2020-03-23 14:28:10 +00:00
Zoe 7053cf0182 Functions to cache session backups key automatically 2020-03-23 14:24:35 +00:00
Bruno Windels 4bd09c45a0 assume the requested method is supported by other party during to_device verification 2020-03-20 13:29:29 +01:00
Zoe 6a7a255081 Merge pull request #1271 from matrix-org/foldleft/12704-key-storage
Rename ssss cache functions to be more general
2020-03-20 11:17:21 +00:00
Zoe 6701fdd486 Rename ssss cache functions to be more general 2020-03-20 10:18:06 +00:00
David Baker ddce14b20b Use the typeof test to avoid undefined 2020-03-19 21:12:57 +00:00
David Baker f1317e824b Don't assume subtleCrypto exists if there's a window
Jest has a window object but doesn't have subtleCrypto
2020-03-19 21:04:36 +00:00
David Baker db285af0b5 Add callback to get the user's current key backup passphrase
And also add a null check
2020-03-19 20:36:00 +00:00
David Baker 0434bf5a48 Add functions to get the raw key backup key 2020-03-19 20:34:57 +00:00
Zoe 78d9111646 Add a store for backup keys 2020-03-19 15:30:28 +00:00
J. Ryan Stinnett 0f28a89c52 Merge pull request #1268 from matrix-org/jryans/send-only-new-key-sigs
Upload only new device signature of master key
2020-03-19 14:56:29 +00:00
Hubert Chathi 92db6599d8 Merge pull request #1270 from matrix-org/uhoreg/expose_prepare_to_encrypt
expose prepareToEncrypt in the client API
2020-03-19 10:56:16 -04:00
Hubert Chathi 70fb5dcaa4 Merge pull request #1269 from matrix-org/uhoreg/device_list_no_dying
don't kill the whole device download if one device gives an error
2020-03-19 10:56:00 -04:00
David Baker a265574da1 Merge remote-tracking branch 'origin/develop' into dbkr/symmetric-ssss-migrate 2020-03-19 14:27:25 +00:00
Hubert Chathi 9911766435 expose prepareToEncrypt in the client API 2020-03-18 18:53:26 -04:00
Hubert Chathi fb08ef9a9b don't kill the whole device download if one device gives an error 2020-03-18 15:28:54 -04:00
J. Ryan Stinnett 2fab06111c Upload only new device signature of master key
This changes bootstrap to only upload the new device signature of the master
key. We were previously _adding_ the new signature, but then uploading both old
and new device key signatures of the master key.

This was particularly bad when re-uploading signatures from deleted devices, as
that would cause the homeserver to reject the entire upload.

Fixes https://github.com/vector-im/riot-web/issues/12752
2020-03-18 18:35:37 +00:00
Bruno Windels 11e3b1ab53 Merge pull request #1267 from matrix-org/bwindels/handleselfverifstartrace
handle racing .start event during self verification
2020-03-18 14:06:45 +00:00
Zoe 3c78f7dbe1 Merge pull request #1266 from matrix-org/foldleft/fix-label-error
A crypto.keySignatureUploadFailure event reported the wrong source
2020-03-18 11:21:16 +00:00
Bruno Windels 999cebc304 handle racing .start event during self verification
by comparing the device id rather than the user id, as defined in the MSC
2020-03-17 17:51:32 +01:00
RiotRobot b2e154377a Merge branch 'master' into develop 2020-03-17 14:09:49 +00:00
RiotRobot d5c68139c0 v5.1.1 2020-03-17 14:07:01 +00:00
RiotRobot cbde77a5cd Prepare changelog for v5.1.1 2020-03-17 14:07:00 +00:00
David Baker 8120041ba7 Merge branch 'symmetric-ssss-migrate' of git://github.com/uhoreg/matrix-js-sdk into uhoreg-symmetric-ssss-migrate 2020-03-17 13:11:01 +00:00
Michael Telatynski 68bc8edaae Merge pull request #1263 from matrix-org/t3chguy/fix_editing
Fix editing of unsent messages by waiting for actual event id
2020-03-17 13:00:55 +00:00
Zoe 7ec339985a a crypto.keySignatureUploadFailure event reported the wrong source 2020-03-17 11:42:03 +00:00
Bruno Windels 70c0abaef8 Merge pull request #1265 from matrix-org/bwindels/fixolmapierror-release
Fix: ensureOlmSessionsForDevices parameter format
2020-03-17 11:25:59 +00:00
Bruno Windels d4dcac93b1 devicesByUser should be userId => array of devices 2020-03-17 12:21:56 +01:00
Michael Telatynski 43889cfb31 use async/await instead
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-03-17 11:14:25 +00:00
Bruno Windels 9e4e14802d Merge pull request #1264 from matrix-org/bwindels/fixolmapierror
Fix: ensureOlmSessionsForDevices parameter format
2020-03-17 11:08:23 +00:00
Bruno Windels 9bebb22746 devicesByUser should be userId => array of devices 2020-03-17 09:51:28 +01:00
Hubert Chathi 3b06b0ffc1 fix lint 2020-03-16 17:22:12 -04:00
Hubert Chathi 1b24d55b24 misc fixes and cleanups 2020-03-16 17:20:54 -04:00
Hubert Chathi c8c6444f6a migrate backup key from asymmetric SSSS to symmetric SSSS 2020-03-16 11:05:07 -04:00
Hubert Chathi 45a88f0517 add function to check that secret storage needs upgrading 2020-03-16 11:00:11 -04:00
Michael Telatynski 53cb3ca79b return the additional promise to simplify the rejection chain
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-03-16 12:23:13 +00:00
Michael Telatynski 68526284f1 fix rejection handling
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-03-16 10:34:39 +00:00
Zoe 68cebc7ff9 If a key upload fails, throw an error and emit an event (#1254) 2020-03-16 10:24:31 +00:00
Michael Telatynski 38286b74e3 tidy up
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-03-16 10:10:22 +00:00
Michael Telatynski 86f56082f0 Make use of scheduler instead of an additional promise
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-03-16 10:09:17 +00:00
Michael Telatynski e87bbfc535 Fix editing of unsent messages by waiting for actual event id
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2020-03-16 09:29:37 +00:00
Travis Ralston 758e12d6dd Merge pull request #1261 from matrix-org/travis/yarn-cleanup
Remove stuff that yarn install doesn't think we need
2020-03-13 09:25:23 -06:00
Bruno Windels bff461081a Merge pull request #1262 from matrix-org/bwindels/nullcheckonreceipts-release
Fix: prevent error being thrown during sync in some cases
2020-03-13 12:47:52 +00:00
Bruno Windels 33d36395aa check if push actions has a tweaks object 2020-03-13 13:41:32 +01:00
Hubert Chathi e373508211 some fixes in SSSS migration 2020-03-12 18:08:54 -04:00
Bruno Windels 9051edad37 Merge pull request #1258 from matrix-org/bwindels/nullcheckonreceipts
Fix: prevent error being thrown during sync in some cases
2020-03-12 17:09:10 +00:00
Travis Ralston 678b268008 Remove stuff that yarn install doesn't think we need 2020-03-12 10:44:52 -06:00
J. Ryan Stinnett 0361bcf94f Merge pull request #1260 from matrix-org/jryans/verified-to-bool-release
Force `is_verified` for key backups to bool and fix computation
2020-03-12 15:48:07 +00:00
J. Ryan Stinnett b1f02d30c1 Check key backup trust for the right user ID
This corrects the key backup trust computation so that we use the user ID for
the device we're checking inside of always using the client's main user ID,
which would always resulted in false for other people.

Fixes https://github.com/vector-im/riot-web/issues/12693
2020-03-12 15:42:14 +00:00
J. Ryan Stinnett 2af0e5b176 Convert trustedLocally to a bool in all cases
This ensure we always have a boolean value, even when device is null.

Part of https://github.com/vector-im/riot-web/issues/12693
2020-03-12 15:42:14 +00:00
J. Ryan Stinnett c204812d9c Merge pull request #1259 from matrix-org/jryans/verified-to-bool
Force `is_verified` for key backups to bool and fix computation
2020-03-12 15:39:26 +00:00
J. Ryan Stinnett 3b7def880f Check key backup trust for the right user ID
This corrects the key backup trust computation so that we use the user ID for
the device we're checking inside of always using the client's main user ID,
which would always resulted in false for other people.

Fixes https://github.com/vector-im/riot-web/issues/12693
2020-03-12 14:47:28 +00:00
J. Ryan Stinnett e5ec2f03c2 Convert trustedLocally to a bool in all cases
This ensure we always have a boolean value, even when device is null.

Part of https://github.com/vector-im/riot-web/issues/12693
2020-03-12 14:21:46 +00:00
Bruno Windels a1b3e8055f check if push actions has a tweaks object 2020-03-12 12:59:43 +01:00
Bruno Windels 1e503261f2 Merge pull request #1257 from matrix-org/bwindels/devicelegacyverif
Add a method for legacy single device verification, returning a verification request
2020-03-12 11:30:08 +00:00
David Baker 9107a3e569 Merge pull request #1256 from matrix-org/dbkr/yarn_upgrade_20200311
yarn upgrade
2020-03-12 09:44:51 +00:00
Bruno Windels b66a1d30a0 method for legacy single device verification, returning a verification request rather than a verifier 2020-03-11 15:53:38 +01:00
David Baker 51e1f56873 yarn upgrade 2020-03-11 14:47:48 +00:00
Hubert Chathi c6b5936f8a use the right operator 2020-02-28 16:09:24 -05:00
Hubert Chathi 1151bdc6db initial work in migrating ssss to symmetric 2020-02-27 22:56:34 -05:00
Hubert Chathi ed223d1d76 remove unnecessary awaits 2020-02-27 22:54:43 -05:00
Hubert Chathi 5d52053caa use symmetric encryption for SSSS 2020-02-24 17:38:53 -05:00
24 changed files with 1550 additions and 776 deletions
+72
View File
@@ -1,3 +1,75 @@
Changes in [5.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.2.0) (2020-03-30)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.2.0-rc.1...v5.2.0)
* Fix isVerified returning false
[\#1290](https://github.com/matrix-org/matrix-js-sdk/pull/1290)
Changes in [5.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.2.0-rc.1) (2020-03-26)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.1...v5.2.0-rc.1)
* Add a flag for whether cross signing signatures are trusted
[\#1285](https://github.com/matrix-org/matrix-js-sdk/pull/1285)
* Cache user and self signing keys during bootstrap
[\#1282](https://github.com/matrix-org/matrix-js-sdk/pull/1282)
* remove unnecessary promise
[\#1283](https://github.com/matrix-org/matrix-js-sdk/pull/1283)
* Functions to cache session backups key automatically
[\#1281](https://github.com/matrix-org/matrix-js-sdk/pull/1281)
* Add function for checking cross-signing is ready
[\#1279](https://github.com/matrix-org/matrix-js-sdk/pull/1279)
* Use symmetric encryption for SSSS
[\#1228](https://github.com/matrix-org/matrix-js-sdk/pull/1228)
* Migrate SSSS to use symmetric algorithm
[\#1238](https://github.com/matrix-org/matrix-js-sdk/pull/1238)
* Migration to symmetric SSSS
[\#1272](https://github.com/matrix-org/matrix-js-sdk/pull/1272)
* Reduce number of one-time-key requests
[\#1280](https://github.com/matrix-org/matrix-js-sdk/pull/1280)
* Fix: assume the requested method is supported by other party with to_device
[\#1275](https://github.com/matrix-org/matrix-js-sdk/pull/1275)
* Use checkDeviceTrust when computing untrusted devices
[\#1278](https://github.com/matrix-org/matrix-js-sdk/pull/1278)
* Add a store for backup keys
[\#1271](https://github.com/matrix-org/matrix-js-sdk/pull/1271)
* Upload only new device signature of master key
[\#1268](https://github.com/matrix-org/matrix-js-sdk/pull/1268)
* Expose prepareToEncrypt in the client API
[\#1270](https://github.com/matrix-org/matrix-js-sdk/pull/1270)
* Don't kill the whole device download if one device gives an error
[\#1269](https://github.com/matrix-org/matrix-js-sdk/pull/1269)
* Handle racing .start event during self verification
[\#1267](https://github.com/matrix-org/matrix-js-sdk/pull/1267)
* A crypto.keySignatureUploadFailure event reported the wrong source
[\#1266](https://github.com/matrix-org/matrix-js-sdk/pull/1266)
* Fix editing of unsent messages by waiting for actual event id
[\#1263](https://github.com/matrix-org/matrix-js-sdk/pull/1263)
* Fix: ensureOlmSessionsForDevices parameter format
[\#1264](https://github.com/matrix-org/matrix-js-sdk/pull/1264)
* Remove stuff that yarn install doesn't think we need
[\#1261](https://github.com/matrix-org/matrix-js-sdk/pull/1261)
* Fix: prevent error being thrown during sync in some cases
[\#1258](https://github.com/matrix-org/matrix-js-sdk/pull/1258)
* Force `is_verified` for key backups to bool and fix computation
[\#1259](https://github.com/matrix-org/matrix-js-sdk/pull/1259)
* Add a method for legacy single device verification, returning a verification
request
[\#1257](https://github.com/matrix-org/matrix-js-sdk/pull/1257)
* yarn upgrade
[\#1256](https://github.com/matrix-org/matrix-js-sdk/pull/1256)
Changes in [5.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.1) (2020-03-17)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.1-rc.1...v5.1.1)
* Fix: ensureOlmSessionsForDevices parameter format
[\#1265](https://github.com/matrix-org/matrix-js-sdk/pull/1265)
* Fix: prevent error being thrown during sync in some cases
[\#1262](https://github.com/matrix-org/matrix-js-sdk/pull/1262)
* Force `is_verified` for key backups to bool and fix computation
[\#1260](https://github.com/matrix-org/matrix-js-sdk/pull/1260)
Changes in [5.1.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.1-rc.1) (2020-03-11)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.0...v5.1.1-rc.1)
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "5.1.1-rc.1",
"version": "5.2.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepare": "yarn build",
@@ -217,6 +217,7 @@ describe.each([
["MemoryCryptoStore", () => {
const store = new IndexedDBCryptoStore(undefined, "tests");
store._backend = new MemoryCryptoStore();
store._backendPromise = Promise.resolve(store._backend);
return store;
}],
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function(name, dbFactory) {
@@ -297,6 +297,10 @@ describe("MegolmDecryption", function() {
},
}));
mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false,
});
const megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
+26
View File
@@ -541,5 +541,31 @@ describe("MegolmBackup", function() {
expect(res.clearEvent.content).toEqual('testytest');
});
});
it('has working cache functions', async function() {
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
await client._crypto.storeSessionBackupPrivateKey(key);
const result = await client._crypto.getSessionBackupPrivateKey();
expect(result).toEqual(key);
});
it('caches session backup keys as it encounters them', async function() {
const cachedNull = await client._crypto.getSessionBackupPrivateKey();
expect(cachedNull).toBeNull();
client._http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
};
await new Promise((resolve) => {
client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BACKUP_INFO,
{ cacheCompleteCallback: resolve },
);
});
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
expect(cachedKey).not.toBeNull();
});
});
});
+36 -10
View File
@@ -16,11 +16,20 @@ limitations under the License.
import '../../olm-loader';
import * as olmlib from "../../../src/crypto/olmlib";
import {SECRET_STORAGE_ALGORITHM_V1} from "../../../src/crypto/SecretStorage";
import {SECRET_STORAGE_ALGORITHM_V1_AES} from "../../../src/crypto/SecretStorage";
import {MatrixEvent} from "../../../src/models/event";
import {TestClient} from '../../TestClient';
import {makeTestClients} from './verification/util';
import * as utils from "../../../src/utils";
try {
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
console.log('nodejs was compiled without crypto support');
}
async function makeTestClient(userInfo, options) {
const client = (new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
@@ -51,9 +60,8 @@ describe("Secrets", function() {
});
it("should store and retrieve a secret", async function() {
const decryption = new global.Olm.PkDecryption();
const pubkey = decryption.generate_key();
const privkey = decryption.get_private_key();
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const signing = new global.Olm.PkSigning();
const signingKey = signing.generate_seed();
@@ -69,7 +77,7 @@ describe("Secrets", function() {
const getKey = jest.fn(e => {
expect(Object.keys(e.keys)).toEqual(["abc"]);
return ['abc', privkey];
return ['abc', key];
});
const alice = await makeTestClient(
@@ -100,8 +108,7 @@ describe("Secrets", function() {
};
const keyAccountData = {
algorithm: SECRET_STORAGE_ALGORITHM_V1,
pubkey: pubkey,
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
};
await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master');
@@ -112,11 +119,11 @@ describe("Secrets", function() {
}),
]);
expect(await secretStorage.isStored("foo")).toBe(false);
expect(await secretStorage.isStored("foo")).toBeFalsy();
await secretStorage.store("foo", "bar", ["abc"]);
expect(await secretStorage.isStored("foo")).toBe(true);
expect(await secretStorage.isStored("foo")).toBeTruthy();
expect(await secretStorage.get("foo")).toBe("bar");
expect(getKey).toHaveBeenCalled();
@@ -149,6 +156,13 @@ describe("Secrets", function() {
});
it("should encrypt with default key if keys is null", async function() {
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const getKey = jest.fn(e => {
expect(Object.keys(e.keys)).toEqual([newKeyId]);
return [newKeyId, key];
});
let keys = {};
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
@@ -156,6 +170,7 @@ describe("Secrets", function() {
cryptoCallbacks: {
getCrossSigningKey: t => keys[t],
saveCrossSigningKeys: k => keys = k,
getSecretStorageKey: getKey,
},
},
);
@@ -170,7 +185,7 @@ describe("Secrets", function() {
alice.resetCrossSigningKeys();
const newKeyId = await alice.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1,
SECRET_STORAGE_ALGORITHM_V1_AES,
);
// we don't await on this because it waits for the event to come down the sync
// which won't happen in the test setup
@@ -252,11 +267,22 @@ describe("Secrets", function() {
});
it("bootstraps when no storage or cross-signing keys locally", async function() {
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const getKey = jest.fn(e => {
return [Object.keys(e.keys)[0], key];
});
const bob = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
{
cryptoCallbacks: {
getSecretStorageKey: getKey,
},
},
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
+110 -19
View File
@@ -217,8 +217,10 @@ function keyFromRecoverySession(session, decryptionKey) {
* Args:
* {object} keys Information about the keys:
* {
* <key name>: {
* pubkey: {UInt8Array}
* keys: {
* <key name>: {
* pubkey: {UInt8Array}
* }, ...
* }
* }
* {string} name the name of the value we want to read out of SSSS, for UI purposes.
@@ -281,13 +283,19 @@ export function MatrixClient(opts) {
this.scheduler = opts.scheduler;
if (this.scheduler) {
const self = this;
this.scheduler.setProcessFunction(function(eventToSend) {
this.scheduler.setProcessFunction(async function(eventToSend) {
const room = self.getRoom(eventToSend.getRoomId());
if (eventToSend.status !== EventStatus.SENDING) {
_updatePendingEventStatus(room, eventToSend,
EventStatus.SENDING);
}
return _sendEventHttpRequest(self, eventToSend);
const res = await _sendEventHttpRequest(self, eventToSend);
if (room) {
// ensure we update pending event before the next scheduler run so that any listeners to event id
// updates on the synchronous event emitter get a chance to run first.
room.updatePendingEvent(eventToSend, EventStatus.SENT, res.event_id);
}
return res;
});
}
this.clientRunning = false;
@@ -407,9 +415,9 @@ export function MatrixClient(opts) {
break;
}
highlightCount += this.getPushActionsForEvent(
event,
).tweaks.highlight ? 1 : 0;
const pushActions = this.getPushActionsForEvent(event);
highlightCount += pushActions.tweaks &&
pushActions.tweaks.highlight ? 1 : 0;
}
// Note: we don't need to handle 'total' notifications because the counts
@@ -752,7 +760,6 @@ MatrixClient.prototype.isCryptoEnabled = function() {
return this._crypto !== null;
};
/**
* Get the Ed25519 key for this device
*
@@ -1127,6 +1134,12 @@ function wrapCryptoFuncs(MatrixClient, names) {
* @returns {boolean} true if the key matches, otherwise false
*/
/**
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
*
* @param {module:models/room} room the room the event is in
*/
wrapCryptoFuncs(MatrixClient, [
"resetCrossSigningKeys",
"getCrossSigningId",
@@ -1135,6 +1148,11 @@ wrapCryptoFuncs(MatrixClient, [
"checkDeviceTrust",
"checkOwnCrossSigningTrust",
"checkCrossSigningPrivateKey",
"legacyDeviceVerification",
"prepareToEncrypt",
"isCrossSigningReady",
"getCryptoTrustCrossSignedDevices",
"setCryptoTrustCrossSignedDevices",
]);
/**
@@ -1233,7 +1251,9 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
* @param {boolean} checkKey check if the secret is encrypted by a trusted
* key
*
* @return {boolean} whether or not the secret is stored
* @return {object?} map of key name to key info the secret is encrypted
* with, or null if it is not present or not encrypted with a trusted
* key
*/
/**
@@ -1281,6 +1301,7 @@ wrapCryptoFuncs(MatrixClient, [
"bootstrapSecretStorage",
"addSecretStorageKey",
"hasSecretStorageKey",
"secretStorageKeyNeedsUpgrade",
"storeSecret",
"getSecret",
"isSecretStored",
@@ -1567,7 +1588,9 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(
/**
* Check whether the key backup private key is stored in secret storage.
* @return {Promise<boolean>} Whether the backup key is stored.
* @return {Promise<object?>} map of key name to key info the secret is
* encrypted with, or null if it is not present or not encrypted with a
* trusted key
*/
MatrixClient.prototype.isKeyBackupKeyStored = async function() {
return this.isSecretStored("m.megolm_backup.v1", false /* checkKey */);
@@ -1720,6 +1743,35 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
}
};
/**
* Get the raw key for a key backup from the password
* Used when migrating key backups into SSSS
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @param {string} password Passphrase
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @return {Promise<Buffer>} key backup key
*/
MatrixClient.prototype.keyBackupKeyFromPassword = function(
password, backupInfo,
) {
return keyFromAuthData(backupInfo.auth_data, password);
};
/**
* Get the raw key for a key backup from the recovery key
* Used when migrating key backups into SSSS
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @param {string} recoveryKey The recovery key
* @return {Buffer} key backup key
*/
MatrixClient.prototype.keyBackupKeyFromRecoveryKey = function(recoveryKey) {
return decodeRecoveryKey(recoveryKey);
};
MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
/**
@@ -1731,15 +1783,16 @@ MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @param {object} opts Optional params such as callbacks
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
password, targetRoomId, targetSessionId, backupInfo,
password, targetRoomId, targetSessionId, backupInfo, opts,
) {
const privKey = await keyFromAuthData(backupInfo.auth_data, password);
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, backupInfo,
privKey, targetRoomId, targetSessionId, backupInfo, opts,
);
};
@@ -1752,15 +1805,16 @@ MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
* Restores all rooms if omitted.
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} opts Optional params such as callbacks
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithSecretStorage = async function(
backupInfo, targetRoomId, targetSessionId,
backupInfo, targetRoomId, targetSessionId, opts,
) {
const privKey = decodeBase64(await this.getSecret("m.megolm_backup.v1"));
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, backupInfo,
privKey, targetRoomId, targetSessionId, backupInfo, opts,
);
};
@@ -1773,20 +1827,47 @@ MatrixClient.prototype.restoreKeyBackupWithSecretStorage = async function(
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @param {object} opts Optional params such as callbacks
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function(
recoveryKey, targetRoomId, targetSessionId, backupInfo,
recoveryKey, targetRoomId, targetSessionId, backupInfo, opts,
) {
const privKey = decodeRecoveryKey(recoveryKey);
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, backupInfo,
privKey, targetRoomId, targetSessionId, backupInfo, opts,
);
};
/**
* Restore from an existing key backup using a cached key, or fail
*
* @param {string} [targetRoomId] Room ID to target a specific room.
* Restores all rooms if omitted.
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @param {object} opts Optional params such as callbacks
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithCache = async function(
targetRoomId, targetSessionId, backupInfo, opts,
) {
const privKey = await this._crypto.getSessionBackupPrivateKey();
if (!privKey) {
throw new Error("Couldn't get key");
}
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, backupInfo, opts,
);
};
MatrixClient.prototype._restoreKeyBackup = function(
privKey, targetRoomId, targetSessionId, backupInfo,
{ cacheCompleteCallback }={}, // For sequencing during tests
) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
@@ -1814,6 +1895,13 @@ MatrixClient.prototype._restoreKeyBackup = function(
return Promise.reject({errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY});
}
// Cache the key, if possible.
// This is async.
this._crypto.storeSessionBackupPrivateKey(privKey)
.catch((e) => {
console.warn("Error caching session backup key:", e);
}).then(cacheCompleteCallback);
return this._http.authedRequest(
undefined, "GET", path.path, path.queryData, undefined,
{prefix: PREFIX_UNSTABLE},
@@ -2425,12 +2513,15 @@ function _sendEvent(client, room, event, callback) {
if (!promise) {
promise = _sendEventHttpRequest(client, event);
if (room) {
promise = promise.then(res => {
room.updatePendingEvent(event, EventStatus.SENT, res.event_id);
return res;
});
}
}
return promise;
}).then(function(res) { // the request was sent OK
if (room) {
room.updatePendingEvent(event, EventStatus.SENT, res.event_id);
}
if (callback) {
callback(null, res);
}
+39 -14
View File
@@ -141,14 +141,28 @@ export class CrossSigningInfo extends EventEmitter {
* want to know this anyway...
*
* @param {SecretStorage} secretStorage The secret store using account data
* @returns {boolean} Whether all private keys were found in storage
* @returns {object} map of key name to key info the secret is encrypted
* with, or null if it is not present or not encrypted with a trusted
* key
*/
async isStoredInSecretStorage(secretStorage) {
let stored = true;
for (const type of ["master", "self_signing", "user_signing"]) {
stored &= await secretStorage.isStored(`m.cross_signing.${type}`, false);
// check what SSSS keys have encrypted the master key (if any)
const stored =
await secretStorage.isStored("m.cross_signing.master", false) || {};
// then check which of those SSSS keys have also encrypted the SSK and USK
function intersect(s) {
for (const k of Object.keys(stored)) {
if (!s[k]) {
delete stored[k];
}
}
}
return stored;
for (const type of ["self_signing", "user_signing"]) {
intersect(
await secretStorage.isStored(`m.cross_signing.${type}`, false) || {},
);
}
return Object.keys(stored).length ? stored : null;
}
/**
@@ -446,17 +460,20 @@ export class CrossSigningInfo extends EventEmitter {
* @param {CrossSigningInfo} userCrossSigning Cross signing info for user
* @param {module:crypto/deviceinfo} device The device to check
* @param {bool} localTrust Whether the device is trusted locally
* @param {bool} trustCrossSignedDevices Whether we trust cross signed devices
*
* @returns {DeviceTrustLevel}
*/
checkDeviceTrust(userCrossSigning, device, localTrust) {
checkDeviceTrust(userCrossSigning, device, localTrust, trustCrossSignedDevices) {
const userTrust = this.checkUserTrust(userCrossSigning);
const userSSK = userCrossSigning.keys.self_signing;
if (!userSSK) {
// if the user has no self-signing key then we cannot make any
// trust assertions about this device from cross-signing
return new DeviceTrustLevel(false, false, localTrust);
return new DeviceTrustLevel(
false, false, localTrust, trustCrossSignedDevices,
);
}
const deviceObj = deviceToObject(device, userCrossSigning.userId);
@@ -468,9 +485,13 @@ export class CrossSigningInfo extends EventEmitter {
deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId,
);
// ...then we trust this device as much as far as we trust the user
return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust);
return DeviceTrustLevel.fromUserTrustLevel(
userTrust, localTrust, trustCrossSignedDevices,
);
} catch (e) {
return new DeviceTrustLevel(false, false, localTrust);
return new DeviceTrustLevel(
false, false, localTrust, trustCrossSignedDevices,
);
}
}
@@ -533,17 +554,19 @@ export class UserTrustLevel {
* Represents the ways in which we trust a device
*/
export class DeviceTrustLevel {
constructor(crossSigningVerified, tofu, localVerified) {
constructor(crossSigningVerified, tofu, localVerified, trustCrossSignedDevices) {
this._crossSigningVerified = crossSigningVerified;
this._tofu = tofu;
this._localVerified = localVerified;
this._trustCrossSignedDevices = trustCrossSignedDevices;
}
static fromUserTrustLevel(userTrustLevel, localVerified) {
static fromUserTrustLevel(userTrustLevel, localVerified, trustCrossSignedDevices) {
return new DeviceTrustLevel(
userTrustLevel._crossSigningVerified,
userTrustLevel._tofu,
localVerified,
trustCrossSignedDevices,
);
}
@@ -551,7 +574,9 @@ export class DeviceTrustLevel {
* @returns {bool} true if this device is verified via any means
*/
isVerified() {
return this.isCrossSigningVerified() || this.isLocallyVerified();
return Boolean(this.isLocallyVerified() || (
this._trustCrossSignedDevices && this.isCrossSigningVerified()
));
}
/**
@@ -585,7 +610,7 @@ export function createCryptoStoreCacheCallbacks(store) {
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
store.getCrossSigningPrivateKey(txn, resolve, type);
store.getSecretStorePrivateKey(txn, resolve, type);
},
);
});
@@ -595,7 +620,7 @@ export function createCryptoStoreCacheCallbacks(store) {
'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
store.storeCrossSigningPrivateKey(txn, type, key);
store.storeSecretStorePrivateKey(txn, type, key);
},
);
},
+41 -17
View File
@@ -317,6 +317,15 @@ export class DeviceList extends EventEmitter {
return stored;
}
/**
* Returns a list of all user IDs the DeviceList knows about
*
* @return {array} All known user IDs
*/
getKnownUserIds() {
return Object.keys(this._devices);
}
/**
* Get the stored device keys for a user id
*
@@ -379,6 +388,26 @@ export class DeviceList extends EventEmitter {
return DeviceInfo.fromStorage(devs[deviceId], deviceId);
}
/**
* Get a user ID by one of their device's curve25519 identity key
*
* @param {string} algorithm encryption algorithm
* @param {string} senderKey curve25519 key to match
*
* @return {string} user ID
*/
getUserByIdentityKey(algorithm, senderKey) {
if (
algorithm !== olmlib.OLM_ALGORITHM &&
algorithm !== olmlib.MEGOLM_ALGORITHM
) {
// we only deal in olm keys
return null;
}
return this._userByIdentityKey[senderKey];
}
/**
* Find a device by curve25519 identity key
*
@@ -388,19 +417,11 @@ export class DeviceList extends EventEmitter {
* @return {module:crypto/deviceinfo?}
*/
getDeviceByIdentityKey(algorithm, senderKey) {
const userId = this._userByIdentityKey[senderKey];
const userId = this.getUserByIdentityKey(algorithm, senderKey);
if (!userId) {
return null;
}
if (
algorithm !== olmlib.OLM_ALGORITHM &&
algorithm !== olmlib.MEGOLM_ALGORITHM
) {
// we only deal in olm keys
return null;
}
const devices = this._devices[userId];
if (!devices) {
return null;
@@ -755,31 +776,34 @@ class DeviceListUpdateSerialiser {
this._baseApis.downloadKeysForUsers(
downloadUsers, opts,
).then((res) => {
).then(async (res) => {
const dk = res.device_keys || {};
const masterKeys = res.master_keys || {};
const ssks = res.self_signing_keys || {};
const usks = res.user_signing_keys || {};
// do each user in a separate promise, to avoid wedging the CPU
// yield to other things that want to execute in between users, to
// avoid wedging the CPU
// (https://github.com/vector-im/riot-web/issues/3158)
//
// of course we ought to do this in a web worker or similar, but
// this serves as an easy solution for now.
let prom = Promise.resolve();
for (const userId of downloadUsers) {
prom = prom.then(sleep(5)).then(() => {
return this._processQueryResponseForUser(
await sleep(5);
try {
await this._processQueryResponseForUser(
userId, dk[userId], {
master: masterKeys[userId],
self_signing: ssks[userId],
user_signing: usks[userId],
},
);
});
} catch (e) {
// log the error but continue, so that one bad key
// doesn't kill the whole process
logger.error(`Error processing keys for ${userId}:`, e);
}
}
return prom;
}).then(() => {
logger.log('Completed key download for ' + downloadUsers);
+141 -103
View File
@@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -19,8 +19,13 @@ import {logger} from '../logger';
import * as olmlib from './olmlib';
import {pkVerify} from './olmlib';
import {randomString} from '../randomstring';
import {encryptAES, decryptAES} from './aes';
export const SECRET_STORAGE_ALGORITHM_V1 = "m.secret_storage.v1.curve25519-aes-sha2";
export const SECRET_STORAGE_ALGORITHM_V1_AES
= "m.secret_storage.v1.aes-hmac-sha2";
// don't use curve25519 for writing data.
export const SECRET_STORAGE_ALGORITHM_V1_CURVE25519
= "m.secret_storage.v1.curve25519-aes-sha2";
/**
* Implements Secure Secret Storage and Sharing (MSC1946)
@@ -85,20 +90,12 @@ export class SecretStorage extends EventEmitter {
}
switch (algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
case SECRET_STORAGE_ALGORITHM_V1_AES:
{
const decryption = new global.Olm.PkDecryption();
try {
const { passphrase, pubkey } = opts;
// Copies in public key details of the form generated by
// the Crypto module's `createRecoveryKeyFromPassphrase`.
if (passphrase && pubkey) {
keyData.passphrase = passphrase;
keyData.pubkey = pubkey;
} else if (pubkey) {
keyData.pubkey = pubkey;
} else {
keyData.pubkey = decryption.generate_key();
if (opts.passphrase) {
keyData.passphrase = opts.passphrase;
}
} finally {
decryption.free();
@@ -155,6 +152,28 @@ export class SecretStorage extends EventEmitter {
);
}
/**
* Get the key information for a given ID.
*
* @param {string} [keyId = default key's ID] The ID of the key to check
* for. Defaults to the default key ID if not provided.
* @returns {Array?} If the key was found, the return value is an array of
* the form [keyId, keyInfo]. Otherwise, null is returned.
*/
async getKey(keyId) {
if (!keyId) {
keyId = await this.getDefaultKeyId();
}
if (!keyId) {
return null;
}
const keyInfo = await this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
return keyInfo ? [keyId, keyInfo] : null;
}
/**
* Check whether we have a key with a given ID.
*
@@ -163,16 +182,16 @@ export class SecretStorage extends EventEmitter {
* @return {boolean} Whether we have the key.
*/
async hasKey(keyId) {
if (!keyId) {
keyId = await this.getDefaultKeyId();
}
if (!keyId) {
return !!(await this.getKey(keyId));
}
async keyNeedsUpgrade(keyId) {
const keyInfo = await this.getKey(keyId);
if (keyInfo && keyInfo[1].algorithm === SECRET_STORAGE_ALGORITHM_V1_CURVE25519) {
return true;
} else {
return false;
}
return !!this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
}
/**
@@ -207,24 +226,13 @@ export class SecretStorage extends EventEmitter {
throw new Error("Unknown key: " + keyId);
}
// check signature of key info
pkVerify(
keyInfo,
this._crossSigningInfo.getId('master'),
this._crossSigningInfo.userId,
);
// encrypt secret, based on the algorithm
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
case SECRET_STORAGE_ALGORITHM_V1_AES:
{
const encryption = new global.Olm.PkEncryption();
try {
encryption.set_recipient_key(keyInfo.pubkey);
encrypted[keyId] = encryption.encrypt(secret);
} finally {
encryption.free();
}
const keys = {[keyId]: keyInfo};
const [, encryption] = await this._getSecretStorageKey(keys, name);
encrypted[keyId] = await encryption.encrypt(secret);
break;
}
default:
@@ -238,29 +246,6 @@ export class SecretStorage extends EventEmitter {
await this._baseApis.setAccountData(name, {encrypted});
}
/**
* Store a secret defined to be the same as the given key.
* No secret information will be stored, instead the secret will
* be stored with a marker to say that the contents of the secret is
* the value of the given key.
* This is useful for migration from systems that predate SSSS such as
* key backup.
*
* @param {string} name The name of the secret
* @param {string} keyId The ID of the key whose value will be the
* value of the secret
* @returns {Promise} resolved when account data is saved
*/
storePassthrough(name, keyId) {
return this._baseApis.setAccountData(name, {
encrypted: {
[keyId]: {
passthrough: true,
},
},
});
}
/**
* Temporary method to fix up existing accounts where secrets
* are incorrectly stored without the 'encrypted' level
@@ -317,7 +302,12 @@ export class SecretStorage extends EventEmitter {
);
const encInfo = secretInfo.encrypted[keyId];
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
case SECRET_STORAGE_ALGORITHM_V1_AES:
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
keys[keyId] = keyInfo;
}
break;
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
if (
keyInfo.pubkey && (
(encInfo.ciphertext && encInfo.mac && encInfo.ephemeral) ||
@@ -344,15 +334,9 @@ export class SecretStorage extends EventEmitter {
// since we just want to return the key itself.
if (encInfo.passthrough) return decryption.get_private_key();
// decrypt secret
switch (keys[keyId].algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
return decryption.decrypt(
encInfo.ephemeral, encInfo.mac, encInfo.ciphertext,
);
}
return await decryption.decrypt(encInfo);
} finally {
if (decryption) decryption.free();
if (decryption && decryption.free) decryption.free();
}
}
@@ -362,22 +346,26 @@ export class SecretStorage extends EventEmitter {
* @param {string} name the name of the secret
* @param {boolean} checkKey check if the secret is encrypted by a trusted key
*
* @return {boolean} whether or not the secret is stored
* @return {object?} map of key name to key info the secret is encrypted
* with, or null if it is not present or not encrypted with a trusted
* key
*/
async isStored(name, checkKey) {
// check if secret exists
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
if (!secretInfo) return false;
if (!secretInfo) return null;
if (!secretInfo.encrypted) {
// try to fix it up
secretInfo = await this._fixupStoredSecret(name, secretInfo);
if (!secretInfo || !secretInfo.encrypted) {
return false;
return null;
}
}
if (checkKey === undefined) checkKey = true;
const ret = {};
// check if secret is encrypted by a known/trusted secret and
// encryption looks sane
for (const keyId of Object.keys(secretInfo.encrypted)) {
@@ -385,32 +373,55 @@ export class SecretStorage extends EventEmitter {
const keyInfo = await this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
if (!keyInfo) return false;
if (!keyInfo) continue;
const encInfo = secretInfo.encrypted[keyId];
if (checkKey) {
pkVerify(
keyInfo,
this._crossSigningInfo.getId('master'),
this._crossSigningInfo.userId,
);
}
// We don't actually need the decryption object if it's a passthrough
// since we just want to return the key itself.
if (encInfo.passthrough) return true;
if (encInfo.passthrough) {
try {
pkVerify(
keyInfo,
this._crossSigningInfo.getId('master'),
this._crossSigningInfo.userId,
);
} catch (e) {
// not trusted, so move on to the next key
continue;
}
ret[keyId] = keyInfo;
continue;
}
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
case SECRET_STORAGE_ALGORITHM_V1_AES:
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
ret[keyId] = keyInfo;
}
break;
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
&& encInfo.ephemeral) {
return true;
if (checkKey) {
try {
pkVerify(
keyInfo,
this._crossSigningInfo.getId('master'),
this._crossSigningInfo.userId,
);
} catch (e) {
// not trusted, so move on to the next key
continue;
}
}
ret[keyId] = keyInfo;
}
break;
default:
// do nothing if we don't understand the encryption algorithm
}
}
return false;
return Object.keys(ret).length ? ret : null;
}
/**
@@ -462,6 +473,7 @@ export class SecretStorage extends EventEmitter {
for (const device of devices) {
toDevice[device] = requestData;
}
logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
this._baseApis.sendToDevice("m.secret.request", {
[this._baseApis.getUserId()]: toDevice,
});
@@ -515,6 +527,7 @@ export class SecretStorage extends EventEmitter {
device_trust: this._baseApis.checkDeviceTrust(sender, deviceId),
});
if (secret) {
logger.info(`Preparing ${content.name} secret for ${deviceId}`);
const payload = {
type: "m.secret.send",
content: {
@@ -551,7 +564,10 @@ export class SecretStorage extends EventEmitter {
},
};
logger.info(`Sending ${content.name} secret for ${deviceId}`);
this._baseApis.sendToDevice("m.room.encrypted", contentMap);
} else {
logger.info(`Request denied for ${content.name} secret for ${deviceId}`);
}
}
}
@@ -563,7 +579,7 @@ export class SecretStorage extends EventEmitter {
return;
}
const content = event.getContent();
logger.log("got secret share for request ", content.request_id);
logger.log("got secret share for request", content.request_id);
const requestControl = this._requests[content.request_id];
if (requestControl) {
// make sure that the device that sent it is one of the devices that
@@ -607,26 +623,48 @@ export class SecretStorage extends EventEmitter {
}
switch (keys[keyId].algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
{
const decryption = new global.Olm.PkDecryption();
let pubkey;
try {
pubkey = decryption.init_with_private_key(privateKey);
} catch (e) {
decryption.free();
throw new Error("getSecretStorageKey callback returned invalid key");
}
if (pubkey !== keys[keyId].pubkey) {
decryption.free();
throw new Error(
"getSecretStorageKey callback returned incorrect key",
);
}
return [keyId, decryption];
case SECRET_STORAGE_ALGORITHM_V1_AES:
{
const decryption = {
encrypt: async function(secret) {
return await encryptAES(secret, privateKey, name);
},
decrypt: async function(encInfo) {
return await decryptAES(encInfo, privateKey, name);
},
};
return [keyId, decryption];
}
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
{
const pkDecryption = new global.Olm.PkDecryption();
let pubkey;
try {
pubkey = pkDecryption.init_with_private_key(privateKey);
} catch (e) {
pkDecryption.free();
throw new Error("getSecretStorageKey callback returned invalid key");
}
default:
throw new Error("Unknown key type: " + keys[keyId].algorithm);
if (pubkey !== keys[keyId].pubkey) {
pkDecryption.free();
throw new Error(
"getSecretStorageKey callback returned incorrect key",
);
}
const decryption = {
free: pkDecryption.free.bind(pkDecryption),
decrypt: async function(encInfo) {
return pkDecryption.decrypt(
encInfo.ephemeral, encInfo.mac, encInfo.ciphertext,
);
},
// needed for passthrough
get_private_key: pkDecryption.get_private_key.bind(pkDecryption),
};
return [keyId, decryption];
}
default:
throw new Error("Unknown key type: " + keys[keyId].algorithm);
}
}
}
+239
View File
@@ -0,0 +1,239 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
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.
*/
import {getCrypto} from '../utils';
import {decodeBase64, encodeBase64} from './olmlib';
const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
// salt for HKDF, with 8 bytes of zeros
const zerosalt = new Uint8Array(8);
/**
* encrypt a string in Node.js
*
* @param {string} data the plaintext to encrypt
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
*/
async function encryptNode(data, key, name) {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
}
const iv = crypto.randomBytes(16);
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
const [aesKey, hmacKey] = deriveKeysNode(key, name);
const cipher = crypto.createCipheriv("aes-256-ctr", aesKey, iv);
const ciphertext = cipher.update(data, "utf-8", "base64")
+ cipher.final("base64");
const hmac = crypto.createHmac("sha256", hmacKey)
.update(ciphertext, "base64").digest("base64");
return {
iv: encodeBase64(iv),
ciphertext: ciphertext,
mac: hmac,
};
}
/**
* decrypt a string in Node.js
*
* @param {object} data the encrypted data
* @param {string} data.ciphertext the ciphertext in base64
* @param {string} data.iv the initialization vector in base64
* @param {string} data.mac the HMAC in base64
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
*/
async function decryptNode(data, key, name) {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
}
const [aesKey, hmacKey] = deriveKeysNode(key, name);
const hmac = crypto.createHmac("sha256", hmacKey)
.update(data.ciphertext, "base64").digest("base64");
if (hmac !== data.mac) {
throw new Error(`Error decrypting secret ${name}: bad MAC`);
}
const decipher = crypto.createDecipheriv(
"aes-256-ctr", aesKey, decodeBase64(data.iv),
);
return decipher.update(data.ciphertext, "base64", "utf-8")
+ decipher.final("utf-8");
}
function deriveKeysNode(key, name) {
const crypto = getCrypto();
const prk = crypto.createHmac("sha256", zerosalt)
.update(key).digest();
const b = Buffer.alloc(1, 1);
const aesKey = crypto.createHmac("sha256", prk)
.update(name, "utf-8").update(b).digest();
b[0] = 2;
const hmacKey = crypto.createHmac("sha256", prk)
.update(aesKey).update(name, "utf-8").update(b).digest();
return [aesKey, hmacKey];
}
/**
* encrypt a string in Node.js
*
* @param {string} data the plaintext to encrypt
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
*/
async function encryptBrowser(data, key, name) {
const iv = new Uint8Array(16);
window.crypto.getRandomValues(iv);
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
// (which would mean we wouldn't be able to decrypt on Android). The loss
// of a single bit of iv is a price we have to pay.
iv[8] &= 0x7f;
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
const encodedData = new TextEncoder().encode(data);
const ciphertext = await subtleCrypto.encrypt(
{
name: "AES-CTR",
counter: iv,
length: 64,
},
aesKey,
encodedData,
);
const hmac = await subtleCrypto.sign(
{name: 'HMAC'},
hmacKey,
ciphertext,
);
return {
iv: encodeBase64(iv),
ciphertext: encodeBase64(ciphertext),
mac: encodeBase64(hmac),
};
}
/**
* decrypt a string in the browser
*
* @param {object} data the encrypted data
* @param {string} data.ciphertext the ciphertext in base64
* @param {string} data.iv the initialization vector in base64
* @param {string} data.mac the HMAC in base64
* @param {Uint8Array} key the encryption key to use
* @param {string} name the name of the secret
*/
async function decryptBrowser(data, key, name) {
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
const ciphertext = decodeBase64(data.ciphertext);
if (!await subtleCrypto.verify(
{name: "HMAC"},
hmacKey,
decodeBase64(data.mac),
ciphertext,
)) {
throw new Error(`Error decrypting secret ${name}: bad MAC`);
}
const plaintext = await subtleCrypto.decrypt(
{
name: "AES-CTR",
counter: decodeBase64(data.iv),
length: 64,
},
aesKey,
ciphertext,
);
return new TextDecoder().decode(new Uint8Array(plaintext));
}
async function deriveKeysBrowser(key, name) {
const hkdfkey = await subtleCrypto.importKey(
'raw',
key,
{name: "HKDF"},
false,
["deriveBits"],
);
const keybits = await subtleCrypto.deriveBits(
{
name: "HKDF",
salt: zerosalt,
info: (new TextEncoder().encode(name)),
hash: "SHA-256",
},
hkdfkey,
512,
);
const aesKey = keybits.slice(0, 32);
const hmacKey = keybits.slice(32);
const aesProm = subtleCrypto.importKey(
'raw',
aesKey,
{name: 'AES-CTR'},
false,
['encrypt', 'decrypt'],
);
const hmacProm = subtleCrypto.importKey(
'raw',
hmacKey,
{
name: 'HMAC',
hash: {name: 'SHA-256'},
},
false,
['sign', 'verify'],
);
return await Promise.all([aesProm, hmacProm]);
}
export function encryptAES(...args) {
return subtleCrypto ? encryptBrowser(...args) : encryptNode(...args);
}
export function decryptAES(...args) {
return subtleCrypto ? decryptBrowser(...args) : decryptNode(...args);
}
+102 -54
View File
@@ -185,12 +185,14 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
*
* @param {Object} devicesInRoom The devices in this room, indexed by user ID
* @param {Object} blocked The devices that are blocked, indexed by user ID
* @param {boolean} [singleOlmCreationPhase] Only perform one round of olm
* session creation
*
* @return {Promise} Promise which resolves to the
* OutboundSessionInfo when setup is complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = async function(
devicesInRoom, blocked,
devicesInRoom, blocked, singleOlmCreationPhase,
) {
let session;
@@ -270,56 +272,53 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
const errorDevices = [];
// meanwhile, establish olm sessions for devices that we don't
// already have a session for, and share keys with them. Use a
// shorter timeout when fetching one-time keys.
// already have a session for, and share keys with them. If
// we're doing two phases of olm session creation, use a
// shorter timeout when fetching one-time keys for the first
// phase.
const start = Date.now();
const failedServers = [];
await this._shareKeyWithDevices(
session, key, payload, devicesWithoutSession, errorDevices, 2000,
session, key, payload, devicesWithoutSession, errorDevices,
singleOlmCreationPhase ? 10000 : 2000, failedServers,
);
(async () => {
// Retry sending keys to devices that we were unable to establish
// an olm session for. This time, we use a longer timeout, but we
// do this in the background and don't block anything else while we
// do this.
const retryDevices = {};
for (const {userId, deviceInfo} of errorDevices) {
retryDevices[userId] = retryDevices[userId] || [];
retryDevices[userId].push(deviceInfo);
}
if (!singleOlmCreationPhase && (Date.now() - start < 10000)) {
// perform the second phase of olm session creation if requested,
// and if the first phase didn't take too long
(async () => {
// Retry sending keys to devices that we were unable to establish
// an olm session for. This time, we use a longer timeout, but we
// do this in the background and don't block anything else while we
// do this. We only need to retry users from servers that didn't
// respond the first time.
const retryDevices = {};
const failedServerMap = new Set;
for (const server of failedServers) {
failedServerMap.add(server);
}
const failedDevices = [];
for (const {userId, deviceInfo} of errorDevices) {
const userHS = userId.slice(userId.indexOf(":") + 1);
if (failedServerMap.has(userHS)) {
retryDevices[userId] = retryDevices[userId] || [];
retryDevices[userId].push(deviceInfo);
} else {
// if we aren't going to retry, then handle it
// as a failed device
failedDevices.push({userId, deviceInfo});
}
}
const failedDevices = [];
await this._shareKeyWithDevices(
session, key, payload, retryDevices, failedDevices,
);
const blockedMap = {};
const filteredFailedDevices =
await this._olmDevice.filterOutNotifiedErrorDevices(
failedDevices,
await this._shareKeyWithDevices(
session, key, payload, retryDevices, failedDevices,
);
for (const {userId, deviceInfo} of filteredFailedDevices) {
blockedMap[userId] = blockedMap[userId] || {};
// we use a similar format to what
// olmlib.ensureOlmSessionsForDevices returns, so that
// we can use the same function to split
blockedMap[userId][deviceInfo.deviceId] = {
device: {
code: "m.no_olm",
reason: WITHHELD_MESSAGES["m.no_olm"],
deviceInfo,
},
};
const deviceId = deviceInfo.deviceId;
// mark this device as "handled" because we don't want to try
// to claim a one-time-key for dead devices on every message.
session.markSharedWithDevice(userId, deviceId, key.chain_index);
}
// notify devices that we couldn't get an olm session
await this._notifyBlockedDevices(session, blockedMap);
})();
await this._notifyFailedOlmDevices(session, key, failedDevices);
})();
} else {
await this._notifyFailedOlmDevices(session, key, errorDevices);
}
})(),
(async () => {
// also, notify blocked devices that they're blocked
@@ -629,9 +628,7 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, {
[userId]: {
[device.deviceId]: device,
},
[userId]: [device],
},
);
@@ -692,12 +689,15 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
*
* @param {Number} [otkTimeout] The timeout in milliseconds when requesting
* one-time keys for establishing new olm sessions.
*
* @param {Array} [failedServers] An array to fill with remote servers that
* failed to respond to one-time-key requests.
*/
MegolmEncryption.prototype._shareKeyWithDevices = async function(
session, key, payload, devicesByUser, errorDevices, otkTimeout,
session, key, payload, devicesByUser, errorDevices, otkTimeout, failedServers,
) {
const devicemap = await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser, otkTimeout,
this._olmDevice, this._baseApis, devicesByUser, otkTimeout, failedServers,
);
this._getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices);
@@ -726,6 +726,52 @@ MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
}
};
/**
* Notify devices that we weren't able to create olm sessions.
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object} key
*
* @param {Array<object>} failedDevices the devices that we were unable to
* create olm sessions for, as returned by _shareKeyWithDevices
*/
MegolmEncryption.prototype._notifyFailedOlmDevices = async function(
session, key, failedDevices,
) {
// mark the devices that failed as "handled" because we don't want to try
// to claim a one-time-key for dead devices on every message.
for (const {userId, deviceInfo} of failedDevices) {
const deviceId = deviceInfo.deviceId;
session.markSharedWithDevice(
userId, deviceId, key.chain_index,
);
}
const filteredFailedDevices =
await this._olmDevice.filterOutNotifiedErrorDevices(
failedDevices,
);
const blockedMap = {};
for (const {userId, deviceInfo} of filteredFailedDevices) {
blockedMap[userId] = blockedMap[userId] || {};
// we use a similar format to what
// olmlib.ensureOlmSessionsForDevices returns, so that
// we can use the same function to split
blockedMap[userId][deviceInfo.deviceId] = {
device: {
code: "m.no_olm",
reason: WITHHELD_MESSAGES["m.no_olm"],
deviceInfo,
},
};
}
// send the notifications
await this._notifyBlockedDevices(session, blockedMap);
};
/**
* Notify blocked devices that they have been blocked.
*
@@ -769,8 +815,6 @@ MegolmEncryption.prototype._notifyBlockedDevices = async function(
* @param {module:models/room} room the room the event is in
*/
MegolmEncryption.prototype.prepareToEncrypt = function(room) {
logger.debug(`Preparing to encrypt events for ${this._roomId}`);
if (this.encryptionPreparation) {
// We're already preparing something, so don't do anything else.
// FIXME: check if we need to restart
@@ -778,6 +822,8 @@ MegolmEncryption.prototype.prepareToEncrypt = function(room) {
return;
}
logger.debug(`Preparing to encrypt events for ${this._roomId}`);
this.encryptionPreparation = (async () => {
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
@@ -788,7 +834,7 @@ MegolmEncryption.prototype.prepareToEncrypt = function(room) {
this._removeUnknownDevices(devicesInRoom);
}
await this._ensureOutboundSession(devicesInRoom, blocked);
await this._ensureOutboundSession(devicesInRoom, blocked, true);
delete this.encryptionPreparation;
})();
@@ -957,8 +1003,10 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
continue;
}
const deviceTrust = this._crypto.checkDeviceTrust(userId, deviceId);
if (userDevices[deviceId].isBlocked() ||
(userDevices[deviceId].isUnverified() && isBlacklisting)
(!deviceTrust.isVerified() && isBlacklisting)
) {
if (!blocked[userId]) {
blocked[userId] = {};
+425 -71
View File
@@ -39,7 +39,7 @@ import {
UserTrustLevel,
createCryptoStoreCacheCallbacks,
} from './CrossSigning';
import {SECRET_STORAGE_ALGORITHM_V1, SecretStorage} from './SecretStorage';
import {SECRET_STORAGE_ALGORITHM_V1_AES, SecretStorage} from './SecretStorage';
import {OutgoingRoomKeyRequestManager} from './OutgoingRoomKeyRequestManager';
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
import {
@@ -55,6 +55,7 @@ import {InRoomChannel, InRoomRequests} from "./verification/request/InRoomChanne
import {ToDeviceChannel, ToDeviceRequests} from "./verification/request/ToDeviceChannel";
import * as httpApi from "../http-api";
import {IllegalMethod} from "./verification/IllegalMethod";
import {KeySignatureUploadError} from "../errors";
const DeviceVerification = DeviceInfo.DeviceVerification;
@@ -119,6 +120,8 @@ export function Crypto(baseApis, sessionStore, userId, deviceId,
this._onDeviceListUserCrossSigningUpdated =
this._onDeviceListUserCrossSigningUpdated.bind(this);
this._trustCrossSignedDevices = true;
this._reEmitter = new ReEmitter(this);
this._baseApis = baseApis;
this._sessionStore = sessionStore;
@@ -318,6 +321,47 @@ Crypto.prototype.init = async function(opts) {
this._checkAndStartKeyBackup();
};
/**
* Whether to trust a others users signatures of their devices.
* If false, devices will only be considered 'verified' if we have
* verified that device individually (effectively disabling cross-signing).
*
* Default: true
*
* @return {bool} True if trusting cross-signed devices
*/
Crypto.prototype.getCryptoTrustCrossSignedDevices = function() {
return this._trustCrossSignedDevices;
};
/**
* See getCryptoTrustCrossSignedDevices
* This may be set before initCrypto() is called to ensure no races occur.
*
* @param {bool} val True to trust cross-signed devices
*/
Crypto.prototype.setCryptoTrustCrossSignedDevices = function(val) {
this._trustCrossSignedDevices = val;
for (const userId of this._deviceList.getKnownUserIds()) {
const devices = this._deviceList.getRawStoredDevicesForUser(userId);
for (const deviceId of Object.keys(devices)) {
const deviceTrust = this.checkDeviceTrust(userId, deviceId);
// If the device is locally verified then isVerified() is always true,
// so this will only have caused the value to change if the device is
// cross-signing verified but not locally verified
if (
!deviceTrust.isLocallyVerified() &&
deviceTrust.isCrossSigningVerified()
) {
const deviceObj = this._deviceList.getStoredDevice(userId, deviceId);
this.emit("deviceVerificationChanged", userId, deviceId, deviceObj);
}
}
}
};
/**
* Create a recovery key from a user-supplied passphrase.
*
@@ -351,6 +395,37 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) {
}
};
/**
* Checks whether cross signing:
* - is enabled on this account
* - is trusted by this device
* - has private keys stored in secret storage
* and that the account has a secret storage key
*
* If this function returns false, bootstrapSecretStorage() can be used
* to fix things such that it returns true. That is to say, after
* bootstrapSecretStorage() completes sucessfully, this function should
* return true.
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @return {bool} True if cross-signing is ready to be used on this device
*/
Crypto.prototype.isCrossSigningReady = async function() {
const publicKeysOnDevice = this._crossSigningInfo.getId();
const privateKeysInStorage = await this._crossSigningInfo.isStoredInSecretStorage(
this._secretStorage,
);
const secretStorageKeyInAccount = await this._secretStorage.hasKey();
return (
publicKeysOnDevice &&
privateKeysInStorage &&
secretStorageKeyInAccount
);
};
/**
* Bootstrap Secure Secret Storage if needed by creating a default key and
* signing it with the cross-signing master key. If everything is already set
@@ -370,6 +445,9 @@ Crypto.prototype.createRecoveryKeyFromPassphrase = async function(password) {
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
* is supplied.
* @param {bool} [opts.setupNewSecretStorage] Optional. Reset even if keys already exist.
* @param {func} [opts.getKeyBackupPassphrase] Optional. Function called to get the user's
* current key backup passphrase. Should return a promise that resolves with a Buffer
* containing the key, or rejects if the key cannot be obtained.
* Returns:
* {Promise} A promise which resolves to key creation data for
* SecretStorage#addKey: an object with `passphrase` and/or `pubkey` fields.
@@ -380,6 +458,7 @@ Crypto.prototype.bootstrapSecretStorage = async function({
keyBackupInfo,
setupNewKeyBackup,
setupNewSecretStorage,
getKeyBackupPassphrase,
} = {}) {
logger.log("Bootstrapping Secure Secret Storage");
@@ -394,10 +473,82 @@ Crypto.prototype.bootstrapSecretStorage = async function({
// use temporary callbacks to weave them through the various APIs.
const appCallbacks = Object.assign({}, this._baseApis._cryptoCallbacks);
// the ID of the new SSSS key, if we create one
let newKeyId = null;
// cache SSSS keys so that we don't need to constantly pester the user about it
const ssssKeys = {};
this._baseApis._cryptoCallbacks.getSecretStorageKey =
async ({keys}, name) => {
// if we already have a key that works, return it
for (const keyId of Object.keys(keys)) {
if (ssssKeys[keyId]) {
return [keyId, ssssKeys[keyId]];
}
}
// otherwise, prompt the user and cache it
const key = await appCallbacks.getSecretStorageKey({keys}, name);
if (key) {
const [keyId, keyData] = key;
ssssKeys[keyId] = keyData;
}
return key;
};
try {
const inStorage = !setupNewSecretStorage &&
await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
if (!this._crossSigningInfo.getId() || !inStorage) {
const decryptionKeys =
await this._crossSigningInfo.isStoredInSecretStorage(this._secretStorage);
const inStorage = !setupNewSecretStorage && decryptionKeys;
if (decryptionKeys && !(Object.values(decryptionKeys).some(
info => info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES,
))) {
// we already have cross-signing keys, but they're encrypted using
// the old algorithm
logger.log("Switching to symmetric");
const keys = {};
// fetch the cross-signing private keys (needed to sign the new
// SSSS key). We store the cross-signing keys, and temporarily set
// a callback so that when the private key is needed while setting
// things up, we can provide it.
this._baseApis._cryptoCallbacks.getCrossSigningKey =
name => crossSigningPrivateKeys[name];
for (const type of ["master", "self_signing", "user_signing"]) {
const secretName = `m.cross_signing.${type}`;
const secret = await this.getSecret(secretName);
keys[type] = secret;
crossSigningPrivateKeys[type] = olmlib.decodeBase64(secret);
}
await this.checkOwnCrossSigningTrust();
const opts = {};
let oldKeyId = null;
for (const [keyId, keyInfo] of Object.entries(decryptionKeys)) {
// See if the old key was generated from a passphrase. If
// yes, use the same settings.
if (keyId in ssssKeys) {
oldKeyId = keyId;
if (keyInfo.passphrase) {
opts.passphrase = keyInfo.passphrase;
}
break;
}
}
// create new symmetric SSSS key and set it as default
newKeyId = await this.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
);
if (oldKeyId) {
ssssKeys[newKeyId] = ssssKeys[oldKeyId];
}
await this.setDefaultSecretStorageKeyId(newKeyId);
// re-encrypt all the keys with the new key
for (const type of ["master", "self_signing", "user_signing"]) {
const secretName = `m.cross_signing.${type}`;
await this.storeSecret(secretName, keys[type], [newKeyId]);
}
} else if (!this._crossSigningInfo.getId() || !inStorage) {
// create new cross-signing keys if necessary.
logger.log(
"Cross-signing public and/or private keys not found, " +
"checking secret storage for private keys",
@@ -423,40 +574,50 @@ Crypto.prototype.bootstrapSecretStorage = async function({
logger.log("Cross signing keys are present in secret storage");
}
// Check if Secure Secret Storage has a default key. If we don't have one, create
// the default key (which will also be signed by the cross-signing master key).
if (setupNewSecretStorage || !await this.hasSecretStorageKey()) {
let newKeyId;
// Check if we need to create a new secret storage key
// - we're resetting secret storage
// - we don't have a default secret storage key yet
// - our default secret storage key is using an older algorithm
// We will also run this part if we created a new secret storage key
// above, so that we can (re-)encrypt the backup with it.
const defaultSSSSKey = await this.getSecretStorageKey();
if (setupNewSecretStorage || newKeyId || !defaultSSSSKey
|| defaultSSSSKey[1].algorithm !== SECRET_STORAGE_ALGORITHM_V1_AES) {
if (keyBackupInfo) {
// if we already have a backup key, use the same key as the
// secret storage key
logger.log("Secret storage default key not found, using key backup key");
const opts = {
pubkey: keyBackupInfo.auth_data.public_key,
};
if (
keyBackupInfo.auth_data.private_key_salt &&
keyBackupInfo.auth_data.private_key_iterations
) {
opts.passphrase = {
algorithm: "m.pbkdf2",
iterations: keyBackupInfo.auth_data.private_key_iterations,
salt: keyBackupInfo.auth_data.private_key_salt,
};
const backupKey = await getKeyBackupPassphrase();
if (!newKeyId) {
const opts = {};
if (
keyBackupInfo.auth_data.private_key_salt &&
keyBackupInfo.auth_data.private_key_iterations
) {
opts.passphrase = {
algorithm: "m.pbkdf2",
iterations: keyBackupInfo.auth_data.private_key_iterations,
salt: keyBackupInfo.auth_data.private_key_salt,
bits: 256,
};
}
newKeyId = await this.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1_AES, opts,
);
this.setDefaultSecretStorageKeyId(newKeyId);
// use the backup key as the new ssss key
ssssKeys[newKeyId] = backupKey;
}
newKeyId = await this.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1, opts,
);
// Add an entry for the backup key in SSSS as a 'passthrough' key
// (ie. the secret is the key itself).
this._secretStorage.storePassthrough('m.megolm_backup.v1', newKeyId);
// if this key backup is trusted, sign it with the cross signing key
// so the key backup can be trusted via cross-signing.
const backupSigStatus = await this.checkKeyBackup(keyBackupInfo);
if (backupSigStatus.trustInfo.usable) {
console.log("Adding cross signing signature to key backup");
logger.log("Adding cross signing signature to key backup");
await this._crossSigningInfo.signObject(
keyBackupInfo.auth_data, "master",
);
@@ -465,20 +626,32 @@ Crypto.prototype.bootstrapSecretStorage = async function({
undefined, keyBackupInfo,
{prefix: httpApi.PREFIX_UNSTABLE},
);
await this.storeSecret(
"m.megolm_backup.v1", olmlib.encodeBase64(backupKey), [newKeyId],
);
} else {
console.log(
logger.log(
"Key backup is NOT TRUSTED: NOT adding cross signing signature",
);
}
} else {
logger.log("Secret storage default key not found, creating new key");
const keyOptions = await createSecretStorageKey();
newKeyId = await this.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1,
keyOptions,
);
if (!newKeyId) {
logger.log("Secret storage default key not found, creating new key");
const keyOptions = await createSecretStorageKey();
newKeyId = await this.addSecretStorageKey(
SECRET_STORAGE_ALGORITHM_V1_AES,
keyOptions,
);
await this.setDefaultSecretStorageKeyId(newKeyId);
}
if (await this.isSecretStored("m.megolm_backup.v1")) {
// we created a new SSSS, and we previously encrypted the
// backup key with the old SSSS key, so re-encrypt with the
// new key
const backupKey = await this.getSecret("m.megolm_backup.v1");
await this.storeSecret("m.megolm_backup.v1", backupKey, [newKeyId]);
}
}
await this.setDefaultSecretStorageKeyId(newKeyId);
} else {
logger.log("Have secret storage key");
}
@@ -504,6 +677,15 @@ Crypto.prototype.bootstrapSecretStorage = async function({
}
}
// Call `getCrossSigningKey` for side effect of caching private keys for
// future gossiping to other devices if enabled via app level callbacks.
if (this._crossSigningInfo._cacheCallbacks) {
for (const type of ["self_signing", "user_signing"]) {
logger.log(`Cache ${type} cross-signing private key locally`);
await this._crossSigningInfo.getCrossSigningKey(type);
}
}
if (setupNewKeyBackup && !keyBackupInfo) {
const info = await this._baseApis.prepareKeyBackupVersion(
null /* random key */,
@@ -534,6 +716,14 @@ Crypto.prototype.hasSecretStorageKey = function(keyID) {
return this._secretStorage.hasKey(keyID);
};
Crypto.prototype.secretStorageKeyNeedsUpgrade = function(keyID) {
return this._secretStorage.keyNeedsUpgrade(keyID);
};
Crypto.prototype.getSecretStorageKey = function(keyID) {
return this._secretStorage.getKey(keyID);
};
Crypto.prototype.storeSecret = function(name, secret, keys) {
return this._secretStorage.store(name, secret, keys);
};
@@ -582,6 +772,41 @@ Crypto.prototype.checkSecretStoragePrivateKey = function(privateKey, expectedPub
}
};
/**
* Fetches the backup private key, if cached
* @returns {Promise} the key, if any, or null
*/
Crypto.prototype.getSessionBackupPrivateKey = async function() {
return new Promise((resolve) => {
this._cryptoStore.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._cryptoStore.getSecretStorePrivateKey(
txn,
resolve,
"m.megolm_backup.v1",
);
},
);
});
};
/**
* Stores the session backup key to the cache
* @param {Uint8Array} key the private key
* @returns {Promise} so you can catch failures
*/
Crypto.prototype.storeSessionBackupPrivateKey = async function(key) {
return this._cryptoStore.doTxn(
'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._cryptoStore.storeSecretStorePrivateKey(txn, "m.megolm_backup.v1", key);
},
);
};
/**
* Checks that a given cross-signing private key matches a given public key.
* This can be used by the getCrossSigningKey callback to verify that the
@@ -667,15 +892,34 @@ Crypto.prototype._afterCrossSigningLocalKeyChange = async function() {
const device = this._deviceList.getStoredDevice(this._userId, this._deviceId);
const signedDevice = await this._crossSigningInfo.signDevice(this._userId, device);
logger.info(`Starting background key sig upload for ${this._deviceId}`);
this._baseApis.uploadKeySignatures({
[this._userId]: {
[this._deviceId]: signedDevice,
},
}).then(() => {
logger.info(`Finished background key sig upload for ${this._deviceId}`);
}).catch(e => {
logger.error(`Error during background key sig upload for ${this._deviceId}`, e);
});
const upload = ({ shouldEmit }) => {
return this._baseApis.uploadKeySignatures({
[this._userId]: {
[this._deviceId]: signedDevice,
},
}).then((response) => {
const { failures } = response || {};
if (Object.keys(failures || []).length > 0) {
if (shouldEmit) {
this._baseApis.emit(
"crypto.keySignatureUploadFailure",
failures,
"_afterCrossSigningLocalKeyChange",
upload, // continuation
);
}
throw new KeySignatureUploadError("Key upload failed", { failures });
}
logger.info(`Finished background key sig upload for ${this._deviceId}`);
}).catch(e => {
logger.error(
`Error during background key sig upload for ${this._deviceId}`,
e,
);
});
};
upload({ shouldEmit: true });
const shouldUpgradeCb = (
this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications
@@ -842,15 +1086,18 @@ Crypto.prototype.checkDeviceTrust = function(userId, deviceId) {
* @returns {DeviceTrustLevel}
*/
Crypto.prototype._checkDeviceInfoTrust = function(userId, device) {
const trustedLocally = device && device.isVerified();
const trustedLocally = !!(device && device.isVerified());
const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId);
if (device && userCrossSigning) {
// The _trustCrossSignedDevices only affects trust of other people's cross-signing
// signatures
const trustCrossSig = this._trustCrossSignedDevices || userId === this._userId;
return this._crossSigningInfo.checkDeviceTrust(
userCrossSigning, device, trustedLocally,
userCrossSigning, device, trustedLocally, trustCrossSig,
);
} else {
return new DeviceTrustLevel(false, false, trustedLocally);
return new DeviceTrustLevel(false, false, trustedLocally, false);
}
};
@@ -952,19 +1199,52 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
}
if (masterChanged) {
await this._signObject(this._crossSigningInfo.keys.master);
keySignatures[this._crossSigningInfo.getId()]
= this._crossSigningInfo.keys.master;
const masterKey = this._crossSigningInfo.keys.master;
await this._signObject(masterKey);
const deviceSig = masterKey.signatures[this._userId]["ed25519:" + this._deviceId];
// Include only the _new_ device signature in the upload.
// We may have existing signatures from deleted devices, which will cause
// the entire upload to fail.
keySignatures[this._crossSigningInfo.getId()] = Object.assign(
{},
masterKey,
{
signatures: {
[this._userId]: {
["ed25519:" + this._deviceId]: deviceSig,
},
},
},
);
}
const keysToUpload = Object.keys(keySignatures);
if (keysToUpload.length) {
logger.info(`Starting background key sig upload for ${keysToUpload}`);
this._baseApis.uploadKeySignatures({ [this._userId]: keySignatures }).then(() => {
logger.info(`Finished background key sig upload for ${keysToUpload}`);
}).catch(e => {
logger.error(`Error during background key sig upload for ${keysToUpload}`, e);
});
const upload = ({ shouldEmit }) => {
logger.info(`Starting background key sig upload for ${keysToUpload}`);
return this._baseApis.uploadKeySignatures({ [this._userId]: keySignatures })
.then((response) => {
const { failures } = response || {};
logger.info(`Finished background key sig upload for ${keysToUpload}`);
if (Object.keys(failures || []).length > 0) {
if (shouldEmit) {
this._baseApis.emit(
"crypto.keySignatureUploadFailure",
failures,
"checkOwnCrossSigningTrust",
upload,
);
}
throw new KeySignatureUploadError("Key upload failed", { failures });
}
}).catch(e => {
logger.error(
`Error during background key sig upload for ${keysToUpload}`,
e,
);
});
};
upload({ shouldEmit: true });
}
this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId));
@@ -1605,12 +1885,33 @@ Crypto.prototype.setDeviceVerification = async function(
);
const device = await this._crossSigningInfo.signUser(xsk);
if (device) {
logger.info("Uploading signature for " + userId + "...");
await this._baseApis.uploadKeySignatures({
[userId]: {
[deviceId]: device,
},
});
const upload = async ({ shouldEmit }) => {
logger.info("Uploading signature for " + userId + "...");
const response = await this._baseApis.uploadKeySignatures({
[userId]: {
[deviceId]: device,
},
});
const { failures } = response || {};
if (Object.keys(failures || []).length > 0) {
if (shouldEmit) {
this._baseApis.emit(
"crypto.keySignatureUploadFailure",
failures,
"setDeviceVerification",
upload,
);
}
/* Throwing here causes the process to be cancelled and the other
* user to be notified */
throw new KeySignatureUploadError(
"Key upload failed",
{ failures },
);
}
};
await upload({ shouldEmit: true });
// This will emit events when it comes back down the sync
// (we could do local echo to speed things up)
}
@@ -1659,12 +1960,27 @@ Crypto.prototype.setDeviceVerification = async function(
userId, DeviceInfo.fromStorage(dev, deviceId),
);
if (device) {
logger.info("Uploading signature for " + deviceId);
await this._baseApis.uploadKeySignatures({
[userId]: {
[deviceId]: device,
},
});
const upload = async ({shouldEmit}) => {
logger.info("Uploading signature for " + deviceId);
const response = await this._baseApis.uploadKeySignatures({
[userId]: {
[deviceId]: device,
},
});
const { failures } = response || {};
if (Object.keys(failures || []).length > 0) {
if (shouldEmit) {
this._baseApis.emit(
"crypto.keySignatureUploadFailure",
failures,
"setDeviceVerification",
upload, // continuation
);
}
throw new KeySignatureUploadError("Key upload failed", { failures });
}
};
await upload({shouldEmit: true});
// XXX: we'll need to wait for the device list to be updated
}
}
@@ -1751,6 +2067,27 @@ Crypto.prototype.beginKeyVerification = function(
return request.beginKeyVerification(method, {userId, deviceId});
};
Crypto.prototype.legacyDeviceVerification = async function(
userId, deviceId, method,
) {
const transactionId = ToDeviceChannel.makeTransactionId();
const channel = new ToDeviceChannel(
this._baseApis, userId, [deviceId], transactionId, deviceId);
const request = new VerificationRequest(
channel, this._verificationMethods, this._baseApis);
this._toDeviceVerificationRequests.setRequestBySenderAndTxnId(
userId, transactionId, request);
const verifier = request.beginKeyVerification(method, {userId, deviceId});
// either reject by an error from verify() while sending .start
// or resolve when the request receives the
// local (fake remote) echo for sending the .start event
await Promise.race([
verifier.verify(),
request.waitFor(r => r.started),
]);
return request;
};
/**
* Get information on the active olm sessions with a user
@@ -2168,10 +2505,13 @@ Crypto.prototype._backupPendingKeys = async function(limit) {
const forwardedCount =
(sessionData.forwarding_curve25519_key_chain || []).length;
const userId = this._deviceList.getUserByIdentityKey(
olmlib.MEGOLM_ALGORITHM, session.senderKey,
);
const device = this._deviceList.getDeviceByIdentityKey(
olmlib.MEGOLM_ALGORITHM, session.senderKey,
);
const verified = this._checkDeviceInfoTrust(this._userId, device).isVerified();
const verified = this._checkDeviceInfoTrust(userId, device).isVerified();
data[roomId]['sessions'][session.sessionId] = {
first_message_index: firstKnownIndex,
@@ -2249,6 +2589,20 @@ Crypto.prototype.flagAllGroupSessionsForBackup = async function() {
return remaining;
};
/**
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
*
* @param {module:models/room} room the room the event is in
*/
Crypto.prototype.prepareToEncrypt = function(room) {
const roomId = room.roomId;
const alg = this._roomEncryptors[roomId];
if (alg) {
alg.prepareToEncrypt(room);
}
};
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
/**
* Encrypt an event according to the configuration of the room.
+6 -3
View File
@@ -19,6 +19,8 @@ import {randomString} from '../randomstring';
const DEFAULT_ITERATIONS = 500000;
const DEFAULT_BITSIZE = 256;
export async function keyFromAuthData(authData, password) {
if (!global.Olm) {
throw new Error("Olm is not available");
@@ -34,6 +36,7 @@ export async function keyFromAuthData(authData, password) {
return await deriveKey(
password, authData.private_key_salt,
authData.private_key_iterations,
authData.private_key_bits || DEFAULT_BITSIZE,
);
}
@@ -44,12 +47,12 @@ export async function keyFromPassphrase(password) {
const salt = randomString(32);
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS);
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS, DEFAULT_BITSIZE);
return { key, salt, iterations: DEFAULT_ITERATIONS };
}
export async function deriveKey(password, salt, iterations) {
export async function deriveKey(password, salt, iterations, numBits = DEFAULT_BITSIZE) {
const subtleCrypto = global.crypto.subtle;
const TextEncoder = global.TextEncoder;
if (!subtleCrypto || !TextEncoder) {
@@ -73,7 +76,7 @@ export async function deriveKey(password, salt, iterations) {
hash: 'SHA-512',
},
key,
global.Olm.PRIVATE_KEY_LENGTH * 8,
numBits,
);
return new Uint8Array(keybits);
+9 -1
View File
@@ -180,14 +180,18 @@ export async function getExistingOlmSessions(
* @param {Number} [otkTimeout] The timeout in milliseconds when requesting
* one-time keys for establishing new olm sessions.
*
* @param {Array} [failedServers] An array to fill with remote servers that
* failed to respond to one-time-key requests.
*
* @return {Promise} resolves once the sessions are complete, to
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
export async function ensureOlmSessionsForDevices(
olmDevice, baseApis, devicesByUser, force, otkTimeout,
olmDevice, baseApis, devicesByUser, force, otkTimeout, failedServers,
) {
if (typeof force === "number") {
failedServers = otkTimeout;
otkTimeout = force;
force = false;
}
@@ -261,6 +265,10 @@ export async function ensureOlmSessionsForDevices(
throw e;
}
if (failedServers && "failures" in res) {
failedServers.push(...Object.keys(res.failures));
}
const otk_res = res.one_time_keys || {};
const promises = [];
for (const [userId, devices] of Object.entries(devicesByUser)) {
@@ -341,7 +341,7 @@ export class Backend {
};
}
getCrossSigningPrivateKey(txn, func, type) {
getSecretStorePrivateKey(txn, func, type) {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get(`ssss_cache:${type}`);
getReq.onsuccess = function() {
@@ -358,7 +358,7 @@ export class Backend {
objectStore.put(keys, "crossSigningKeys");
}
storeCrossSigningPrivateKey(txn, type, key) {
storeSecretStorePrivateKey(txn, type, key) {
const objectStore = txn.objectStore("account");
objectStore.put(key, `ssss_cache:${type}`);
}
+4 -4
View File
@@ -313,8 +313,8 @@ export class IndexedDBCryptoStore {
* @param {function(string)} func Called with the private key
* @param {string} type A key type
*/
getCrossSigningPrivateKey(txn, func, type) {
this._backend.getCrossSigningPrivateKey(txn, func, type);
getSecretStorePrivateKey(txn, func, type) {
this._backend.getSecretStorePrivateKey(txn, func, type);
}
/**
@@ -334,8 +334,8 @@ export class IndexedDBCryptoStore {
* @param {string} type The type of cross-signing private key to store
* @param {string} key keys object as getCrossSigningKeys()
*/
storeCrossSigningPrivateKey(txn, type, key) {
this._backend.storeCrossSigningPrivateKey(txn, type, key);
storeSecretStorePrivateKey(txn, type, key) {
this._backend.storeSecretStorePrivateKey(txn, type, key);
}
// Olm sessions
@@ -367,7 +367,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
func(keys);
}
getCrossSigningPrivateKey(txn, func, type) {
getSecretStorePrivateKey(txn, func, type) {
const key = getJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`);
func(key ? Uint8Array.from(key) : key);
}
@@ -378,7 +378,7 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
);
}
storeCrossSigningPrivateKey(txn, type, key) {
storeSecretStorePrivateKey(txn, type, key) {
setJsonItem(
this.store, E2E_PREFIX + `ssss_cache.${type}`, Array.from(key),
);
+3 -2
View File
@@ -34,6 +34,7 @@ export class MemoryCryptoStore {
this._account = null;
this._crossSigningKeys = null;
this._privateKeys = {};
this._backupKeys = {};
// Map of {devicekey -> {sessionId -> session pickle}}
this._sessions = {};
@@ -256,7 +257,7 @@ export class MemoryCryptoStore {
func(this._crossSigningKeys);
}
getCrossSigningPrivateKey(txn, func, type) {
getSecretStorePrivateKey(txn, func, type) {
const result = this._privateKeys[type];
return func(result || null);
}
@@ -265,7 +266,7 @@ export class MemoryCryptoStore {
this._crossSigningKeys = keys;
}
storeCrossSigningPrivateKey(txn, type, key) {
storeSecretStorePrivateKey(txn, type, key) {
this._privateKeys[type] = key;
}
@@ -214,6 +214,15 @@ export class VerificationRequest extends EventEmitter {
const theirMethodEvent = this._eventsByThem.get(REQUEST_TYPE) ||
this._eventsByThem.get(READY_TYPE);
if (!theirMethodEvent) {
// if we started straight away with .start event,
// we are assuming that the other side will support the
// chosen method, so return true for that.
if (this.started && this.initiatedByMe) {
const myStartEvent = this._eventsByUs.get(START_TYPE);
const content = myStartEvent && myStartEvent.getContent();
const myStartMethod = content && content.method;
return method == myStartMethod;
}
return false;
}
const content = theirMethodEvent.getContent();
@@ -566,6 +575,43 @@ export class VerificationRequest extends EventEmitter {
return newTransitions;
}
_isWinningStartRace(newEvent) {
if (newEvent.getType() !== START_TYPE) {
return false;
}
const oldEvent = this._verifier.startEvent;
const isSelfVerification = this.channel.userId === this._client.getUserId();
let oldRaceIdentifier;
if (isSelfVerification) {
// if the verifier does not have a startEvent,
// it is because it's still sending and we are on the initator side
// we know we are sending a .start event because we already
// have a verifier (checked in calling method)
if (oldEvent) {
const oldContent = oldEvent.getContent();
oldRaceIdentifier = oldContent && oldContent.from_device;
} else {
oldRaceIdentifier = this._client.getDeviceId();
}
} else {
if (oldEvent) {
oldRaceIdentifier = oldEvent.getSender();
} else {
oldRaceIdentifier = this._client.getUserId();
}
}
let newRaceIdentifier;
if (isSelfVerification) {
const newContent = newEvent.getContent();
newRaceIdentifier = newContent && newContent.from_device;
} else {
newRaceIdentifier = newEvent.getSender();
}
return newRaceIdentifier < oldRaceIdentifier;
}
/**
* Changes the state of the request and verifier in response to a key verification event.
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
@@ -600,12 +646,7 @@ export class VerificationRequest extends EventEmitter {
// only pass events from the other side to the verifier,
// no remote echos of our own events
if (this._verifier && !this.observeOnly) {
const oldEvent = this._verifier.startEvent;
// if the verifier does not have a startEvent, it is because it's still sending and we are on the initator side
const oldSender = oldEvent ?
oldEvent.getSender() :
this._client.getUserId();
const newEventWinsRace = event.getSender() < oldSender;
const newEventWinsRace = this._isWinningStartRace(event);
if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) {
this._verifier.switchStartEvent(event);
} else if (!isRemoteEcho) {
+7
View File
@@ -44,3 +44,10 @@ InvalidCryptoStoreError.prototype = Object.create(Error.prototype, {
},
});
Reflect.setPrototypeOf(InvalidCryptoStoreError, Error);
export class KeySignatureUploadError extends Error {
constructor(message, value) {
super(message);
this.value = value;
}
}
+7
View File
@@ -21,5 +21,12 @@ import request from "request";
matrixcs.request(request);
utils.runPolyfills();
try {
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
console.log('nodejs was compiled without crypto support');
}
export * from "./matrix";
export default matrixcs;
+14
View File
@@ -734,3 +734,17 @@ export async function promiseMapSeries<T>(
export function promiseTry<T>(fn: () => T): Promise<T> {
return new Promise((resolve) => resolve(fn()));
}
// We need to be able to access the Node.js crypto library from within the
// Matrix SDK without needing to `require("crypto")`, which will fail in
// browsers. So `index.ts` will call `setCrypto` to store it, and when we need
// it, we can call `getCrypto`.
let crypto: Object;
export function setCrypto(c: Object) {
crypto = c;
}
export function getCrypto(): Object {
return crypto;
}
+212 -467
View File
File diff suppressed because it is too large Load Diff