Compare commits

...

438 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
RiotRobot c6070519ed v5.1.1-rc.1 2020-03-11 15:05:49 +00:00
RiotRobot 30ece1be70 Prepare changelog for v5.1.1-rc.1 2020-03-11 15:05:48 +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 86304fd037 Merge pull request #1252 from matrix-org/uhoreg/megolm_speed
refactor megolm encryption to improve perceived speed
2020-03-10 20:09:41 -04:00
Hubert Chathi 04387e78cc some cleanups 2020-03-10 15:56:33 -04:00
Travis Ralston 2bfc44b947 Merge pull request #1253 from matrix-org/travis/remove-v1-identity
Remove v1 identity server fallbacks
2020-03-10 09:30:22 -06:00
Bruno Windels 33941eb37b Merge pull request #1251 from matrix-org/bwindels/altaliasesforname
Use alt_aliases instead of local ones for room names
2020-03-10 12:42:50 +00:00
J. Ryan Stinnett 0a45559276 Merge pull request #1250 from matrix-org/jryans/xsign-slow-login
Upload cross-signing key signatures in the background
2020-03-10 11:07:45 +00:00
Travis Ralston 800441e0ed Appease the linter 2020-03-09 17:10:37 -06:00
Travis Ralston 95164d08d5 Remove v1 identity server fallbacks
Fixes https://github.com/vector-im/riot-web/issues/10443

**Review with https://github.com/matrix-org/matrix-react-sdk/pull/4191**
2020-03-09 17:06:10 -06:00
Hubert Chathi 98d955ef1f refactor megolm encryption to improve perceived speed
- allow applications to pre-send decryption keys before the message is sent
- establish new olm sessions with a shorter timeout first, and then re-try in
  the background with a longer timeout without blocking message sending
2020-03-09 18:38:18 -04:00
Bruno Windels 950dadc14e fix tests 2020-03-09 18:33:20 +01:00
Bruno Windels 31d2f0135b use alt aliases instead of local ones for room names 2020-03-09 17:13:50 +01:00
J. Ryan Stinnett c02928f294 Upload cross-signing key signatures in the background
At the moment, uploading cross-signing key signatures is a slow process that can
potentially take many minutes (!) for large accounts / slow servers. This
changes to do the bootstrapping related versions of this in the background.

Note that key signature uploads for interactive flows like verification are
still blocking for now.

Fixes https://github.com/vector-im/riot-web/issues/12223
2020-03-09 15:08:14 +00:00
J. Ryan Stinnett 951fff45e6 Skip device verif upgrades when callback not present
This skips the upgrade when the upgrade callback is not present (which is
expected as no one sets it currently). This adds logging for around the upgrade
process.
2020-03-09 15:03:02 +00:00
J. Ryan Stinnett 4fdd817ff5 Add logging around key change post-processing 2020-03-09 14:46:10 +00:00
J. Ryan Stinnett acba31bd6d Merge pull request #1249 from matrix-org/jryans/sharing-names
Fix secret sharing names to match spec
2020-03-09 13:48:06 +00:00
J. Ryan Stinnett b5eea01848 Fix secret sharing names to match spec
When sharing keys, we should use `m.cross_signing` prefix.

Part of https://github.com/vector-im/riot-web/issues/12661
2020-03-09 13:40:02 +00:00
Bruno Windels 074e02ccf2 Merge pull request #1248 from matrix-org/bwindels/removecryptoverifstartevent
Cleanup: remove crypto.verification.start event
2020-03-06 16:47:34 +00:00
Bruno Windels 4b9bc67cb6 remove crypto.verification.start event
as it is not used anymore by the react-sdk
2020-03-06 16:48:44 +01:00
Zoe 936ef4116b For self-verifications, also request keys from the other device (#1245)
* For self-verifications, also request keys from the other device
* removed some XXX's so the editor doesn't think it's three issues
* add methods to access key cache callbacks
2020-03-06 09:56:56 +00:00
J. Ryan Stinnett 9883d6851a Merge pull request #1246 from matrix-org/jryans/xsign-trust-bool
Fix regression in key backup request params
2020-03-05 14:16:16 +00:00
J. Ryan Stinnett 4c08e126ca Fix regression in key backup request params
This converts the cross-signing trust to a boolean as required by the
homeserver.

Regressed by https://github.com/vector-im/riot-web/issues/12599
Fixes https://github.com/vector-im/riot-web/issues/12618
2020-03-05 12:17:42 +00:00
J. Ryan Stinnett bc53f8fdec Merge pull request #1244 from matrix-org/jryans/xsign-key-backup-verif
Use cross-signing trust to mark backups verified
2020-03-03 18:03:46 +00:00
J. Ryan Stinnett 0b76d3d7bd Merge pull request #1243 from matrix-org/jryans/xsign-auto-share
Check both cross-signing and local trust for key sharing
2020-03-03 18:03:38 +00:00
J. Ryan Stinnett abaf71418e Use cross-signing trust to mark backups verified
This changes to cross-signing trust as well as local trust when we decide
whether to tell the homeserver a session of room keys is verified.

Fixes https://github.com/vector-im/riot-web/issues/12599
2020-03-03 15:52:38 +00:00
J. Ryan Stinnett c96a906b39 Check both cross-signing and local trust for key sharing
When sharing room keys with our own devices, this ensure we check both
cross-signing and local trust.

Fixes https://github.com/vector-im/riot-web/issues/12596
2020-03-03 15:12:40 +00:00
RiotRobot da96765020 Merge branch 'master' into develop 2020-03-02 16:55:55 +00:00
RiotRobot f654c8a892 v5.1.0 2020-03-02 16:53:10 +00:00
RiotRobot 336fce55df Prepare changelog for v5.1.0 2020-03-02 16:53:10 +00:00
Zoe d11946d86b Merge pull request #1242 from matrix-org/foldleft/fix-bad-merge
Fixed up tests to match new way that crypto stores are created
2020-03-02 15:01:27 +00:00
Zoe 3a4c72ac08 actually, returning is unnecessary 2020-03-02 14:46:26 +00:00
Zoe 6d3f0f653b there's some days that the linter and i, we just really don't see eye-to-eye 2020-03-02 14:38:24 +00:00
Zoe 81d3534569 added return back 2020-03-02 13:06:13 +00:00
Zoe c54922dba3 Fixed up tests to match new way that crypto stores are created 2020-03-02 12:51:47 +00:00
Zoe a4ed3d97fc Merge pull request #1235 from matrix-org/foldleft/12299-local-ssk
Store USK and SSK locally
2020-03-02 09:52:44 +00:00
Zoe 656694ee00 proper spacing for test output text 2020-03-02 09:45:55 +00:00
Hubert Chathi c6b5936f8a use the right operator 2020-02-28 16:09:24 -05:00
Travis Ralston 03752ab60c Merge pull request #1236 from matrix-org/travis/unpadded-qr-codes
Use unpadded base64 for QR code secrets
2020-02-28 10:20:57 -07:00
Bruno Windels 7203542cfd Merge pull request #1239 from matrix-org/bwindels/dontrequiredoneforselfverif
Don't require .done event for finishing self-verification
2020-02-28 15:16:09 +00:00
Bruno Windels 4b36bbc122 Merge pull request #1237 from matrix-org/bwindels/dontcancelas3rdparty
Don't cancel as 3rd party in verification request
2020-02-28 15:15:49 +00:00
Bruno Windels ecaf21ceb0 Don't require .done event for finishing self-verification
Instead, call onVerifierFinished from the verifier on the request
so we can internally mark it as done. This flag is not persisted,
but we don't have historical (persisted) to-device requests anyway.
2020-02-28 14:56:38 +01:00
Zoe 67fe4e1460 lint & only cache valid keys 2020-02-28 11:04:28 +00:00
Zoe a94503ad03 address PR feedback 2020-02-28 10:43:57 +00:00
Bruno Windels ce6dd8688c Merge pull request #1234 from matrix-org/bwindels/evenmoreloggingforverif
Verification: log when switching start event
2020-02-28 10:24:09 +00: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
Bruno Windels 650eee7705 dont cancel as 3rd party in verification request 2020-02-27 18:38:16 +01:00
Travis Ralston 4510eb6540 Match all the equals
Co-Authored-By: Hubert Chathi <hubert@uhoreg.ca>
2020-02-27 10:10:24 -07:00
Travis Ralston 9a236f317d Use unpadded base64 for QR code secrets 2020-02-27 10:00:56 -07:00
Zoe 25c467d608 Wire cache through to matrix client 2020-02-27 16:53:26 +00:00
Zoe c2daf0d74e Store data in cryptostore 2020-02-27 16:53:26 +00:00
J. Ryan Stinnett fa19616ad1 Merge pull request #1233 from matrix-org/jryans/safari-e2e-idb
Perform crypto store operations directly after transaction
2020-02-27 16:48:09 +00:00
Zoe 02cbd33284 Added cache callbacks to CrossSigningInfo 2020-02-27 16:37:25 +00:00
Zoe 941ae18d74 Added tests for CrossSigningInfo.getCrossSigningKey 2020-02-27 16:37:25 +00:00
Bruno Windels 90f400abe1 log when switching start event 2020-02-27 17:35:58 +01:00
J. Ryan Stinnett ff2d93d421 Perform crypto store operations directly after transaction
At least on Safari but perhaps other browsers as well, you must perform
IndexedDB operations in the same JS task as you start the transaction. As a
concrete example, you cannot open the transaction and await some promise before
actually using it.

This fixes the crypto store to meet this requirement.

Fixes https://github.com/vector-im/riot-web/issues/12207
2020-02-27 14:57:07 +00:00
Bruno Windels 8d26bd9a17 Merge pull request #1232 from matrix-org/bwindels/logeventidinverifreq
More verification request logging
2020-02-27 13:26:53 +00:00
J. Ryan Stinnett a9fa0484ff Add exception handling to crypto store paths
A few of the crypto store backend paths were missing try / catch wrappers to
abort the transaction if the inner callback throws.
2020-02-27 12:26:18 +00:00
J. Ryan Stinnett d3d12ab62f Merge pull request #1231 from matrix-org/jryans/upgrade-deps-2020-02-26
Upgrade deps
2020-02-27 11:24:33 +00:00
Bruno Windels 1e29b1a31d log event id in verif request to differentiate between double processing vs double sending 2020-02-26 18:49:18 +01:00
J. Ryan Stinnett 9318bf5f2f Upgrade deps 2020-02-26 15:00:43 +00:00
RiotRobot 6b35302442 v5.1.0-rc.1 2020-02-26 14:16:57 +00:00
RiotRobot 2937e58215 Prepare changelog for v5.1.0-rc.1 2020-02-26 14:16:57 +00:00
J. Ryan Stinnett d42589b6cc Merge pull request #1230 from matrix-org/jryans/dist-tags
Add latest dist-tag for releases
2020-02-26 14:14:09 +00:00
J. Ryan Stinnett 26e9dfb4fb Add latest dist-tag for a release 2020-02-26 14:07:20 +00:00
J. Ryan Stinnett f27d03a6bc Always publish to next tag
This ensures that anyone who wants the latest version (pre-release or final
release) can always use the `next` tag.
2020-02-26 13:55:46 +00:00
J. Ryan Stinnett b1e3150a81 Reset device list dirty flag only after writing
This ensures we wait until after the device list writes to the crypto store
before marking thing as clean. This is particularly important for the error
path, as the write to the crypto store can fail.

Part of https://github.com/vector-im/riot-web/issues/12207
2020-02-25 17:56:47 +00:00
Hubert Chathi 5d52053caa use symmetric encryption for SSSS 2020-02-24 17:38:53 -05:00
Bruno Windels ce668d051c Merge pull request #1225 from matrix-org/bwindels/aliasautocomplete
Add room method for alt_aliases
2020-02-24 12:17:45 +00:00
David Baker e06579ecf5 Merge pull request #1227 from matrix-org/dbkr/move_bk_pipelines
Remove buildkite pipeline
2020-02-21 17:34:09 +00:00
David Baker 6c30af245c Remove buildkite pipeline
Now moved to the pipelines repo
2020-02-21 17:21:42 +00:00
Bruno Windels c9c40a6dde Merge pull request #1226 from matrix-org/bwindels/dontfailonhistoricalcancelafterstart
don't assume verify has been called when receiving a cancellation in verifier
2020-02-21 17:19:03 +00:00
Travis Ralston e748ac3d00 Merge pull request #1221 from matrix-org/travis/qr-binary
Reduce secret size for new binary packing
2020-02-21 10:05:03 -07:00
Bruno Windels aec79f3a79 don't assume verify has been called when receiving a cancellation in verifier 2020-02-21 17:26:29 +01:00
Hubert Chathi bf92cb1522 try to re-fetch devices before giving up on trying to heal a broken olm (#1224) 2020-02-21 10:20:46 -05:00
Bruno Windels 14e1920ff5 fix docs parser error 2020-02-21 13:43:08 +01:00
Bruno Windels c95cdf5a11 add room method for alt_aliases 2020-02-21 13:37:14 +01:00
Bruno Windels c14d0616ea always return null if there is no canonical alias 2020-02-21 13:36:52 +01:00
Hubert Chathi 0112701145 Merge pull request #1223 from matrix-org/uhoreg/misc_rageshake_fixes
misc rageshake fixes
2020-02-20 16:28:49 -05:00
Hubert Chathi cb69515be9 add some logging when sender could not establish an olm session 2020-02-20 14:49:32 -05:00
Hubert Chathi 3cd791e08f add function for getting the user's curve25519 key 2020-02-20 14:44:28 -05:00
Hubert Chathi 6e233e860e remove leftover debugging messages 2020-02-20 14:43:59 -05:00
Hubert Chathi b4f0ea441b remove obsolete comment 2020-02-20 14:43:24 -05:00
Bruno Windels 39974d3a61 Merge pull request #1220 from matrix-org/bwindels/fixhistoricalcancelledrequests
Fix cancelled historical requests not appearing as cancelled
2020-02-20 17:07:35 +00:00
Bruno Windels a998006842 Merge pull request #1217 from matrix-org/bwindels/fixqrcode
Fix renaming error that broke QR code verification
2020-02-20 11:00:39 +00:00
Travis Ralston 765fbe2182 Reduce secret size for new binary packing
See https://github.com/matrix-org/matrix-react-sdk/pull/4091
2020-02-19 17:21:56 -07:00
Bruno Windels 08dfa73b57 pending excludes observeOnly now, still allow observeOnly requests to get cancelled 2020-02-19 17:51:53 +01:00
RiotRobot a58e7a34e7 v5.0.1 2020-02-19 15:03:04 +00:00
RiotRobot 7a481beec6 Prepare changelog for v5.0.1 2020-02-19 15:03:03 +00:00
Bruno Windels d51fad2de4 Merge pull request #1219 from matrix-org/bwindels/fixaliases
add method for new /aliases endpoint
2020-02-19 10:02:32 +00:00
Bruno Windels c66755a756 jsdoc 2020-02-19 10:13:32 +01:00
Bruno Windels 886ad03505 add method to check server feature flag 2020-02-19 10:08:05 +01:00
Bruno Windels ba33ef0a68 use unstable prefix 2020-02-19 10:07:52 +01:00
Bruno Windels fe97dc3ece add method for new /aliases endpoint 2020-02-18 15:33:41 +01:00
Bruno Windels 76c4875088 fix targetDevice renaming 2020-02-18 11:23:04 +01:00
Bruno Windels 04a3aaee35 Merge pull request #1213 from matrix-org/bwindels/filterverifmethods
method for checking if other party supports verification method
2020-02-18 10:15:49 +00:00
Bruno Windels fef03cda9b Update src/crypto/verification/request/VerificationRequest.js
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-02-18 10:03:02 +00:00
Bruno Windels 3292fde41b Merge pull request #1210 from matrix-org/bwindels/localecho2
add local echo state for accepting or declining a verif req
2020-02-18 09:55:09 +00:00
RiotRobot 38cf25ac5a Merge branch 'master' into develop 2020-02-17 11:58:01 +00:00
RiotRobot 13d5d2f958 v5.0.0 2020-02-17 11:55:26 +00:00
RiotRobot 7f6b66c824 Prepare changelog for v5.0.0 2020-02-17 11:55:25 +00:00
Bruno Windels 62c344b633 Merge pull request #1214 from matrix-org/bwindels/workswithrageshakes
make logging compatible with rageshakes
2020-02-14 16:39:05 +00:00
Bruno Windels 75ce2729f9 comment typo 2020-02-14 17:35:07 +01:00
Bruno Windels 6669554867 make logging compatible with rageshakes 2020-02-14 17:31:40 +01:00
Bruno Windels d3294da37c Merge pull request #1209 from matrix-org/bwindels/oneverifrequest
Find existing requests when starting a new verification request
2020-02-14 15:33:06 +00:00
Bruno Windels 9b56bf25cf Update src/crypto/verification/request/InRoomChannel.js
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-02-14 14:43:50 +00:00
Bruno Windels e1a33d8a7b Update src/crypto/verification/request/ToDeviceChannel.js
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-02-14 13:41:38 +00:00
Bruno Windels 47a1224c13 Merge pull request #1211 from matrix-org/bwindels/logsasmac
log MAC calculation during SAS
2020-02-14 12:54:40 +00:00
Bruno Windels 5c57d81e94 method for checking if other party supports verification method 2020-02-14 13:47:24 +01:00
Bruno Windels edefd3ec88 log MAC calculation 2020-02-14 12:20:02 +01:00
Bruno Windels f15098efde add local echo state for accepting or declining a verif req 2020-02-13 17:27:18 +01:00
RiotRobot 8ee99a0616 v5.0.0-rc.1 2020-02-13 15:41:46 +00:00
RiotRobot 3ace1d04cd Prepare changelog for v5.0.0-rc.1 2020-02-13 15:41:45 +00:00
Bruno Windels 365bb772bc also find existing request for to-device verification 2020-02-13 15:37:21 +01:00
Bruno Windels 5ee6ada973 use pending instead of individual checks 2020-02-13 15:37:04 +01:00
Bruno Windels ee0fa0e687 fix lint 2020-02-13 14:47:35 +01:00
Bruno Windels 0d41f6aafc remove commented out logging 2020-02-13 14:36:18 +01:00
Bruno Windels 91b6499815 more consistent naming 2020-02-13 14:36:09 +01:00
Bruno Windels 7cd1166a47 allow finding existing verif req without starting a new one 2020-02-13 14:31:33 +01:00
Bruno Windels f76cb677ff store sasEvent on verifier so we can get it if we missed show_sas event 2020-02-13 14:31:03 +01:00
Bruno Windels 05e7f4e6f7 look for existing verification request when trying to start a new one 2020-02-13 14:30:38 +01:00
Bruno Windels 6684574bdf Merge pull request #1206 from matrix-org/bwindels/dontpassmethodstoverify
Remove methods argument to verification
2020-02-13 08:51:27 +00:00
Hubert Chathi 36a945f8e2 Merge pull request #1207 from matrix-org/uhoreg/fix_opts_request
don't do a dynamic import of request
2020-02-11 13:54:12 -05:00
Hubert Chathi 6a3d322033 don't do a dynamic import of request 2020-02-11 13:02:34 -05:00
Bruno Windels 00c003ec65 remove methods arg to requestVerification(DM)
as it's easy to have this argument be out of sync from all
the places this is called from the js-sdk. There is also little point,
as you can already specify the methods a consumer of the js-sdk
wants to provide through the verificationMethods option when creating
the client object.
2020-02-11 17:42:49 +01:00
Bruno Windels f4d335c161 use default methods if none are provided to the client 2020-02-11 17:42:17 +01:00
Bruno Windels 659f42139b Merge pull request #1201 from matrix-org/travis/wip/qr
QR self-verification fixes
2020-02-11 15:17:02 +00:00
Bruno Windels 0e791ed022 Merge pull request #1204 from matrix-org/bwindels/logverif
Log every verification event
2020-02-11 13:17:23 +00:00
Bruno Windels 48655aa1a3 log every verification event 2020-02-11 10:08:17 +01:00
Bruno Windels 83fa80cfda Merge pull request #1203 from matrix-org/bwindels/dontrequiredoneconfirmation
dont require .done event from other party
2020-02-11 08:18:46 +00:00
Bruno Windels cf5b5ee085 dont require .done event from other party 2020-02-10 18:00:24 +01:00
Bruno Windels 429a4e3526 fix lint 2020-02-10 17:21:22 +01:00
Zoe d66d4c1cd9 Merge pull request #1202 from matrix-org/foldleft/12221-reset-cross-signing
New option to fully reset Secret Storage keys in boostrapSecretStorage
2020-02-10 09:59:28 +00:00
Zoe 7a1bbdf2dd oops 2020-02-07 15:51:27 +00:00
Travis Ralston 29c1459568 Merge pull request #1190 from matrix-org/travis/qr-code-request-based
Add function to estimate target device for a VerificationRequest
2020-02-07 15:37:49 +00:00
Travis Ralston efad46a8a4 Rename target device prop 2020-02-07 15:37:34 +00:00
Zoe a69c621305 New option to fully reset Secret Storage keys in boostrapSecretStorage 2020-02-07 14:45:10 +00:00
Bruno Windels ad6dde6f26 Merge pull request #1200 from matrix-org/bwindels/4sunlockpurpose
pass ssss item name to callback so we can differentiate UI on it
2020-02-07 08:58:43 +00:00
Bruno Windels 2627e46723 add jsdoc for new param 2020-02-06 18:43:46 +01:00
Bruno Windels 408d70b55e pass ssss item name to callback so we can differentiate UI on it 2020-02-06 16:54:12 +01:00
Hubert Chathi 3f369e528b Merge pull request #1167 from cedricvanrompay/1-olm-device-export-import
add export/import of Olm devices
2020-02-05 20:09:01 -05:00
Zoe 312976294b Merge pull request #1199 from matrix-org/foldleft/types-for-utils
Convert utils.js -> utils.ts
2020-02-05 12:41:17 +00:00
Zoe 77f42c479b Update src/utils.ts
Co-Authored-By: Travis Ralston <travpc@gmail.com>
2020-02-05 11:50:39 +00:00
Zoe d60bd22674 actually let's not get into the business of writing types for our deps 2020-02-05 11:43:11 +00:00
Zoe 2e67f77d3e compiler flags 2020-02-05 11:17:55 +00:00
Zoe 6d8e8e6bd7 fix tests 2020-02-05 11:07:55 +00:00
Zoe 9c01945a05 copyright notice *sigh* 2020-02-05 10:23:24 +00:00
Zoe 7ce5ddd380 lint 2020-02-05 10:14:26 +00:00
Zoe 2b5de914f5 review feedback 2020-02-05 09:57:46 +00:00
Zoe 18a2426707 Convert utils.js -> utils.ts 2020-02-04 19:09:48 +00:00
David Baker 367fac6d54 Merge pull request #1197 from matrix-org/dbkr/stop_signing_yourself
Don't sign ourselves as a user
2020-02-04 14:31:08 +00:00
David Baker 157cc9e5eb Merge remote-tracking branch 'origin/develop' into dbkr/stop_signing_yourself 2020-02-04 14:26:58 +00:00
David Baker 81daf12598 Merge pull request #1196 from matrix-org/dbkr/verfication_logging
Add a bunch of logging to verification
2020-02-04 14:21:08 +00:00
Bruno Windels 9249b0652f Merge pull request #1198 from matrix-org/bwindels/fixverifroomeventtype
Fix: always return a valid string from InRoomChannel.getEventType
2020-02-04 14:16:14 +00:00
Bruno Windels ee4c6b6265 Merge pull request #1195 from matrix-org/bwindels/logoncancel
add logging when a request is being cancelled
2020-02-04 13:08:13 +00:00
David Baker 68deab4a68 We still need to mark our master key locally verified 2020-02-04 12:27:53 +00:00
Bruno Windels c9c765b5b8 fix getEventType 2020-02-04 13:12:38 +01:00
David Baker 616f73d8c6 forgive me, o great linter 2020-02-04 12:12:02 +00:00
Bruno Windels 208c371afb add failing test for getEventType 2020-02-04 13:10:06 +01:00
David Baker 3a59cfa9c0 Don't sign ourselves as a user 2020-02-04 12:09:42 +00:00
David Baker cf94527bd5 Add a bunch of logging to verification
So we have a better idea of what's going on
2020-02-04 12:04:50 +00:00
Travis Ralston fa93479863 Merge pull request #1194 from matrix-org/travis/fix-type
Don't explode verification validation if we don't have an event type
2020-02-04 11:48:25 +00:00
Bruno Windels 8bc0ef8c27 add logging when a request is being cancelled
so we can more easily see (especially for to_device requests)
why something was cancelled
2020-02-04 12:48:02 +01:00
Travis Ralston bd403b6d87 Don't explode verification validation if we don't have an event type
I don't know why this is undefined at this point, or why membership events are ending up here, but this fixes develop for people.

See https://github.com/vector-im/riot-web/issues/12231
2020-02-04 11:46:31 +00:00
Bruno Windels 57a7328065 Merge pull request #1193 from matrix-org/bwindels/dontshowverifrequestnotforme
Fix: verification request appearing for users that are not the receiver or sender if they are in room
2020-02-04 09:39:45 +00:00
Bruno Windels 4945463beb fix lint 2020-02-03 20:12:21 +01:00
Bruno Windels dfafa791f2 fix getOtherPartyUserId 2020-02-03 19:17:40 +01:00
Bruno Windels 5f2cb6b3a4 only an m.room.message with msgtype can be a .request 2020-02-03 19:17:18 +01:00
Bruno Windels 5398fac348 add (failing) tests for getEventType and getOtherPartyUserId 2020-02-03 19:16:48 +01:00
Cédric Van Rompay b217f6aa81 minor doc update (with sign-off)
Signed-off-by: Cédric Van Rompay <cedric.vanrompay@gmail.com>
2020-02-03 10:32:32 +01:00
Cédric Van Rompay ec597bea93 fix new way of calling OlmDevice.init 2020-02-03 10:27:10 +01:00
Cédric Van Rompay 7a5c54fef7 set pickle key through OlmDevice.init 2020-02-03 09:58:18 +01:00
David Baker 4064f18de2 Merge pull request #1192 from matrix-org/dbkr/fix_passthrough_key_get
Fix getting secrets encoded with passthrough keys
2020-02-02 19:12:39 +00:00
David Baker 6d13457172 Fix getting secrets encoded with passthrough keys 2020-02-01 17:29:08 +00:00
Travis Ralston f39518ef93 Unreviewed crypto verification for self 2020-02-01 10:49:32 +00:00
Bruno Windels 4b1cecd246 also set the deviceId on .ready so we know who to send .start to 2020-01-31 14:50:48 +01:00
Cédric Van Rompay 352509fd3a Update src/crypto/OlmDevice.js
Co-Authored-By: Hubert Chathi <hubert@uhoreg.ca>
2020-01-31 11:53:20 +01:00
Cédric Van Rompay d0f08f8839 Update src/crypto/OlmDevice.js
Co-Authored-By: Hubert Chathi <hubert@uhoreg.ca>
2020-01-31 11:53:09 +01:00
Cédric Van Rompay efd38a3471 Update src/crypto/OlmDevice.js
Co-Authored-By: Hubert Chathi <hubert@uhoreg.ca>
2020-01-31 11:52:54 +01:00
Cédric Van Rompay a4e74fea94 fix linting errors 2020-01-31 11:51:17 +01:00
Travis Ralston fdb33b6189 Merge remote-tracking branch 'origin/bwindels/todevicereadystartdone' into travis/wip 2020-01-30 18:06:31 +00:00
Bruno Windels dcbb67838b for the right panel to work, the verifier should send .done events 2020-01-30 18:46:22 +01:00
Bruno Windels 1727d636a3 don't assume both parties have a different userId in verif ping-pong 2020-01-30 18:45:54 +01:00
Travis Ralston 9eadc7f868 Add function to estimate target device for a VerificationRequest
For https://github.com/matrix-org/matrix-react-sdk/pull/4001
2020-01-30 16:57:01 +00:00
Travis Ralston 620118af5f Merge pull request #1175 from matrix-org/travis/update-qr-code
Update QR code handling for new spec
2020-01-30 11:25:57 +00:00
Travis Ralston 3645764f9a Appease the linter 2020-01-30 11:15:25 +00:00
Travis Ralston 769bfeb10f Verify all the things 2020-01-30 11:10:25 +00:00
Travis Ralston 5fbaa9cfa7 Fix verification of the master key 2020-01-29 18:06:25 +00:00
Travis Ralston 007508ba12 Merge branch 'develop' into travis/update-qr-code 2020-01-29 16:57:57 +00:00
David Baker 0f1f18b232 Merge pull request #1188 from matrix-org/dbkr/dont_add_epemeral_events_to_timeline_when_peeking
Don't add ephemeral events to timeline when peeking
2020-01-29 15:17:27 +00:00
David Baker d6b754b133 Merge pull request #1189 from matrix-org/dbkr/be_prepaed
Fix typo
2020-01-29 15:16:56 +00:00
Travis Ralston 1b80c83676 Merge branch 'develop' into travis/update-qr-code 2020-01-29 15:11:06 +00:00
Travis Ralston ec4dc582b6 Remove tests for old QR code stuff 2020-01-29 15:10:35 +00:00
David Baker 65646ff9e2 Fix typo
This would probably just cause apps to wait until the first live
sync had finished rather than the one from the store, so slowing
them down / breaking offline support.
2020-01-29 15:06:19 +00:00
Travis Ralston 92f6ec918b Appease the linter 2020-01-29 15:06:13 +00:00
David Baker 62bd41d2e6 Don't add ephemeral events to timeline when peeking
As hopefully explained by comment.

Fixes https://github.com/vector-im/riot-web/issues/11120
2020-01-29 15:04:09 +00:00
Bruno Windels 9d864ffd60 Merge pull request #1187 from matrix-org/bwindels/fixstartrace-rebased
Verification: resolve race between .start events from both parties
2020-01-29 15:04:00 +00:00
Travis Ralston c45b38cece Actually do the verification 2020-01-29 14:56:28 +00:00
Travis Ralston 0d7aee2c36 Misc cleanup 2020-01-29 14:52:04 +00:00
Travis Ralston be345a523f Fix verification flow 2020-01-29 14:43:37 +00:00
Bruno Windels 470bdf8741 fix tests 2020-01-29 15:19:18 +01:00
Bruno Windels 59319fb55b use logger instead of console 2020-01-29 15:19:07 +01:00
Bruno Windels fb7695fdbc fix unrelated issue: errorFactory returns function, so call it 2020-01-29 15:18:48 +01:00
Bruno Windels 25b7552683 startEvent can always be passed to verifier
as we'll check the sender there to see on which side we are
2020-01-29 15:18:18 +01:00
Bruno Windels 21d520378f apply same algo to pick .start event initially when changing phase
smallest sender userid wins
2020-01-29 15:17:36 +01:00
Bruno Windels 9cd6607520 attempt to switch start event if we already have a verifier 2020-01-29 15:16:54 +01:00
Bruno Windels efd3550f53 support switching startEvent while waiting for .accept on initiator side
if we get a .start event from the other party and we've also sent one,
the .start event with the sender that is first in sorting order should
be taken, and the other one ignored.

At the point where we will receive it, the verifier has already
been returned from beginKeyVerification, so we'll need to switch
start event internally, and retry the verification, now on the
receiver (sending .accept) side instead of initiator side
(sending .start).
2020-01-29 15:13:59 +01:00
Travis Ralston 76402ec8d7 Lie to the verification handling 2020-01-29 13:45:02 +00:00
Travis Ralston f689142806 Define NAME as a property higher up 2020-01-29 10:52:26 +00:00
Travis Ralston fd563bda6a Remove irrelevant verification flows for QR codes
You can't actually get at these through our verification framework - they scan/show steps are pre-verification framework.
2020-01-29 09:26:29 +00:00
Travis Ralston 09a8f7122c Merge branch 'develop' into travis/update-qr-code 2020-01-29 00:18:15 +00:00
David Baker 608fb00844 Merge pull request #1184 from matrix-org/dbkr/new_keybackup_in_bootstrap
Add option to bootstrap to start new key backup
2020-01-28 22:02:10 +00:00
David Baker 5c45e9c306 Add option to bootstrap to start new key backup
The key backup needs to be signed by the cross-signing key so
doing it here allows us to do it before we blow the private part
out of memory.
2020-01-28 19:36:00 +00:00
Travis Ralston 950221dc13 Merge branch 'develop' into travis/update-qr-code 2020-01-28 17:27:38 +00:00
Travis Ralston f816679596 Merge pull request #1182 from matrix-org/travis/null-guards
Add a bunch of null guards to feature checks
2020-01-28 14:24:53 +00:00
Travis Ralston 80ccf18b16 Merge pull request #1183 from salzig/docs/fix_matrix_client_reference
docs: fix MatrixClient reference
2020-01-28 14:13:59 +00:00
Ben Rexin c7abd9062a docs: fix MatrixClient reference 2020-01-28 15:05:13 +01:00
Travis Ralston 4287f2229b Add a bunch of null guards to feature checks 2020-01-28 13:21:01 +00:00
Michael Telatynski 8408055137 Merge pull request #1180 from matrix-org/t3chguy/cs_verification_decoration
Add helper to obtain the cancellation code for a verification request
2020-01-28 11:23:24 +00:00
Michael Telatynski cc0965d703 s/^t/T/ 2020-01-28 11:19:05 +00:00
Michael Telatynski 94b3d9d3e1 Add helper to obtain the cancellation code for a verification request 2020-01-28 11:15:07 +00:00
J. Ryan Stinnett 772bf7d6ff Merge pull request #1178 from matrix-org/jryans/tag-prerelease-next
Publish pre-releases as a separate tag on npm
2020-01-27 22:32:56 +00:00
J. Ryan Stinnett 15c2e4bb07 Publish pre-releases as a separate tag on npm
npm will install the newest version a package has published to the `latest` tag,
including pre-releases, which is not ideal since those may not be ready for
production use yet.

This uses an alternate tag (`next` is a common convention, but it can be
anything) for pre-releases so the default installs only get stable versions.

Fixes https://github.com/vector-im/riot-web/issues/12029
2020-01-27 20:40:35 +00:00
Travis Ralston 419693023f Add untested reciprocate function 2020-01-27 11:41:52 -07:00
Travis Ralston 2d081f2c19 Merge branch 'develop' into travis/update-qr-code 2020-01-27 11:41:05 -07:00
David Baker c76ce1fd85 Merge pull request #1177 from matrix-org/dbkr/fix_passthrough_keys
Fix support for passthrough keys
2020-01-27 16:55:27 +00:00
David Baker f38b4d37e6 Check for the whole thing being null 2020-01-27 16:25:08 +00:00
David Baker 73c92dfc57 Merge pull request #1174 from matrix-org/dbkr/trust_cross_signing_on_verify
Trust our own cross-signing keys if we verify them with another device
2020-01-27 16:11:08 +00:00
David Baker 61c5430deb Fix support for passthrough keys
and add code to fix up ones mis-stored by the old code
2020-01-27 15:50:01 +00:00
J. Ryan Stinnett 21e4c597d9 Merge pull request #1176 from matrix-org/jryans/await-device-list
Ensure cross-signing keys are downloaded when checking trust
2020-01-27 15:34:19 +00:00
J. Ryan Stinnett 4dbeee8cb3 Ignore downloading for tests 2020-01-27 15:28:36 +00:00
J. Ryan Stinnett adc76c636e Merge pull request #1172 from matrix-org/bwindels/reduceveriflogging
Don't log verification validation errors for normal messages
2020-01-27 15:04:00 +00:00
J. Ryan Stinnett 0dbf89b2b4 Ensure cross-signing keys are downloaded when checking trust
When checking cross-signing trust during login, we may not have downloaded keys
yet. This ensures we make an attempt first if needed.

Fixes https://github.com/vector-im/riot-web/issues/12068
2020-01-27 14:55:20 +00:00
Travis Ralston 83241ac17d Update QR code handling for new URL
This doesn't have any meaningful change on the process, just makes it more in line with what we do.
2020-01-27 06:59:04 -07:00
Cédric Van Rompay 6aa5d39357 move new example to own directory 2020-01-27 14:28:50 +01:00
Cédric Van Rompay 1304ecbe03 factor out _initializeFromExportedDevice 2020-01-27 14:12:43 +01:00
RiotRobot aafc027812 Merge branch 'master' into develop 2020-01-27 11:31:09 +00:00
Cédric Van Rompay 3a4b6f0ea0 rename "kwargs" to "opts" 2020-01-27 11:48:28 +01:00
Cédric Van Rompay b3d10ace21 mention export method in import 2020-01-27 11:45:17 +01:00
Cédric Van Rompay c17df7a6f7 fix typo in comments 2020-01-27 11:42:15 +01:00
David Baker 1c13f5026e Merge pull request #1173 from matrix-org/dbkr/fix_bootstrap_cleanup
Fix bootstrap cleanup
2020-01-27 10:18:37 +00:00
David Baker b9cfede888 Trust our own cross-signing keys if we verify them with another device 2020-01-25 20:38:11 +00:00
David Baker 49fd9e90a0 this can be const now 2020-01-25 19:48:36 +00:00
David Baker e09038232e Fix bootstrap cleanup
As hopefully explained in the comment. The symptom of this was that
bootstrapping would work just fine the first time you called it
in any run of the app, but then if called a second time (eg. if you
cancelled by dismissing the password prompt) it would create keys and
upload the public parts but not store the private parts in SSSS,
leaving you with cross signing keys you don't have the private parts
of.

Also use object.assign in the save keys callback just in case we
ever reset a subset of the keys (and also because it makes it a
bit simpler to reason about what objects are where).
2020-01-25 19:42:02 +00:00
Travis Ralston 2cfe310e89 Merge pull request #1155 from matrix-org/travis/qr-verif-rp
QR code verification
2020-01-24 08:55:42 -07:00
Bruno Windels 973c7467e8 Merge pull request #1171 from matrix-org/bwindels/fixverifyowndevice
expose deviceId prop on device channel
2020-01-24 11:24:23 +00:00
Bruno Windels 583df7ed7d don't log verification validation errors for normal messages 2020-01-24 12:23:18 +01:00
Bruno Windels 6d05376f04 expose deviceId prop on device channel
used to check if a verification came through to_device in the toast
2020-01-24 12:01:20 +01:00
Cédric Van Rompay e1f832bfa7 fix linting errors 2020-01-24 09:20:43 +01:00
Travis Ralston b8092cd00b Make the tests pass 2020-01-23 20:41:52 -07:00
Travis Ralston 3c1dca6cef Generate a shared secret if we don't have one 2020-01-23 20:15:02 -07:00
Travis Ralston c0f7dd6fe9 Fix secret size 2020-01-23 20:06:04 -07:00
Travis Ralston 6af6e99480 Expose the request event more readily for consumers 2020-01-23 20:05:56 -07:00
Travis Ralston c5cbe48668 Remove docs too 2020-01-23 19:29:42 -07:00
Travis Ralston 15707956ef Remove private key accessors for cross-signing 2020-01-23 19:29:42 -07:00
Travis Ralston 4668fc87a1 Add cross-signing accessors and QR code stuff 2020-01-23 19:29:42 -07:00
Jack Works 468fb2cc41 chore: remove custom promise, use es6 standard
Signed-off-by: Jack Works <jackworks@protonmail.com>
2020-01-23 19:23:08 -07:00
Jack Works 7c79e7e836 fix: typos
Signed-off-by: Jack Works <jackworks@protonmail.com>
2020-01-23 19:21:19 -07:00
Travis Ralston 0bf1f48623 Merge pull request #1169 from matrix-org/travis/fix-build
Move & upgrade babel runtime into dependencies (like it wants)
2020-01-23 15:45:53 -07:00
Travis Ralston f286eb4d11 Move & upgrade babel runtime into dependencies (like it wants)
https://babeljs.io/docs/en/babel-runtime
2020-01-23 15:44:36 -07:00
Cédric Van Rompay 9346c83dc1 fix destructuration of potentially nil value 2020-01-23 18:53:42 +01:00
Bruno Windels a76267f5b0 Merge pull request #1166 from matrix-org/bwindels/verifyowndevicechecks
Add unit tests for verifying your own device, remove .event property on verification request
2020-01-23 17:01:43 +00:00
Cédric Van Rompay 1d3a7b3d52 add example for export/import in browser 2020-01-23 16:55:16 +01:00
Cédric Van Rompay f78f04d553 userId must be included in exported data 2020-01-23 16:55:16 +01:00
Cédric Van Rompay 7b6dabbe9c add high-level export/import methods
not sure how to test these high-level methods though
2020-01-23 16:55:16 +01:00
Cédric Van Rompay ed01b3b8cf stop checking structure of exported data
it should suffice that the exported data
allows to recreate a device that can do crypto
2020-01-23 16:55:16 +01:00
Cédric Van Rompay 7880a30e57 add importing in OlmDevice.init() 2020-01-23 16:55:16 +01:00
Cédric Van Rompay 3a3ff93450 improve export doc 2020-01-23 16:55:16 +01:00
Cédric Van Rompay 3a1cdd37a3 move export test with other Olm tests to have active sessions 2020-01-23 16:55:16 +01:00
Cédric Van Rompay 8db38f8e75 fix output of getAllEndToEndSessions 2020-01-23 16:55:16 +01:00
Cédric Van Rompay ff24ef4ee5 add OlmDevice.prototype.export
- only exporting account and P2P sessions
- test is halfway done:
  - it only prints the export result instead of running assertions on it
  - there are no sessions to export

Note: to run only the added test:

    node_modules/.bin/jest spec/unit/crypto/algorithms/olm.spec.js --testEnvironment node --testNamePattern OlmDevice
2020-01-23 16:55:16 +01:00
Bruno Windels 3faeec4add fix lint 2020-01-23 15:59:47 +01:00
Bruno Windels 7d56ee5084 with the change in the linked react-sdk PR, event isn't used anymore 2020-01-23 15:52:23 +01:00
Bruno Windels b2afaabb8c add unit tests for verifying your own device over to_device messages 2020-01-23 15:52:07 +01:00
Bruno Windels 3efaf90bc8 Merge pull request #1163 from matrix-org/bwindels/verificationaccceptedbyotherdevice
For dm-verification, also consider events sent by other devices of same user as "our" events
2020-01-23 13:27:15 +00:00
Bruno Windels 0c52887688 copyright year
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-01-23 10:51:19 +00:00
David Baker d5e9155a33 Merge pull request #1161 from matrix-org/dbkr/prepublish
Add a prepare script
2020-01-22 20:56:59 +00:00
Michael Telatynski 5def5ab074 Merge pull request #1162 from matrix-org/t3chguy/crypto/keys/upload/deviceId
Remove :deviceId from /keys/upload/:deviceId as not spec-compliant
2020-01-22 17:13:21 +00:00
Bruno Windels 1b242e636b remove obsolete comment 2020-01-22 17:39:21 +01:00
Bruno Windels 05f05c889a don't verify in observeOnly mode 2020-01-22 17:39:21 +01:00
Bruno Windels 1367e285c8 have channel decide what is considered "sent by us"
for in room verification, if another client accepts the request,
we still want to observe so those events should still be
considered ours, so looking at from_device doesn't work there.
2020-01-22 17:39:21 +01:00
Bruno Windels 45ec3e0bb9 also emit if the phase didn't change but observeOnly did 2020-01-22 17:39:21 +01:00
Bruno Windels dc38f78da2 add unit tests for verification request 2020-01-22 17:39:21 +01:00
Michael Telatynski 1b6a74fd93 Remove :deviceId from /keys/upload/:deviceId as not spec-compliant 2020-01-22 15:20:13 +00:00
David Baker 9d8a1494aa Turns out prepublish is deprecated and should be prepare 2020-01-22 14:14:24 +00:00
David Baker 08465cf236 Add a prepublish script
So we actually build the lib directory before publishing it
2020-01-22 14:09:01 +00:00
Damir Jelić 7016848401 Merge branch 'poljar/timeline-window-refactor' into develop 2020-01-21 17:01:16 +01:00
poljar bdd2a9e7e8 timeline-window: Small docfix.
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-01-21 16:55:12 +01:00
David Baker 80256e6782 Merge pull request #1158 from matrix-org/dbkr/upload_device_keys_empty_auth
Allow a device key upload request without auth
2020-01-21 15:03:58 +00:00
Damir Jelić 7907ef44f8 timeline-window: Refactor out and expose the logic to extend the window. 2020-01-21 15:42:55 +01:00
Damir Jelić 3a97a24686 timeline-window: Refactor out the TimelineIndex getting logic. 2020-01-21 15:21:10 +01:00
David Baker 7f208ed44e Allow a device key upload request without auth
This is useful for querying the supported auth methods.
2020-01-21 11:35:49 +00:00
Bruno Windels 22e6cfaebb Merge pull request #1140 from matrix-org/bwindels/verification-right-panel
Support for .ready verification event (MSC2366) & other things
2020-01-20 17:17:51 +00:00
Bruno Windels 9d6f873048 remove obsolete and now broken method
a request should be accepted by calling accept() on the request.
2020-01-20 18:13:18 +01:00
Bruno Windels d526229a0f update jsdoc of requestVerificationDM
which now returns a Promise of VerificationRequest instead of verifier
2020-01-20 18:12:52 +01:00
Bruno Windels aac68290ac remove obsolete comment 2020-01-20 17:56:28 +01:00
Bruno Windels bd9a2c13eb implement API change in sas test for requestVerificationDM 2020-01-20 17:55:48 +01:00
Bruno Windels e5c65d53f8 set transaction_id for remote echos in TestClient
as InRoomChannel looks at this to decide whether an event is
a remote echo (and to pass it to the verifier or not)
2020-01-20 17:54:26 +01:00
Bruno Windels 121e9d0225 don't overwrite a request when the remote echo arrives before event_id 2020-01-20 17:39:18 +01:00
Bruno Windels c12a3b6610 more fixup: make sure remote echo doesn't arrive earlier for TestClient 2020-01-20 17:35:44 +01:00
Bruno Windels 77d0a76186 fixup: another timeout 2020-01-20 14:52:34 +01:00
Bruno Windels e89528315d enable fake timers for consistency
although it doesn't make or break the test
2020-01-20 14:04:32 +01:00
Bruno Windels c34ccc9d53 adjust test: requestVerification returns the request instead of verifier 2020-01-20 14:03:43 +01:00
Bruno Windels e51ba795f3 to make this work while using fake timers, don't use setTimeout
but instead use Promise.resolved() as then always runs in the next tick.
2020-01-20 13:56:39 +01:00
Bruno Windels cbe2965849 mention reason in cancellation error 2020-01-17 19:01:30 +01:00
Bruno Windels 59bfc45856 use setTimeout of setInterval 2020-01-17 19:01:08 +01:00
Bruno Windels 07cc93cca2 fix lint 2020-01-17 16:58:19 +01:00
Travis Ralston 1205178e26 Merge branch 'develop' into bwindels/verification-right-panel 2020-01-16 13:13:00 -07:00
Bruno Windels 72fd1e4e7c add note to fix bug later 2020-01-03 18:21:33 +01:00
Bruno Windels f44e0a8e12 parenthesis in wrong place broke logic 2020-01-03 18:21:18 +01:00
Bruno Windels 9338d9c2a6 commit logging 2020-01-03 18:20:59 +01:00
Bruno Windels 75fc25feb5 fix method names 2020-01-03 18:20:50 +01:00
Bruno Windels 5919874f6f check !unsent instead of requested for emitting the crypto.request event 2020-01-03 18:20:16 +01:00
Bruno Windels 213bb9dba2 allow to move straight from UNSENT to STARTED
this was one of the things breaking to_device verification
2020-01-03 18:19:49 +01:00
Bruno Windels 3a9dc37d02 new state machine relies on having remote echos, so fake for to_device 2020-01-03 18:18:39 +01:00
Bruno Windels 423c8a886d use isRemoteEcho to determine if the event is theirs or not
rather than the sender and from_device (which is not always set)

as this was one of the things breaking to_device verification
of ones own devices.
2020-01-03 18:16:25 +01:00
Bruno Windels 3ec8233a2d fixes & implement timeout 2020-01-03 13:42:06 +01:00
Bruno Windels 8ed51c806e don't cancel or timeout when verify isn't called 2020-01-03 13:42:06 +01:00
Bruno Windels 57135a898f don't mark events loaded from cache as live events
this makes the verifier want to interact with the other party
when just reloading the session.
2020-01-03 13:42:06 +01:00
Bruno Windels 0d3d27a519 fixes and cleanup for historical 2020-01-03 13:42:06 +01:00
Bruno Windels cf42ad83da WIP historical 2020-01-03 13:42:06 +01:00
Bruno Windels e7bcb61a3b attempt at only creating verifier for live events
but doesn't work yet? data where liveEvent is fished out is undefined
2020-01-03 13:42:06 +01:00
Bruno Windels 883b83f1da move blocking non-participating users back to InRoomChannel
as it doesn't need to happen for ToDeviceChannel
2020-01-03 13:42:06 +01:00
Bruno Windels 48977e6eaa get other party user id by inspecting initial event sender/to fields
also fail validation with any event not sent by or directed to us
2020-01-03 13:42:06 +01:00
Bruno Windels efe2488155 get other user id from channel
next up is inspecting the .request event to
determine it reliably in InRoomChannel
2020-01-03 13:42:06 +01:00
Bruno Windels 29c04b6f9c only move to PHASE_DONE when both .done events are received
as once in done, the request is removed from the request map
and the second .done event that comes in will not find the request
anymore, so the request wouldn't be attached to the event anymore,
breaking rendering it in the timeline.
2020-01-03 13:42:06 +01:00
Bruno Windels 984b6234d2 don't block remote echos to VerificationRequests
also put logic to block non-participating senders in VerificationRequest
so it is shared between both channels.

Remote echo's should not be passed to the verifier though.
2020-01-03 13:42:06 +01:00
Bruno Windels dac4a5452d make this a public prop 2020-01-03 13:42:06 +01:00
Bruno Windels 5f9e82204a more ready and remote echo support 2020-01-03 13:42:06 +01:00
Bruno Windels c4142d93c3 store in-room verification requests by roomId, txnId
as it's harder to determine the other side of a request, given
the in-room code also processes remote echos for own events.
2020-01-03 13:42:06 +01:00
Bruno Windels b34a2c7ee2 WIP 2020-01-03 13:42:06 +01:00
Bruno Windels cd7cc1b71f set verification request on event 2020-01-03 13:42:06 +01:00
Bruno Windels 4c6dd564a4 filter verification methods from argument 2020-01-03 13:42:06 +01:00
Bruno Windels 28e46a82ea expose common phases as properties
so we don't need to import the PHASE_ constants where we need to check
2020-01-03 13:42:06 +01:00
Bruno Windels 10e294784e waitForVerifier is unused now, make it more broadly useful with callback 2020-01-03 13:42:06 +01:00
Bruno Windels 2da725340c return request instead of verifier from verification methods
as MSC2366 adds an extra interactive step to the verification process,
we can't wait for the verifier after sending the request.

This is a breaking change in the js-sdk as it changes the return type
of an existing method.
2020-01-03 13:42:06 +01:00
Bruno Windels 882d3a765d support .ready event in VerificationRequest 2020-01-03 13:42:06 +01:00
61 changed files with 6236 additions and 3092 deletions
-58
View File
@@ -1,58 +0,0 @@
steps:
- label: ":eslint: JS Lint"
command:
- "yarn install"
- "yarn lint:js"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: ":tslint: TS Lint"
command:
- "yarn install"
- "yarn lint:ts"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: ":typescript: Types Lint"
command:
- "yarn install"
- "yarn lint:types"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: "🛠 Build"
command:
- "yarn install"
- "yarn build"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: ":jest: Tests"
command:
- "yarn install"
- "yarn test"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: "📃 Docs"
command:
- "yarn install"
- "yarn gendoc"
plugins:
- docker#v3.0.1:
image: "node:12"
- wait
- label: "🐴 Trigger matrix-react-sdk"
trigger: "matrix-react-sdk"
branches: "develop"
build:
branch: "develop"
message: "[js-sdk] ${BUILDKITE_MESSAGE}"
async: true
+262
View File
@@ -1,3 +1,265 @@
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)
* refactor megolm encryption to improve perceived speed
[\#1252](https://github.com/matrix-org/matrix-js-sdk/pull/1252)
* Remove v1 identity server fallbacks
[\#1253](https://github.com/matrix-org/matrix-js-sdk/pull/1253)
* Use alt_aliases instead of local ones for room names
[\#1251](https://github.com/matrix-org/matrix-js-sdk/pull/1251)
* Upload cross-signing key signatures in the background
[\#1250](https://github.com/matrix-org/matrix-js-sdk/pull/1250)
* Fix secret sharing names to match spec
[\#1249](https://github.com/matrix-org/matrix-js-sdk/pull/1249)
* Cleanup: remove crypto.verification.start event
[\#1248](https://github.com/matrix-org/matrix-js-sdk/pull/1248)
* Fix regression in key backup request params
[\#1246](https://github.com/matrix-org/matrix-js-sdk/pull/1246)
* Use cross-signing trust to mark backups verified
[\#1244](https://github.com/matrix-org/matrix-js-sdk/pull/1244)
* Check both cross-signing and local trust for key sharing
[\#1243](https://github.com/matrix-org/matrix-js-sdk/pull/1243)
* Fixed up tests to match new way that crypto stores are created
[\#1242](https://github.com/matrix-org/matrix-js-sdk/pull/1242)
* Store USK and SSK locally
[\#1235](https://github.com/matrix-org/matrix-js-sdk/pull/1235)
* Use unpadded base64 for QR code secrets
[\#1236](https://github.com/matrix-org/matrix-js-sdk/pull/1236)
* Don't require .done event for finishing self-verification
[\#1239](https://github.com/matrix-org/matrix-js-sdk/pull/1239)
* Don't cancel as 3rd party in verification request
[\#1237](https://github.com/matrix-org/matrix-js-sdk/pull/1237)
* Verification: log when switching start event
[\#1234](https://github.com/matrix-org/matrix-js-sdk/pull/1234)
* Perform crypto store operations directly after transaction
[\#1233](https://github.com/matrix-org/matrix-js-sdk/pull/1233)
* More verification request logging
[\#1232](https://github.com/matrix-org/matrix-js-sdk/pull/1232)
* Upgrade deps
[\#1231](https://github.com/matrix-org/matrix-js-sdk/pull/1231)
Changes in [5.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.0) (2020-03-02)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.0-rc.1...v5.1.0)
* No changes since rc.1
Changes in [5.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.0-rc.1) (2020-02-26)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.0.1...v5.1.0-rc.1)
* Add latest dist-tag for releases
[\#1230](https://github.com/matrix-org/matrix-js-sdk/pull/1230)
* Add room method for alt_aliases
[\#1225](https://github.com/matrix-org/matrix-js-sdk/pull/1225)
* Remove buildkite pipeline
[\#1227](https://github.com/matrix-org/matrix-js-sdk/pull/1227)
* don't assume verify has been called when receiving a cancellation in
verifier
[\#1226](https://github.com/matrix-org/matrix-js-sdk/pull/1226)
* Reduce secret size for new binary packing
[\#1221](https://github.com/matrix-org/matrix-js-sdk/pull/1221)
* misc rageshake fixes
[\#1223](https://github.com/matrix-org/matrix-js-sdk/pull/1223)
* Fix cancelled historical requests not appearing as cancelled
[\#1220](https://github.com/matrix-org/matrix-js-sdk/pull/1220)
* Fix renaming error that broke QR code verification
[\#1217](https://github.com/matrix-org/matrix-js-sdk/pull/1217)
Changes in [5.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.0.1) (2020-02-19)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.0.0...v5.0.1)
* add method for new /aliases endpoint
[\#1219](https://github.com/matrix-org/matrix-js-sdk/pull/1219)
* method for checking if other party supports verification method
[\#1213](https://github.com/matrix-org/matrix-js-sdk/pull/1213)
* add local echo state for accepting or declining a verif req
[\#1210](https://github.com/matrix-org/matrix-js-sdk/pull/1210)
* make logging compatible with rageshakes
[\#1214](https://github.com/matrix-org/matrix-js-sdk/pull/1214)
* Find existing requests when starting a new verification request
[\#1209](https://github.com/matrix-org/matrix-js-sdk/pull/1209)
* log MAC calculation during SAS
[\#1211](https://github.com/matrix-org/matrix-js-sdk/pull/1211)
Changes in [5.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.0.0) (2020-02-17)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.0.0-rc.1...v5.0.0)
* No changes since rc.1
Changes in [5.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.0.0-rc.1) (2020-02-13)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v4.0.0...v5.0.0-rc.1)
BREAKING CHANGES
---
* The verification methods API has removed an argument ([\#1206](https://github.com/matrix-org/matrix-js-sdk/pull/1206))
All Changes
---
* Remove methods argument to verification
[\#1206](https://github.com/matrix-org/matrix-js-sdk/pull/1206)
* don't do a dynamic import of request
[\#1207](https://github.com/matrix-org/matrix-js-sdk/pull/1207)
* QR self-verification fixes
[\#1201](https://github.com/matrix-org/matrix-js-sdk/pull/1201)
* Log every verification event
[\#1204](https://github.com/matrix-org/matrix-js-sdk/pull/1204)
* dont require .done event from other party
[\#1203](https://github.com/matrix-org/matrix-js-sdk/pull/1203)
* New option to fully reset Secret Storage keys in boostrapSecretStorage
[\#1202](https://github.com/matrix-org/matrix-js-sdk/pull/1202)
* Add function to estimate target device for a VerificationRequest
[\#1190](https://github.com/matrix-org/matrix-js-sdk/pull/1190)
* pass ssss item name to callback so we can differentiate UI on it
[\#1200](https://github.com/matrix-org/matrix-js-sdk/pull/1200)
* add export/import of Olm devices
[\#1167](https://github.com/matrix-org/matrix-js-sdk/pull/1167)
* Convert utils.js -> utils.ts
[\#1199](https://github.com/matrix-org/matrix-js-sdk/pull/1199)
* Don't sign ourselves as a user
[\#1197](https://github.com/matrix-org/matrix-js-sdk/pull/1197)
* Add a bunch of logging to verification
[\#1196](https://github.com/matrix-org/matrix-js-sdk/pull/1196)
* Fix: always return a valid string from InRoomChannel.getEventType
[\#1198](https://github.com/matrix-org/matrix-js-sdk/pull/1198)
* add logging when a request is being cancelled
[\#1195](https://github.com/matrix-org/matrix-js-sdk/pull/1195)
* Don't explode verification validation if we don't have an event type
[\#1194](https://github.com/matrix-org/matrix-js-sdk/pull/1194)
* Fix: verification request appearing for users that are not the receiver or
sender if they are in room
[\#1193](https://github.com/matrix-org/matrix-js-sdk/pull/1193)
* Fix getting secrets encoded with passthrough keys
[\#1192](https://github.com/matrix-org/matrix-js-sdk/pull/1192)
* Update QR code handling for new spec
[\#1175](https://github.com/matrix-org/matrix-js-sdk/pull/1175)
* Don't add ephemeral events to timeline when peeking
[\#1188](https://github.com/matrix-org/matrix-js-sdk/pull/1188)
* Fix typo
[\#1189](https://github.com/matrix-org/matrix-js-sdk/pull/1189)
* Verification: resolve race between .start events from both parties
[\#1187](https://github.com/matrix-org/matrix-js-sdk/pull/1187)
* Add option to bootstrap to start new key backup
[\#1184](https://github.com/matrix-org/matrix-js-sdk/pull/1184)
* Add a bunch of null guards to feature checks
[\#1182](https://github.com/matrix-org/matrix-js-sdk/pull/1182)
* docs: fix MatrixClient reference
[\#1183](https://github.com/matrix-org/matrix-js-sdk/pull/1183)
* Add helper to obtain the cancellation code for a verification request
[\#1180](https://github.com/matrix-org/matrix-js-sdk/pull/1180)
* Publish pre-releases as a separate tag on npm
[\#1178](https://github.com/matrix-org/matrix-js-sdk/pull/1178)
* Fix support for passthrough keys
[\#1177](https://github.com/matrix-org/matrix-js-sdk/pull/1177)
* Trust our own cross-signing keys if we verify them with another device
[\#1174](https://github.com/matrix-org/matrix-js-sdk/pull/1174)
* Ensure cross-signing keys are downloaded when checking trust
[\#1176](https://github.com/matrix-org/matrix-js-sdk/pull/1176)
* Don't log verification validation errors for normal messages
[\#1172](https://github.com/matrix-org/matrix-js-sdk/pull/1172)
* Fix bootstrap cleanup
[\#1173](https://github.com/matrix-org/matrix-js-sdk/pull/1173)
* QR code verification
[\#1155](https://github.com/matrix-org/matrix-js-sdk/pull/1155)
* expose deviceId prop on device channel
[\#1171](https://github.com/matrix-org/matrix-js-sdk/pull/1171)
* Move & upgrade babel runtime into dependencies (like it wants)
[\#1169](https://github.com/matrix-org/matrix-js-sdk/pull/1169)
* Add unit tests for verifying your own device, remove .event property on
verification request
[\#1166](https://github.com/matrix-org/matrix-js-sdk/pull/1166)
* For dm-verification, also consider events sent by other devices of same user
as "our" events
[\#1163](https://github.com/matrix-org/matrix-js-sdk/pull/1163)
* Add a prepare script
[\#1161](https://github.com/matrix-org/matrix-js-sdk/pull/1161)
* Remove :deviceId from /keys/upload/:deviceId as not spec-compliant
[\#1162](https://github.com/matrix-org/matrix-js-sdk/pull/1162)
* Refactor and expose some logic publicly for the TimelineWindow class.
[\#1159](https://github.com/matrix-org/matrix-js-sdk/pull/1159)
* Allow a device key upload request without auth
[\#1158](https://github.com/matrix-org/matrix-js-sdk/pull/1158)
* Support for .ready verification event (MSC2366) & other things
[\#1140](https://github.com/matrix-org/matrix-js-sdk/pull/1140)
Changes in [4.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v4.0.0) (2020-01-27)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v4.0.0-rc.1...v4.0.0)
+2
View File
@@ -0,0 +1,2 @@
olm.js
olm.wasm
+1
View File
@@ -0,0 +1 @@
../../../dist/browser-matrix.js
@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Test Crypto in Browser</title>
<script src="lib/olm.js"></script>
<script src="lib/matrix.js"></script>
</head>
<body>
<h1>Testing export/import of Olm devices in the browser</h1>
<ul>
<li>
Make sure you built the current version of the Matrix JS SDK
(<code>yarn build</code>)
</li>
<li>
copy <code>olm.js</code> and <code>olm.wasm</code>
from a recent release of Olm (was tested with version 3.1.4)
in directory <code>lib/</code>
</li>
<li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li>
<li>Serve this HTML file (e.g. <code>python3 -m http.server</code>) and go to it through your browser</li>
<li>
in the JS console, do:
<pre>
aliceMatrixClient = await newMatrixClient("alice-"+randomHex());
await aliceMatrixClient.exportDevice();
await aliceMatrixClient.getAccessToken();
</pre>
</li>
<li>
copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere
(<strong>not</strong> in a JS variable as it will be destroyed when you refresh the page)
</li>
<li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li>
<li>
Do the following, replacing <code>ALICE_ID</code>
with the user ID of Alice (you can find it in the exported data)
<pre>
bobMatrixClient = await newMatrixClient("bob-"+randomHex());
roomId = await bobMatrixClient.createEncryptedRoom([ALICE_ID]);
await bobMatrixClient.sendTextMessage('Hi Alice!', roomId);
</pre>
</li>
<li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li>
<li>
Now do the following, using the exported data and the access token you saved previously:
<pre>
aliceMatrixClient = await importMatrixClient(EXPORTED_DATA, ACCESS_TOKEN);
</pre>
</li>
<li>You should see the message sent by Bob printed in the console.</li>
</ul>
<script src="olm-device-export-import.js"></script>
</body>
</html>
@@ -0,0 +1,122 @@
if (!Olm) {
console.error(
"global.Olm does not seem to be present."
+ " Did you forget to add olm in the lib/ directory?"
);
}
const BASE_URL = 'http://localhost:8008';
const ROOM_CRYPTO_CONFIG = { algorithm: 'm.megolm.v1.aes-sha2' };
const PASSWORD = 'password';
// useful to create new usernames
window.randomHex = () => Math.floor(Math.random() * (10**6)).toString(16);
window.newMatrixClient = async function (username) {
const registrationClient = matrixcs.createClient(BASE_URL);
const userRegisterResult = await registrationClient.register(
username,
PASSWORD,
null,
{ type: 'm.login.dummy' }
);
const matrixClient = matrixcs.createClient({
baseUrl: BASE_URL,
userId: userRegisterResult.user_id,
accessToken: userRegisterResult.access_token,
deviceId: userRegisterResult.device_id,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
});
extendMatrixClient(matrixClient);
await matrixClient.initCrypto();
await matrixClient.startClient();
return matrixClient;
}
window.importMatrixClient = async function (exportedDevice, accessToken) {
const matrixClient = matrixcs.createClient({
baseUrl: BASE_URL,
deviceToImport: exportedDevice,
accessToken,
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
cryptoStore: new matrixcs.MemoryCryptoStore(),
});
extendMatrixClient(matrixClient);
await matrixClient.initCrypto();
await matrixClient.startClient();
return matrixClient;
}
function extendMatrixClient(matrixClient) {
// automatic join
matrixClient.on('RoomMember.membership', async (event, member) => {
if (member.membership === 'invite' && member.userId === matrixClient.getUserId()) {
await matrixClient.joinRoom(member.roomId);
// setting up of room encryption seems to be triggered automatically
// but if we don't wait for it the first messages we send are unencrypted
await matrixClient.setRoomEncryption(member.roomId, { algorithm: 'm.megolm.v1.aes-sha2' })
}
});
matrixClient.onDecryptedMessage = message => {
console.log('Got encrypted message: ', message);
}
matrixClient.on('Event.decrypted', (event) => {
if (event.getType() === 'm.room.message'){
matrixClient.onDecryptedMessage(event.getContent().body);
} else {
console.log('decrypted an event of type', event.getType());
console.log(event);
}
});
matrixClient.createEncryptedRoom = async function(usersToInvite) {
const {
room_id: roomId,
} = await this.createRoom({
visibility: 'private',
invite: usersToInvite,
});
// matrixClient.setRoomEncryption() only updates local state
// but does not send anything to the server
// (see https://github.com/matrix-org/matrix-js-sdk/issues/905)
// so we do it ourselves with 'sendStateEvent'
await this.sendStateEvent(
roomId, 'm.room.encryption', ROOM_CRYPTO_CONFIG,
);
await this.setRoomEncryption(
roomId, ROOM_CRYPTO_CONFIG,
);
// Marking all devices as verified
let room = this.getRoom(roomId);
let members = (await room.getEncryptionTargetMembers()).map(x => x["userId"])
let memberkeys = await this.downloadKeys(members);
for (const userId in memberkeys) {
for (const deviceId in memberkeys[userId]) {
await this.setDeviceVerified(userId, deviceId);
}
}
return roomId;
}
matrixClient.sendTextMessage = async function(message, roomId) {
return matrixClient.sendMessage(
roomId,
{
body: message,
msgtype: 'm.text',
}
)
}
}
+3 -1
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "4.0.0",
"version": "5.2.0",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepare": "yarn build",
@@ -78,7 +78,9 @@
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-jest": "^23.0.4",
"exorcist": "^1.0.1",
"fake-indexeddb": "^3.0.0",
"jest": "^24.9.0",
"jest-localstorage-mock": "^2.4.0",
"jsdoc": "^3.5.5",
"matrix-mock-request": "^1.2.3",
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
+8 -1
View File
@@ -296,7 +296,14 @@ rm "${latest_changes}"
# Login and publish continues to use `npm`, as it seems to have more clearly
# defined options and semantics than `yarn` for writing to the registry.
npm publish
# Tag both releases and prereleases as `next` so the last stable release remains
# the default.
npm publish --tag next
if [ $prerelease -eq 0 ]; then
# For a release, also add the default `latest` tag.
package=$(cat package.json | jq -er .name)
npm dist-tag add "$package@$release" latest
fi
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
+248
View File
@@ -0,0 +1,248 @@
/*
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 '../../olm-loader';
import {
CrossSigningInfo,
createCryptoStoreCacheCallbacks,
} from '../../../src/crypto/CrossSigning';
import {
IndexedDBCryptoStore,
} from '../../../src/crypto/store/indexeddb-crypto-store';
import {MemoryCryptoStore} from '../../../src/crypto/store/memory-crypto-store';
import 'fake-indexeddb/auto';
import 'jest-localstorage-mock';
const userId = "@alice:example.com";
// Private key for tests only
const testKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
]);
const types = [
{ type: "master", shouldCache: false },
{ type: "self_signing", shouldCache: true },
{ type: "user_signing", shouldCache: true },
{ type: "invalid", shouldCache: false },
];
const badKey = Uint8Array.from(testKey);
badKey[0] ^= 1;
const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("CrossSigningInfo.getCrossSigningKey", function() {
if (!global.Olm) {
console.warn('Not running megolm backup unit tests: libolm not present');
return;
}
beforeAll(function() {
return global.Olm.init();
});
it("should throw if no callback is provided", async () => {
const info = new CrossSigningInfo(userId);
await expect(info.getCrossSigningKey("master")).rejects.toThrow();
});
it.each(types)("should throw if the callback returns falsey",
async ({type, shouldCache}) => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: () => false,
});
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
});
it("should throw if the expected key doesn't come back", async () => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: () => masterKeyPub,
});
await expect(info.getCrossSigningKey("master", "")).rejects.toThrow();
});
it("should return a key from its callback", async () => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: () => testKey,
});
const [pubKey, ab] = await info.getCrossSigningKey("master", masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(ab).toEqual({a: 106712, b: 106712});
});
it.each(types)("should request a key from the cache callback (if set)" +
" and does not call app if one is found" +
" %o",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
return Promise.reject(new Error("Regular callback called"));
} else {
return Promise.resolve(testKey);
}
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
if (shouldCache) {
expect(getCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
}
});
it.each(types)("should store a key with the cache callback (if set)",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
if (shouldCache) {
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
}
});
it.each(types)("does not store a bad key to the cache",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ storeCrossSigningKeyCache },
);
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
});
it.each(types)("does not store a value to the cache if it came from the cache",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
return Promise.reject(new Error("Regular callback called"));
} else {
return Promise.resolve(testKey);
}
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(
new Error("Tried to store a value from cache"),
);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
});
it.each(types)("requests a key from the cache callback (if set) and then calls app" +
" if one is not found", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
});
it.each(types)("requests a key from the cache callback (if set) and then" +
" calls app if that key doesn't match", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
});
});
/*
* Note that MemoryStore is weird. It's only used for testing - as far as I can tell,
* it's not possible to get one in normal execution unless you hack as we do here.
*/
describe.each([
["IndexedDBCryptoStore",
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore",
() => new IndexedDBCryptoStore(undefined, "tests")],
["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) {
let store;
beforeAll(() => {
store = dbFactory();
});
beforeEach(async () => {
await store.deleteAllData();
});
it("should cache data to the store and retrieve it", async () => {
await store.startup();
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } =
createCryptoStoreCacheCallbacks(store);
await storeCrossSigningKeyCache("self_signing", testKey);
// If we've not saved anything, don't expect anything
// Definitely don't accidentally return the wrong key for the type
const nokey = await getCrossSigningKeyCache("self", "");
expect(nokey).toBeNull();
const key = await getCrossSigningKeyCache("self_signing", "");
expect(key).toEqual(testKey);
});
});
+24 -18
View File
@@ -297,6 +297,10 @@ describe("MegolmDecryption", function() {
},
}));
mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false,
});
const megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
@@ -320,14 +324,14 @@ describe("MegolmDecryption", function() {
// this should have claimed a key for alice as it's starting a new session
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
mockBaseApis.claimOneTimeKeys.mockReset();
@@ -516,21 +520,22 @@ describe("MegolmDecryption", function() {
};
};
let run = false;
aliceClient.sendToDevice = async (msgtype, contentMap) => {
run = true;
expect(msgtype).toBe("org.matrix.room_key.withheld");
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice: {
algorithm: "m.megolm.v1.aes-sha2",
code: 'm.no_olm',
reason: 'Unable to establish a secure channel.',
sender_key: aliceDevice.deviceCurve25519Key,
const sendPromise = new Promise((resolve, reject) => {
aliceClient.sendToDevice = async (msgtype, contentMap) => {
expect(msgtype).toBe("org.matrix.room_key.withheld");
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice: {
algorithm: "m.megolm.v1.aes-sha2",
code: 'm.no_olm',
reason: 'Unable to establish a secure channel.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
},
});
};
});
resolve();
};
});
const event = new MatrixEvent({
type: "m.room.message",
@@ -540,8 +545,7 @@ describe("MegolmDecryption", function() {
content: {},
});
await aliceClient._crypto.encryptEvent(event, aliceRoom);
expect(run).toBe(true);
await sendPromise;
});
it("throws an error describing why it doesn't have a key", async function() {
@@ -598,6 +602,7 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
aliceClient._crypto.downloadKeys = async () => {};
const bobDevice = bobClient._crypto._olmDevice;
const roomId = "!someroom";
@@ -649,6 +654,7 @@ describe("MegolmDecryption", function() {
bobClient.initCrypto(),
]);
const bobDevice = bobClient._crypto._olmDevice;
aliceClient._crypto.downloadKeys = async () => {};
const roomId = "!someroom";
+55 -1
View File
@@ -41,7 +41,7 @@ async function setupSession(initiator, opponent) {
return sid;
}
describe("OlmDecryption", function() {
describe("OlmDevice", function() {
if (!global.Olm) {
logger.warn('Not running megolm unit tests: libolm not present');
return;
@@ -81,6 +81,60 @@ describe("OlmDecryption", function() {
);
});
it('exports picked account and olm sessions', async function() {
const sessionId = await setupSession(aliceOlmDevice, bobOlmDevice);
const exported = await bobOlmDevice.export();
// At this moment only Alice (the “initiator” in setupSession) has a session
expect(exported.sessions).toEqual([]);
const MESSAGE = (
"The olm or proteus is an aquatic salamander"
+ " in the family Proteidae"
);
const ciphertext = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key,
sessionId,
MESSAGE,
);
const bobRecreatedOlmDevice = makeOlmDevice();
bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
const decrypted = await bobRecreatedOlmDevice.createInboundSession(
aliceOlmDevice.deviceCurve25519Key,
ciphertext.type,
ciphertext.body,
);
expect(decrypted.payload).toEqual(MESSAGE);
const exportedAgain = await bobRecreatedOlmDevice.export();
// this time we expect Bob to have a session to export
expect(exportedAgain.sessions).toHaveLength(1);
const MESSAGE_2 = (
"In contrast to most amphibians,"
+ " the olm is entirely aquatic"
);
const ciphertext2 = await aliceOlmDevice.encryptMessage(
bobOlmDevice.deviceCurve25519Key,
sessionId,
MESSAGE_2,
);
const bobRecreatedAgainOlmDevice = makeOlmDevice();
bobRecreatedAgainOlmDevice.init({ fromExportedDevice: exportedAgain });
// Note: "decrypted_2" does not have the same structure as "decrypted"
const decrypted2 = await bobRecreatedAgainOlmDevice.decryptMessage(
aliceOlmDevice.deviceCurve25519Key,
decrypted.session_id,
ciphertext2.type,
ciphertext2.body,
);
expect(decrypted2).toEqual(MESSAGE_2);
});
it("creates only one session at a time", async function() {
// if we call ensureOlmSessionsForDevices multiple times, it should
// only try to create one session at a time, even if the server is
+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();
});
});
});
+4 -4
View File
@@ -174,7 +174,7 @@ describe("Cross Signing", function() {
HttpResponse.PUSH_RULES_RESPONSE,
{
method: "POST",
path: "/keys/upload/Osborne2",
path: "/keys/upload",
data: {
one_time_key_counts: {
curve25519: 100,
@@ -237,7 +237,7 @@ describe("Cross Signing", function() {
},
{
method: "POST",
path: "/keys/upload/Osborne2",
path: "/keys/upload",
data: {
one_time_key_counts: {
curve25519: 100,
@@ -429,7 +429,7 @@ describe("Cross Signing", function() {
HttpResponse.PUSH_RULES_RESPONSE,
{
method: "POST",
path: "/keys/upload/Osborne2",
path: "/keys/upload",
data: {
one_time_key_counts: {
curve25519: 100,
@@ -487,7 +487,7 @@ describe("Cross Signing", function() {
},
{
method: "POST",
path: "/keys/upload/Osborne2",
path: "/keys/upload",
data: {
one_time_key_counts: {
curve25519: 100,
+39 -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,
@@ -34,6 +43,9 @@ async function makeTestClient(userInfo, options) {
await client.initCrypto();
// No need to download keys for these tests
client._crypto.downloadKeys = async function() {};
return client;
}
@@ -48,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();
@@ -66,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(
@@ -97,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');
@@ -109,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();
@@ -146,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"},
@@ -153,6 +170,7 @@ describe("Secrets", function() {
cryptoCallbacks: {
getCrossSigningKey: t => keys[t],
saveCrossSigningKeys: k => keys = k,
getSecretStorageKey: getKey,
},
},
);
@@ -167,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
@@ -249,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 () => {};
@@ -0,0 +1,98 @@
/*
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 {InRoomChannel} from "../../../../src/crypto/verification/request/InRoomChannel";
"../../../../src/crypto/verification/request/ToDeviceChannel";
import {MatrixEvent} from "../../../../src/models/event";
describe("InRoomChannel tests", function() {
const ALICE = "@alice:hs.tld";
const BOB = "@bob:hs.tld";
const MALORY = "@malory:hs.tld";
const client = {
getUserId() { return ALICE; },
};
it("getEventType only returns .request for a message with a msgtype", function() {
const invalidEvent = new MatrixEvent({
type: "m.key.verification.request",
});
expect(InRoomChannel.getEventType(invalidEvent)).toStrictEqual("");
const validEvent = new MatrixEvent({
type: "m.room.message",
content: { msgtype: "m.key.verification.request" },
});
expect(InRoomChannel.getEventType(validEvent)).
toStrictEqual("m.key.verification.request");
const validFooEvent = new MatrixEvent({ type: "m.foo" });
expect(InRoomChannel.getEventType(validFooEvent)).
toStrictEqual("m.foo");
});
it("getEventType should return m.room.message for messages", function() {
const messageEvent = new MatrixEvent({
type: "m.room.message",
content: { msgtype: "m.text" },
});
// XXX: The event type doesn't matter too much, just as long as it's not a verification event
expect(InRoomChannel.getEventType(messageEvent)).
toStrictEqual("m.room.message");
});
it("getEventType should return actual type for non-message events", function() {
const event = new MatrixEvent({
type: "m.room.member",
content: { },
});
expect(InRoomChannel.getEventType(event)).
toStrictEqual("m.room.member");
});
it("getOtherPartyUserId should not return anything for a request not " +
"directed at me", function() {
const event = new MatrixEvent({
sender: BOB,
type: "m.room.message",
content: { msgtype: "m.key.verification.request", to: MALORY },
});
expect(InRoomChannel.getOtherPartyUserId(event, client)).toStrictEqual(undefined);
});
it("getOtherPartyUserId should not return anything an event that is not of a valid " +
"request type", function() {
// invalid because this should be a room message with msgtype
const invalidRequest = new MatrixEvent({
sender: BOB,
type: "m.key.verification.request",
content: { to: ALICE },
});
expect(InRoomChannel.getOtherPartyUserId(invalidRequest, client))
.toStrictEqual(undefined);
const startEvent = new MatrixEvent({
sender: BOB,
type: "m.key.verification.start",
content: { to: ALICE },
});
expect(InRoomChannel.getOtherPartyUserId(startEvent, client))
.toStrictEqual(undefined);
const fooEvent = new MatrixEvent({
sender: BOB,
type: "m.foo",
content: { to: ALICE },
});
expect(InRoomChannel.getOtherPartyUserId(fooEvent, client))
.toStrictEqual(undefined);
});
});
+7 -120
View File
@@ -16,8 +16,6 @@ limitations under the License.
*/
import "../../../olm-loader";
import {logger} from "../../../../src/logger";
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
import {ScanQRCode, ShowQRCode} from "../../../../src/crypto/verification/QRCode";
const Olm = global.Olm;
@@ -31,124 +29,13 @@ describe("QR code verification", function() {
return Olm.init();
});
describe("showing", function() {
it("should emit an event to show a QR code", async function() {
const channel = {
send: jest.fn(),
};
const qrCode = new ShowQRCode(channel, {
getUserId: () => "@alice:example.com",
deviceId: "ABCDEFG",
getDeviceEd25519Key: function() {
return "device+ed25519+key";
},
});
const spy = jest.fn((e) => {
qrCode.done();
});
qrCode.on("show_qr_code", spy);
await qrCode.verify();
expect(spy).toHaveBeenCalledWith({
url: "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey",
});
});
});
describe("scanning", function() {
const QR_CODE_URL = "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey";
it("should verify when a QR code is sent", async function() {
const device = DeviceInfo.fromStorage(
{
algorithms: [],
keys: {
"curve25519:ABCDEFG": "device+curve25519+key",
"ed25519:ABCDEFG": "device+ed25519+key",
},
verified: false,
known: false,
unsigned: {},
},
"ABCDEFG",
);
const client = {
getStoredDevice: jest.fn().mockReturnValue(device),
setDeviceVerified: jest.fn(),
};
const channel = {
send: jest.fn(),
};
const qrCode = new ScanQRCode(channel, client);
qrCode.on("confirm_user_id", ({userId, confirm}) => {
if (userId === "@alice:example.com") {
confirm();
} else {
qrCode.cancel(new Error("Incorrect user"));
}
});
qrCode.on("scan", ({done}) => {
done(QR_CODE_URL);
});
await qrCode.verify();
expect(client.getStoredDevice)
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
expect(client.setDeviceVerified)
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
});
it("should error when the user ID doesn't match", async function() {
const client = {
getStoredDevice: jest.fn(),
setDeviceVerified: jest.fn(),
};
const channel = {
send: jest.fn(),
};
const qrCode = new ScanQRCode(channel, client, "@bob:example.com", "ABCDEFG");
qrCode.on("scan", ({done}) => {
done(QR_CODE_URL);
});
const spy = jest.fn();
await qrCode.verify().catch(spy);
expect(spy).toHaveBeenCalled();
expect(channel.send).toHaveBeenCalled();
expect(client.getStoredDevice).not.toHaveBeenCalled();
expect(client.setDeviceVerified).not.toHaveBeenCalled();
});
it("should error if the key doesn't match", async function() {
const device = DeviceInfo.fromStorage(
{
algorithms: [],
keys: {
"curve25519:ABCDEFG": "device+curve25519+key",
"ed25519:ABCDEFG": "a+different+device+ed25519+key",
},
verified: false,
known: false,
unsigned: {},
},
"ABCDEFG",
);
const client = {
getStoredDevice: jest.fn().mockReturnValue(device),
setDeviceVerified: jest.fn(),
};
const channel = {
send: jest.fn(),
};
const qrCode = new ScanQRCode(
channel, client, "@alice:example.com", "ABCDEFG");
qrCode.on("scan", ({done}) => {
done(QR_CODE_URL);
});
const spy = jest.fn();
await qrCode.verify().catch(spy);
expect(spy).toHaveBeenCalled();
expect(channel.send).toHaveBeenCalled();
expect(client.getStoredDevice).toHaveBeenCalled();
expect(client.setDeviceVerified).not.toHaveBeenCalled();
describe("reciprocate", () => {
it("should verify the secret", () => {
// TODO: Actually write a test for this.
// Tests are hard because we are running before the verification
// process actually begins, and are largely UI-driven rather than
// logic-driven (compared to something like SAS). In the interest
// of time, tests are currently excluded.
});
});
});
+12 -3
View File
@@ -18,20 +18,27 @@ import "../../../olm-loader";
import {verificationMethods} from "../../../../src/crypto";
import {logger} from "../../../../src/logger";
import {SAS} from "../../../../src/crypto/verification/SAS";
import {makeTestClients} from './util';
import {makeTestClients, setupWebcrypto, teardownWebcrypto} from './util';
const Olm = global.Olm;
describe("verification request", function() {
jest.useFakeTimers();
describe("verification request integration tests with crypto layer", function() {
if (!global.Olm) {
logger.warn('Not running device verification unit tests: libolm not present');
return;
}
beforeAll(function() {
setupWebcrypto();
return Olm.init();
});
afterAll(() => {
teardownWebcrypto();
});
it("should request and accept a verification", async function() {
const [alice, bob] = await makeTestClients(
[
@@ -64,7 +71,9 @@ describe("verification request", function() {
// XXX: Private function access (but it's a test, so we're okay)
bobVerifier._endTimer();
});
const aliceVerifier = await alice.client.requestVerification("@bob:example.com");
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
await aliceRequest.waitFor(r => r.started);
const aliceVerifier = aliceRequest.verifier;
expect(aliceVerifier).toBeInstanceOf(SAS);
// XXX: Private function access (but it's a test, so we're okay)
+16 -9
View File
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import "../../../olm-loader";
import {makeTestClients} from './util';
import {makeTestClients, setupWebcrypto, teardownWebcrypto} from './util';
import {MatrixEvent} from "../../../../src/models/event";
import {SAS} from "../../../../src/crypto/verification/SAS";
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
@@ -35,9 +35,14 @@ describe("SAS verification", function() {
}
beforeAll(function() {
setupWebcrypto();
return Olm.init();
});
afterAll(() => {
teardownWebcrypto();
});
it("should error on an unexpected event", async function() {
const sas = new SAS({}, "@alice:example.com", "ABCDEFG");
sas.handleEvent(new MatrixEvent({
@@ -117,8 +122,8 @@ describe("SAS verification", function() {
bobSasEvent = null;
bobPromise = new Promise((resolve, reject) => {
bob.client.on("crypto.verification.start", (verifier) => {
verifier.on("show_sas", (e) => {
bob.client.on("crypto.verification.request", request => {
request.verifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
@@ -134,7 +139,7 @@ describe("SAS verification", function() {
}
}
});
resolve(verifier);
resolve(request.verifier);
});
});
@@ -334,11 +339,11 @@ describe("SAS verification", function() {
};
const bobPromise = new Promise((resolve, reject) => {
bob.client.on("crypto.verification.start", (verifier) => {
verifier.on("show_sas", (e) => {
bob.client.on("crypto.verification.request", request => {
request.verifier.on("show_sas", (e) => {
e.mismatch();
});
resolve(verifier);
resolve(request.verifier);
});
});
@@ -442,9 +447,11 @@ describe("SAS verification", function() {
});
});
aliceVerifier = await alice.client.requestVerificationDM(
bob.client.getUserId(), "!room_id", [verificationMethods.SAS],
const aliceRequest = await alice.client.requestVerificationDM(
bob.client.getUserId(), "!room_id",
);
await aliceRequest.waitFor(r => r.started);
aliceVerifier = aliceRequest.verifier;
aliceVerifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
@@ -0,0 +1,104 @@
/*
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 {VerificationBase} from '../../../../src/crypto/verification/Base';
import {CrossSigningInfo} from '../../../../src/crypto/CrossSigning';
import {encodeBase64} from "../../../../src/crypto/olmlib";
import {setupWebcrypto, teardownWebcrypto} from './util';
jest.useFakeTimers();
// Private key for tests only
const testKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
]);
const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("self-verifications", () => {
beforeAll(function() {
setupWebcrypto();
return global.Olm.init();
});
afterAll(() => {
teardownWebcrypto();
});
it("triggers a request for key sharing upon completion", async () => {
const userId = "@test:localhost";
const cacheCallbacks = {
getCrossSigningKeyCache: jest.fn().mockReturnValue(null),
storeCrossSigningKeyCache: jest.fn(),
};
const _crossSigningInfo = new CrossSigningInfo(
userId,
{},
cacheCallbacks,
);
_crossSigningInfo.keys = {
self_signing: { keys: { X: testKeyPub } },
user_signing: { keys: { X: testKeyPub } },
};
const _secretStorage = {
request: jest.fn().mockReturnValue({
promise: Promise.resolve(encodeBase64(testKey)),
}),
};
const client = {
_crypto: {
_crossSigningInfo,
_secretStorage,
},
getUserId: () => userId,
};
const request = {
onVerifierFinished: () => undefined,
};
const verification = new VerificationBase(
undefined, // channel
client, // baseApis
userId,
"ABC", // deviceId
undefined, // startEvent
request,
);
verification._resolve = () => undefined;
const result = await verification.done();
/* We should request, and store, two keys */
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(2);
expect(_secretStorage.request.mock.calls.length).toBe(2);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1])
.toEqual(testKey);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[1][1])
.toEqual(testKey);
expect(result).toBeInstanceOf(Array);
expect(result[0][0]).toBe(testKeyPub);
expect(result[1][0]).toBe(testKeyPub);
});
});
+41 -18
View File
@@ -17,6 +17,7 @@ limitations under the License.
import {TestClient} from '../../../TestClient';
import {MatrixEvent} from "../../../../src/models/event";
import nodeCrypto from "crypto";
export async function makeTestClients(userInfos, options) {
const clients = [];
@@ -33,15 +34,13 @@ export async function makeTestClients(userInfos, options) {
content: msg,
});
const client = clientMap[userId][deviceId];
if (event.isEncrypted()) {
event.attemptDecryption(client._crypto)
.then(() => client.emit("toDeviceEvent", event));
} else {
setTimeout(
() => client.emit("toDeviceEvent", event),
0,
);
}
const decryptionPromise = event.isEncrypted() ?
event.attemptDecryption(client._crypto) :
Promise.resolve();
decryptionPromise.then(
() => client.emit("toDeviceEvent", event),
);
}
}
}
@@ -50,21 +49,33 @@ export async function makeTestClients(userInfos, options) {
const sendEvent = function(room, type, content) {
// make up a unique ID as the event ID
const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this
const event = new MatrixEvent({
const rawEvent = {
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
type: type,
content: content,
room_id: room,
event_id: eventId,
});
for (const tc of clients) {
setTimeout(
() => tc.client.emit("Room.timeline", event),
0,
);
}
origin_server_ts: Date.now(),
};
const event = new MatrixEvent(rawEvent);
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
unsigned: {
transaction_id: this.makeTxnId(), // eslint-disable-line babel/no-invalid-this
},
}));
return {event_id: eventId};
setImmediate(() => {
for (const tc of clients) {
if (tc.client === this) { // eslint-disable-line babel/no-invalid-this
console.log("sending remote echo!!");
tc.client.emit("Room.timeline", remoteEcho);
} else {
tc.client.emit("Room.timeline", event);
}
}
});
return Promise.resolve({event_id: eventId});
};
for (const userInfo of userInfos) {
@@ -92,3 +103,15 @@ export async function makeTestClients(userInfos, options) {
return clients;
}
export function setupWebcrypto() {
global.crypto = {
getRandomValues: (buf) => {
return nodeCrypto.randomFillSync(buf);
},
};
}
export function teardownWebcrypto() {
global.crypto = undefined;
}
@@ -0,0 +1,249 @@
/*
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 {VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE} from
"../../../../src/crypto/verification/request/VerificationRequest";
import {InRoomChannel} from "../../../../src/crypto/verification/request/InRoomChannel";
import {ToDeviceChannel} from
"../../../../src/crypto/verification/request/ToDeviceChannel";
import {MatrixEvent} from "../../../../src/models/event";
import {setupWebcrypto, teardownWebcrypto} from "./util";
function makeMockClient(userId, deviceId) {
let counter = 1;
let events = [];
const deviceEvents = {};
return {
getUserId() { return userId; },
getDeviceId() { return deviceId; },
sendEvent(roomId, type, content) {
counter = counter + 1;
const eventId = `$${userId}-${deviceId}-${counter}`;
events.push(new MatrixEvent({
sender: userId,
event_id: eventId,
room_id: roomId,
type,
content,
origin_server_ts: Date.now(),
}));
return Promise.resolve({event_id: eventId});
},
sendToDevice(type, msgMap) {
for (const userId of Object.keys(msgMap)) {
const deviceMap = msgMap[userId];
for (const deviceId of Object.keys(deviceMap)) {
const content = deviceMap[deviceId];
const event = new MatrixEvent({content, type});
deviceEvents[userId] = deviceEvents[userId] || {};
deviceEvents[userId][deviceId] = deviceEvents[userId][deviceId] || [];
deviceEvents[userId][deviceId].push(event);
}
}
return Promise.resolve();
},
popEvents() {
const e = events;
events = [];
return e;
},
popDeviceEvents(userId, deviceId) {
const forDevice = deviceEvents[userId];
const events = forDevice && forDevice[deviceId];
const result = events || [];
if (events) {
delete forDevice[deviceId];
}
return result;
},
};
}
const MOCK_METHOD = "mock-verify";
class MockVerifier {
constructor(channel, client, userId, deviceId, startEvent) {
this._channel = channel;
this._startEvent = startEvent;
}
get events() {
return [DONE_TYPE];
}
async start() {
if (this._startEvent) {
await this._channel.send(DONE_TYPE, {});
} else {
await this._channel.send(START_TYPE, {method: MOCK_METHOD});
}
}
async handleEvent(event) {
if (event.getType() === DONE_TYPE && !this._startEvent) {
await this._channel.send(DONE_TYPE, {});
}
}
canSwitchStartEvent() {
return false;
}
}
function makeRemoteEcho(event) {
return new MatrixEvent(Object.assign({}, event.event, {
unsigned: {
transaction_id: "abc",
},
}));
}
async function distributeEvent(ownRequest, theirRequest, event) {
await ownRequest.channel.handleEvent(
makeRemoteEcho(event), ownRequest, true);
await theirRequest.channel.handleEvent(event, theirRequest, true);
}
describe("verification request unit tests", function() {
beforeAll(function() {
setupWebcrypto();
});
afterAll(() => {
teardownWebcrypto();
});
it("transition from UNSENT to DONE through happy path", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()),
new Map([[MOCK_METHOD, MockVerifier]]), alice);
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"),
new Map([[MOCK_METHOD, MockVerifier]]), bob);
expect(aliceRequest.invalid).toBe(true);
expect(bobRequest.invalid).toBe(true);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
expect(requestEvent.getType()).toBe("m.room.message");
await distributeEvent(aliceRequest, bobRequest, requestEvent);
expect(aliceRequest.requested).toBe(true);
expect(bobRequest.requested).toBe(true);
await bobRequest.accept();
const [readyEvent] = bob.popEvents();
expect(readyEvent.getType()).toBe(READY_TYPE);
await distributeEvent(bobRequest, aliceRequest, readyEvent);
expect(bobRequest.ready).toBe(true);
expect(aliceRequest.ready).toBe(true);
const verifier = aliceRequest.beginKeyVerification(MOCK_METHOD);
await verifier.start();
const [startEvent] = alice.popEvents();
expect(startEvent.getType()).toBe(START_TYPE);
await distributeEvent(aliceRequest, bobRequest, startEvent);
expect(aliceRequest.started).toBe(true);
expect(aliceRequest.verifier).toBeInstanceOf(MockVerifier);
expect(bobRequest.started).toBe(true);
expect(bobRequest.verifier).toBeInstanceOf(MockVerifier);
await bobRequest.verifier.start();
const [bobDoneEvent] = bob.popEvents();
expect(bobDoneEvent.getType()).toBe(DONE_TYPE);
await distributeEvent(bobRequest, aliceRequest, bobDoneEvent);
const [aliceDoneEvent] = alice.popEvents();
expect(aliceDoneEvent.getType()).toBe(DONE_TYPE);
await distributeEvent(aliceRequest, bobRequest, aliceDoneEvent);
expect(aliceRequest.done).toBe(true);
expect(bobRequest.done).toBe(true);
});
it("methods only contains common methods", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob = makeMockClient("@bob:matrix.tld", "device1");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob.getUserId()),
new Map([["c", function() {}], ["a", function() {}]]), alice);
const bobRequest = new VerificationRequest(
new InRoomChannel(bob, "!room"),
new Map([["c", function() {}], ["b", function() {}]]), bob);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
await distributeEvent(aliceRequest, bobRequest, requestEvent);
await bobRequest.accept();
const [readyEvent] = bob.popEvents();
await distributeEvent(bobRequest, aliceRequest, readyEvent);
expect(aliceRequest.methods).toStrictEqual(["c"]);
expect(bobRequest.methods).toStrictEqual(["c"]);
});
it("other client accepting request puts it in observeOnly mode", async function() {
const alice = makeMockClient("@alice:matrix.tld", "device1");
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
const aliceRequest = new VerificationRequest(
new InRoomChannel(alice, "!room", bob1.getUserId()), new Map(), alice);
await aliceRequest.sendRequest();
const [requestEvent] = alice.popEvents();
const bob1Request = new VerificationRequest(
new InRoomChannel(bob1, "!room"), new Map(), bob1);
const bob2Request = new VerificationRequest(
new InRoomChannel(bob2, "!room"), new Map(), bob2);
await bob1Request.channel.handleEvent(requestEvent, bob1Request, true);
await bob2Request.channel.handleEvent(requestEvent, bob2Request, true);
await bob1Request.accept();
const [readyEvent] = bob1.popEvents();
expect(bob2Request.observeOnly).toBe(false);
await bob2Request.channel.handleEvent(readyEvent, bob2Request, true);
expect(bob2Request.observeOnly).toBe(true);
});
it("verify own device with to_device messages", async function() {
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
const bob1Request = new VerificationRequest(
new ToDeviceChannel(bob1, bob1.getUserId(), ["device1", "device2"],
ToDeviceChannel.makeTransactionId(), "device2"),
new Map([[MOCK_METHOD, MockVerifier]]), bob1);
const to = {userId: "@bob:matrix.tld", deviceId: "device2"};
const verifier = bob1Request.beginKeyVerification(MOCK_METHOD, to);
expect(verifier).toBeInstanceOf(MockVerifier);
await verifier.start();
const [startEvent] = bob1.popDeviceEvents(to.userId, to.deviceId);
expect(startEvent.getType()).toBe(START_TYPE);
const bob2Request = new VerificationRequest(
new ToDeviceChannel(bob2, bob2.getUserId(), ["device1"]),
new Map([[MOCK_METHOD, MockVerifier]]), bob2);
await bob2Request.channel.handleEvent(startEvent, bob2Request, true);
await bob2Request.verifier.start();
const [doneEvent1] = bob2.popDeviceEvents("@bob:matrix.tld", "device1");
expect(doneEvent1.getType()).toBe(DONE_TYPE);
await bob1Request.channel.handleEvent(doneEvent1, bob1Request, true);
const [doneEvent2] = bob1.popDeviceEvents("@bob:matrix.tld", "device2");
expect(doneEvent2.getType()).toBe(DONE_TYPE);
await bob2Request.channel.handleEvent(doneEvent2, bob2Request, true);
expect(bob1Request.done).toBe(true);
expect(bob2Request.done).toBe(true);
});
});
+5 -10
View File
@@ -617,15 +617,10 @@ describe("Room", function() {
}, event: true,
})]);
};
const setAliases = function(aliases, stateKey) {
if (!stateKey) {
stateKey = aliases.length
? aliases[0].split(':').splice(1).join(':') // domain+port
: 'fibble';
}
const setAltAliases = function(aliases) {
room.addLiveEvents([utils.mkEvent({
type: "m.room.aliases", room: roomId, skey: stateKey, content: {
aliases: aliases,
type: "m.room.canonical_alias", room: roomId, skey: "", content: {
alt_aliases: aliases,
}, event: true,
})]);
};
@@ -862,7 +857,7 @@ describe("Room", function() {
"(invite join_rules) rooms if a room name doesn't exist.", function() {
const alias = "#room_alias:here";
setJoinRule("invite");
setAliases([alias, "#another:here"]);
setAltAliases([alias, "#another:here"]);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
@@ -872,7 +867,7 @@ describe("Room", function() {
"(public join_rules) rooms if a room name doesn't exist.", function() {
const alias = "#room_alias:here";
setJoinRule("public");
setAliases([alias, "#another:here"]);
setAltAliases([alias, "#another:here"]);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
+176 -260
View File
File diff suppressed because it is too large Load Diff
+290 -136
View File
File diff suppressed because it is too large Load Diff
+113 -24
View File
@@ -23,6 +23,7 @@ limitations under the License.
import {decodeBase64, encodeBase64, pkSign, pkVerify} from './olmlib';
import {EventEmitter} from 'events';
import {logger} from '../logger';
import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store';
function publicKeyFromKeyInfo(keyInfo) {
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
@@ -40,8 +41,9 @@ export class CrossSigningInfo extends EventEmitter {
* @param {string} userId the user that the information is about
* @param {object} callbacks Callbacks used to interact with the app
* Requires getCrossSigningKey and saveCrossSigningKeys
* @param {object} cacheCallbacks Callbacks used to interact with the cache
*/
constructor(userId, callbacks) {
constructor(userId, callbacks, cacheCallbacks) {
super();
// you can't change the userId
@@ -50,6 +52,7 @@ export class CrossSigningInfo extends EventEmitter {
value: userId,
});
this._callbacks = callbacks || {};
this._cacheCallbacks = cacheCallbacks || {};
this.keys = {};
this.firstUse = true;
}
@@ -62,6 +65,8 @@ export class CrossSigningInfo extends EventEmitter {
* @returns {Array} An array with [ public key, Olm.PkSigning ]
*/
async getCrossSigningKey(type, expectedPubkey) {
const shouldCache = ["self_signing", "user_signing"].indexOf(type) >= 0;
if (!this._callbacks.getCrossSigningKey) {
throw new Error("No getCrossSigningKey callback supplied");
}
@@ -70,22 +75,47 @@ export class CrossSigningInfo extends EventEmitter {
expectedPubkey = this.getId(type);
}
const privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey);
function validateKey(key) {
if (!key) return;
const signing = new global.Olm.PkSigning();
const gotPubkey = signing.init_with_seed(key);
if (gotPubkey === expectedPubkey) {
return [gotPubkey, signing];
}
signing.free();
}
let privkey;
if (this._cacheCallbacks.getCrossSigningKeyCache && shouldCache) {
privkey = await this._cacheCallbacks
.getCrossSigningKeyCache(type, expectedPubkey);
}
const cacheresult = validateKey(privkey);
if (cacheresult) {
return cacheresult;
}
privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey);
const result = validateKey(privkey);
if (result) {
if (this._cacheCallbacks.storeCrossSigningKeyCache && shouldCache) {
await this._cacheCallbacks.storeCrossSigningKeyCache(type, privkey);
}
return result;
}
/* No keysource even returned a key */
if (!privkey) {
throw new Error(
"getCrossSigningKey callback for " + type + " returned falsey",
);
}
const signing = new global.Olm.PkSigning();
const gotPubkey = signing.init_with_seed(privkey);
if (gotPubkey !== expectedPubkey) {
signing.free();
throw new Error(
"Key type " + type + " from getCrossSigningKey callback did not match",
);
} else {
return [gotPubkey, signing];
}
/* We got some keys from the keysource, but none of them were valid */
throw new Error(
"Key type " + type + " from getCrossSigningKey callback did not match",
);
}
static fromStorage(obj, userId) {
@@ -111,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;
}
/**
@@ -348,6 +392,7 @@ export class CrossSigningInfo extends EventEmitter {
async signUser(key) {
if (!this.keys.user_signing) {
logger.info("No user signing key: not signing user");
return;
}
return this.signObject(key.keys.master, "user_signing");
@@ -360,6 +405,7 @@ export class CrossSigningInfo extends EventEmitter {
);
}
if (!this.keys.self_signing) {
logger.info("No self signing key: not signing device");
return;
}
return this.signObject(
@@ -414,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);
@@ -436,11 +485,22 @@ 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,
);
}
}
/**
* @returns {object} Cache callbacks
*/
getCacheCallbacks() {
return this._cacheCallbacks;
}
}
function deviceToObject(device, userId) {
@@ -494,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,
);
}
@@ -512,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()
));
}
/**
@@ -537,3 +601,28 @@ export class DeviceTrustLevel {
return this._tofu;
}
}
export function createCryptoStoreCacheCallbacks(store) {
return {
getCrossSigningKeyCache: function(type, _expectedPublicKey) {
return new Promise((resolve) => {
return store.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
store.getSecretStorePrivateKey(txn, resolve, type);
},
);
});
},
storeCrossSigningKeyCache: function(type, key) {
return store.doTxn(
'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
store.storeSecretStorePrivateKey(txn, type, key);
},
);
},
};
}
+52 -22
View File
@@ -197,16 +197,16 @@ export class DeviceList extends EventEmitter {
const resolveSavePromise = this._resolveSavePromise;
this._savePromiseTime = targetTime;
this._saveTimer = setTimeout(() => {
logger.log('Saving device tracking data at token ' + this._syncToken);
logger.log('Saving device tracking data', this._syncToken);
// null out savePromise now (after the delay but before the write),
// otherwise we could return the existing promise when the save has
// actually already happened. Likewise for the dirty flag.
// actually already happened.
this._savePromiseTime = null;
this._saveTimer = null;
this._savePromise = null;
this._resolveSavePromise = null;
this._dirty = false;
this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.storeEndToEndDeviceData({
@@ -217,7 +217,13 @@ export class DeviceList extends EventEmitter {
}, txn);
},
).then(() => {
// The device list is considered dirty until the write
// completes.
this._dirty = false;
resolveSavePromise();
}, err => {
logger.error('Failed to save device tracking data', this._syncToken);
logger.error(err);
});
}, delay);
}
@@ -311,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
*
@@ -373,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
*
@@ -382,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;
@@ -594,7 +621,7 @@ export class DeviceList extends EventEmitter {
*
* @param {String[]} users list of userIds
*
* @return {module:client.Promise} resolves when all the users listed have
* @return {Promise} resolves when all the users listed have
* been updated. rejects if there was a problem updating any of the
* users.
*/
@@ -699,7 +726,7 @@ class DeviceListUpdateSerialiser {
* @param {String} syncToken sync token to pass in the query request, to
* help the HS give the most recent results
*
* @return {module:client.Promise} resolves when all the users listed have
* @return {Promise} resolves when all the users listed have
* been updated. rejects if there was a problem updating any of the
* users.
*/
@@ -749,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);
+131 -14
View File
@@ -111,16 +111,52 @@ export function OlmDevice(cryptoStore) {
* Initialise the OlmAccount. This must be called before any other operations
* on the OlmDevice.
*
* Data from an exported Olm device can be provided
* in order to re-create this device.
*
* Attempts to load the OlmAccount from the crypto store, or creates one if none is
* found.
*
* Reads the device keys from the OlmAccount object.
*
* @param {object} opts
* @param {object} opts.fromExportedDevice (Optional) data from exported device
* that must be re-created.
* If present, opts.pickleKey is ignored
* (exported data already provides a pickle key)
* @param {object} opts.pickleKey (Optional) pickle key to set instead of default one
*/
OlmDevice.prototype.init = async function() {
OlmDevice.prototype.init = async function(opts = {}) {
let e2eKeys;
const account = new global.Olm.Account();
const { pickleKey, fromExportedDevice } = opts;
try {
await _initialiseAccount(this._cryptoStore, this._pickleKey, account);
if (fromExportedDevice) {
if (pickleKey) {
console.warn(
'ignoring opts.pickleKey'
+ ' because opts.fromExportedDevice is present.',
);
}
this._pickleKey = fromExportedDevice.pickleKey;
await _initialiseFromExportedDevice(
fromExportedDevice,
this._cryptoStore,
this._pickleKey,
account,
);
} else {
if (pickleKey) {
this._pickleKey = pickleKey;
}
await _initialiseAccount(
this._cryptoStore,
this._pickleKey,
account,
);
}
e2eKeys = JSON.parse(account.identity_keys());
this._maxOneTimeKeys = account.max_number_of_one_time_keys();
@@ -132,18 +168,67 @@ OlmDevice.prototype.init = async function() {
this.deviceEd25519Key = e2eKeys.ed25519;
};
async function _initialiseAccount(cryptoStore, pickleKey, account) {
await cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
cryptoStore.getAccount(txn, (pickledAccount) => {
if (pickledAccount !== null) {
account.unpickle(pickleKey, pickledAccount);
} else {
account.create();
pickledAccount = account.pickle(pickleKey);
cryptoStore.storeAccount(txn, pickledAccount);
}
});
/**
* Populates the crypto store using data that was exported from an existing device.
* Note that for now only the “account” and “sessions” stores are populated;
* Other stores will be as with a new device.
*
* @param {Object} exportedData Data exported from another device
* through the “export” method.
* @param {module:crypto/store/base~CryptoStore} cryptoStore storage for the crypto layer
* @param {string} pickleKey the key that was used to pickle the exported data
* @param {Olm.Account} account an olm account to initialize
*/
async function _initialiseFromExportedDevice(
exportedData,
cryptoStore,
pickleKey,
account,
) {
await cryptoStore.doTxn(
'readwrite',
[
IndexedDBCryptoStore.STORE_ACCOUNT,
IndexedDBCryptoStore.STORE_SESSIONS,
],
(txn) => {
cryptoStore.storeAccount(txn, exportedData.pickledAccount);
exportedData.sessions.forEach((session) => {
const {
deviceKey,
sessionId,
} = session;
const sessionInfo = {
session: session.session,
lastReceivedMessageTs: session.lastReceivedMessageTs,
};
cryptoStore.storeEndToEndSession(
deviceKey,
sessionId,
sessionInfo,
txn,
);
});
});
account.unpickle(pickleKey, exportedData.pickledAccount);
}
async function _initialiseAccount(cryptoStore, pickleKey, account) {
await cryptoStore.doTxn(
'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
cryptoStore.getAccount(txn, (pickledAccount) => {
if (pickledAccount !== null) {
account.unpickle(pickleKey, pickledAccount);
} else {
account.create();
pickledAccount = account.pickle(pickleKey);
cryptoStore.storeAccount(txn, pickledAccount);
}
});
},
);
}
/**
@@ -191,6 +276,38 @@ OlmDevice.prototype._storeAccount = function(txn, account) {
this._cryptoStore.storeAccount(txn, account.pickle(this._pickleKey));
};
/**
* Export data for re-creating the Olm device later.
* TODO export data other than just account and (P2P) sessions.
*
* @return {Promise<object>} The exported data
*/
OlmDevice.prototype.export = async function() {
const result = {
pickleKey: this._pickleKey,
};
await this._cryptoStore.doTxn(
'readonly',
[
IndexedDBCryptoStore.STORE_ACCOUNT,
IndexedDBCryptoStore.STORE_SESSIONS,
],
(txn) => {
this._cryptoStore.getAccount(txn, (pickledAccount) => {
result.pickledAccount = pickledAccount;
});
result.sessions = [];
// Note that the pickledSession object we get in the callback
// is not exactly the same thing you get in method _getSession
// see documentation of IndexedDBCryptoStore.getAllEndToEndSessions
this._cryptoStore.getAllEndToEndSessions(txn, (pickledSession) => {
result.sessions.push(pickledSession);
});
},
);
return result;
};
/**
* extract an OlmSession from the session store and call the given function
* The session is useable only within the callback passed to this
@@ -1110,7 +1227,7 @@ OlmDevice.prototype.decryptGroupMessage = async function(
*
* @param {string} roomId room in which the message was received
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {sring} sessionId session identifier
* @param {string} sessionId session identifier
*
* @returns {Promise<boolean>} true if we have the keys to this session
*/
+186 -101
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:
@@ -239,24 +247,30 @@ export class SecretStorage extends EventEmitter {
}
/**
* 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.
* Temporary method to fix up existing accounts where secrets
* are incorrectly stored without the 'encrypted' level
*
* @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
* @param {object} secretInfo The account data object
* @returns {object} The fixed object or null if no fix was performed
*/
storePassthrough(name, keyId) {
return this._baseApis.setAccountData(name, {
[keyId]: {
passthrough: true,
},
});
async _fixupStoredSecret(name, secretInfo) {
// We assume the secret was only stored passthrough for 1
// key - this was all the broken code supported.
const keys = Object.keys(secretInfo);
if (
keys.length === 1 && keys[0] !== 'encrypted' &&
secretInfo[keys[0]].passthrough
) {
const hasKey = await this.hasKey(keys[0]);
if (hasKey) {
console.log("Fixing up passthrough secret: " + name);
await this.storePassthrough(name, keys[0]);
const newData = await this._baseApis.getAccountDataFromServer(name);
return newData;
}
}
return null;
}
/**
@@ -267,12 +281,16 @@ export class SecretStorage extends EventEmitter {
* @return {string} the contents of the secret
*/
async get(name) {
const secretInfo = await this._baseApis.getAccountDataFromServer(name);
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
if (!secretInfo) {
return;
}
if (!secretInfo.encrypted) {
throw new Error("Content is not encrypted!");
// try to fix it up
secretInfo = await this._fixupStoredSecret(name, secretInfo);
if (!secretInfo || !secretInfo.encrypted) {
throw new Error("Content is not encrypted!");
}
}
// get possible keys to decrypt
@@ -284,9 +302,18 @@ export class SecretStorage extends EventEmitter {
);
const encInfo = secretInfo.encrypted[keyId];
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
&& encInfo.ephemeral) {
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) ||
encInfo.passthrough
)
) {
keys[keyId] = keyInfo;
}
break;
@@ -299,7 +326,7 @@ export class SecretStorage extends EventEmitter {
let decryption;
try {
// fetch private key from app
[keyId, decryption] = await this._getSecretStorageKey(keys);
[keyId, decryption] = await this._getSecretStorageKey(keys, name);
const encInfo = secretInfo.encrypted[keyId];
@@ -307,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();
}
}
@@ -325,17 +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
const secretInfo = await this._baseApis.getAccountDataFromServer(name);
if (!secretInfo || !secretInfo.encrypted) {
return false;
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
if (!secretInfo) return null;
if (!secretInfo.encrypted) {
// try to fix it up
secretInfo = await this._fixupStoredSecret(name, secretInfo);
if (!secretInfo || !secretInfo.encrypted) {
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)) {
@@ -343,27 +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) {
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;
}
/**
@@ -415,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,
});
@@ -468,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: {
@@ -504,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}`);
}
}
}
@@ -516,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
@@ -540,12 +603,12 @@ export class SecretStorage extends EventEmitter {
}
}
async _getSecretStorageKey(keys) {
async _getSecretStorageKey(keys, name) {
if (!this._cryptoCallbacks.getSecretStorageKey) {
throw new Error("No getSecretStorageKey callback supplied");
}
const returned = await this._cryptoCallbacks.getSecretStorageKey({ keys });
const returned = await this._cryptoCallbacks.getSecretStorageKey({ keys }, name);
if (!returned) {
throw new Error("getSecretStorageKey callback returned falsey");
@@ -560,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);
}
+10 -1
View File
@@ -60,6 +60,15 @@ export class EncryptionAlgorithm {
this._roomId = params.roomId;
}
/**
* 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
*/
prepareToEncrypt(room) {
}
/**
* Encrypt a message event
*
@@ -70,7 +79,7 @@ export class EncryptionAlgorithm {
* @param {string} eventType
* @param {object} plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
* @return {Promise} Promise which resolves to the new event body
*/
/**
+295 -184
View File
@@ -185,15 +185,15 @@ 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 {module:client.Promise} Promise which resolves to the
* @return {Promise} Promise which resolves to the
* OutboundSessionInfo when setup is complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = async function(
devicesInRoom, blocked,
devicesInRoom, blocked, singleOlmCreationPhase,
) {
const self = this;
let session;
// takes the previous OutboundSessionInfo, and considers whether to create
@@ -201,12 +201,12 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
// Updates `session` to hold the final OutboundSessionInfo.
//
// returns a promise which resolves once the keyshare is successful.
async function prepareSession(oldSession) {
const prepareSession = async (oldSession) => {
session = oldSession;
// need to make a brand new session?
if (session && session.needsRotation(self._sessionRotationPeriodMsgs,
self._sessionRotationPeriodMs)
if (session && session.needsRotation(this._sessionRotationPeriodMsgs,
this._sessionRotationPeriodMs)
) {
logger.log("Starting new megolm session because we need to rotate.");
session = null;
@@ -218,32 +218,20 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
}
if (!session) {
logger.log(`Starting new megolm session for room ${self._roomId}`);
session = await self._prepareNewSession();
logger.log(`Starting new megolm session for room ${this._roomId}`);
session = await this._prepareNewSession();
logger.log(`Started new megolm session ${session.sessionId} ` +
`for room ${self._roomId}`);
self._outboundSessions[session.sessionId] = session;
`for room ${this._roomId}`);
this._outboundSessions[session.sessionId] = session;
}
// now check if we need to share with any devices
const shareMap = {};
for (const userId in devicesInRoom) {
if (!devicesInRoom.hasOwnProperty(userId)) {
continue;
}
const userDevices = devicesInRoom[userId];
for (const deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
const deviceInfo = userDevices[deviceId];
for (const [userId, userDevices] of Object.entries(devicesInRoom)) {
for (const [deviceId, deviceInfo] of Object.entries(userDevices)) {
const key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
if (key == this._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
}
@@ -258,51 +246,99 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
}
}
const errorDevices = [];
await self._shareKeyWithDevices(
session, shareMap, errorDevices,
const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
const payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session.sessionId,
session_key: key.key,
chain_index: key.chain_index,
},
};
const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(
this._olmDevice, this._baseApis, shareMap,
);
// are there any new blocked devices that we need to notify?
const blockedMap = {};
for (const userId in blocked) {
if (!blocked.hasOwnProperty(userId)) {
continue;
}
await Promise.all([
(async () => {
// share keys with devices that we already have a session for
await this._shareKeyWithOlmSessions(
session, key, payload, olmSessions,
);
})(),
(async () => {
const errorDevices = [];
const userBlockedDevices = blocked[userId];
// meanwhile, establish olm sessions for devices that we don't
// 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,
singleOlmCreationPhase ? 10000 : 2000, failedServers,
);
for (const deviceId in userBlockedDevices) {
if (!userBlockedDevices.hasOwnProperty(deviceId)) {
continue;
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});
}
}
await this._shareKeyWithDevices(
session, key, payload, retryDevices, failedDevices,
);
await this._notifyFailedOlmDevices(session, key, failedDevices);
})();
} else {
await this._notifyFailedOlmDevices(session, key, errorDevices);
}
})(),
(async () => {
// also, notify blocked devices that they're blocked
const blockedMap = {};
for (const [userId, userBlockedDevices] of Object.entries(blocked)) {
for (const [deviceId, device] of Object.entries(userBlockedDevices)) {
if (
!session.blockedDevicesNotified[userId] ||
session.blockedDevicesNotified[userId][deviceId] === undefined
) {
blockedMap[userId] = blockedMap[userId] || {};
blockedMap[userId][deviceId] = {device};
}
}
}
if (
!session.blockedDevicesNotified[userId] ||
session.blockedDevicesNotified[userId][deviceId] === undefined
) {
blockedMap[userId] = blockedMap[userId] || [];
blockedMap[userId].push(userBlockedDevices[deviceId]);
}
}
}
const filteredErrorDevices =
await self._olmDevice.filterOutNotifiedErrorDevices(errorDevices);
for (const {userId, deviceInfo} of filteredErrorDevices) {
blockedMap[userId] = blockedMap[userId] || [];
blockedMap[userId].push({
code: "m.no_olm",
reason: WITHHELD_MESSAGES["m.no_olm"],
deviceInfo,
});
}
// notify blocked devices that they're blocked
await self._notifyBlockedDevices(session, blockedMap);
}
await this._notifyBlockedDevices(session, blockedMap);
})(),
]);
};
// helper which returns the session prepared by prepareSession
function returnSession() {
@@ -349,96 +385,47 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
};
/**
* Splits the user device map into multiple chunks to reduce the number of
* devices we encrypt to per API call. Also filters out devices we don't have
* a session with.
* Determines what devices in devicesByUser don't have an olm session as given
* in devicemap.
*
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
* @param {object} devicemap the devices that have olm sessions, as returned by
* olmlib.ensureOlmSessionsForDevices.
* @param {object} devicesByUser a map of user IDs to array of deviceInfo
* @param {array} [noOlmDevices] an array to fill with devices that don't have
* olm sessions
*
* @param {number} chainIndex current chain index
*
* @param {object<userId, deviceId>} devicemap
* mapping from userId to deviceId to {@link module:crypto~OlmSessionResult}
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @param {array<object>} errorDevices
* array that will be populated with the devices that can't get an
* olm session for
*
* @return {array<object<userid, deviceInfo>>}
* @return {array} an array of devices that don't have olm sessions. If
* noOlmDevices is specified, then noOlmDevices will be returned.
*/
MegolmEncryption.prototype._splitUserDeviceMap = function(
session, chainIndex, devicemap, devicesByUser, errorDevices,
MegolmEncryption.prototype._getDevicesWithoutSessions = function(
devicemap, devicesByUser, noOlmDevices,
) {
const maxUsersPerRequest = 20;
noOlmDevices = noOlmDevices || [];
// use an array where the slices of a content map gets stored
const mapSlices = [];
let currentSliceId = 0; // start inserting in the first slice
let entriesInCurrentSlice = 0;
for (const userId of Object.keys(devicesByUser)) {
const devicesToShareWith = devicesByUser[userId];
for (const [userId, devicesToShareWith] of Object.entries(devicesByUser)) {
const sessionResults = devicemap[userId];
for (let i = 0; i < devicesToShareWith.length; i++) {
const deviceInfo = devicesToShareWith[i];
for (const deviceInfo of devicesToShareWith) {
const deviceId = deviceInfo.deviceId;
const sessionResult = sessionResults[deviceId];
if (!sessionResult.sessionId) {
// no session with this device, probably because there
// were no one-time keys.
//
// we could send them a to_device message anyway, as a
// signal that they have missed out on the key sharing
// message because of the lack of keys, but there's not
// much point in that really; it will mostly serve to clog
// up to_device inboxes.
// 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, chainIndex);
errorDevices.push({userId, deviceInfo});
noOlmDevices.push({userId, deviceInfo});
delete sessionResults[deviceId];
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
continue;
}
logger.log(
"share keys with device " + userId + ":" + deviceId,
);
if (!mapSlices[currentSliceId]) {
mapSlices[currentSliceId] = [];
}
mapSlices[currentSliceId].push({
userId: userId,
deviceInfo: deviceInfo,
});
entriesInCurrentSlice++;
}
// We do this in the per-user loop as we prefer that all messages to the
// same user end up in the same API call to make it easier for the
// server (e.g. only have to send one EDU if a remote user, etc). This
// does mean that if a user has many devices we may go over the desired
// limit, but its not a hard limit so that is fine.
if (entriesInCurrentSlice > maxUsersPerRequest) {
// the current slice is filled up. Start inserting into the next slice
entriesInCurrentSlice = 0;
currentSliceId++;
}
}
return mapSlices;
return noOlmDevices;
};
/**
@@ -451,20 +438,18 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
*
* @return {array<array<object>>} the blocked devices, split into chunks
*/
MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
const maxUsersPerRequest = 20;
MegolmEncryption.prototype._splitDevices = function(devicesByUser) {
const maxDevicesPerRequest = 20;
// use an array where the slices of a content map gets stored
let currentSlice = [];
const mapSlices = [currentSlice];
for (const userId of Object.keys(devicesByUser)) {
const userBlockedDevicesToShareWith = devicesByUser[userId];
for (const blockedInfo of userBlockedDevicesToShareWith) {
for (const [userId, userDevices] of Object.entries(devicesByUser)) {
for (const deviceInfo of Object.values(userDevices)) {
currentSlice.push({
userId: userId,
blockedInfo: blockedInfo,
deviceInfo: deviceInfo.device,
});
}
@@ -473,7 +458,7 @@ MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
// server (e.g. only have to send one EDU if a remote user, etc). This
// does mean that if a user has many devices we may go over the desired
// limit, but its not a hard limit so that is fine.
if (currentSlice.length > maxUsersPerRequest) {
if (currentSlice.length > maxDevicesPerRequest) {
// the current slice is filled up. Start inserting into the next slice
currentSlice = [];
mapSlices.push(currentSlice);
@@ -497,7 +482,7 @@ MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
*
* @param {object} payload fields to include in the encrypted payload
*
* @return {module:client.Promise} Promise which resolves once the key sharing
* @return {Promise} Promise which resolves once the key sharing
* for the given userDeviceMap is generated and has been sent.
*/
MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
@@ -558,7 +543,7 @@ MegolmEncryption.prototype._encryptAndSendKeysToDevices = function(
*
* @param {object} payload fields to include in the notification payload
*
* @return {module:client.Promise} Promise which resolves once the notifications
* @return {Promise} Promise which resolves once the notifications
* for the given userDeviceMap is generated and has been sent.
*/
MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function(
@@ -568,7 +553,7 @@ MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function(
for (const val of userDeviceMap) {
const userId = val.userId;
const blockedInfo = val.blockedInfo;
const blockedInfo = val.deviceInfo;
const deviceInfo = blockedInfo.deviceInfo;
const deviceId = deviceInfo.deviceId;
@@ -643,9 +628,7 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, {
[userId]: {
[device.deviceId]: device,
},
[userId]: [device],
},
);
@@ -688,37 +671,44 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
};
/**
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object} key the session key as returned by
* OlmDevice.getOutboundGroupSessionKey
*
* @param {object} payload the base to-device message payload for sharing keys
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @param {array<object>} errorDevices
* array that will be populated with the devices that we can't get an
* olm session for
*
* @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, devicesByUser, errorDevices,
session, key, payload, devicesByUser, errorDevices, otkTimeout, failedServers,
) {
const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
const payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session.sessionId,
session_key: key.key,
chain_index: key.chain_index,
},
};
const devicemap = await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser,
this._olmDevice, this._baseApis, devicesByUser, otkTimeout, failedServers,
);
const userDeviceMaps = this._splitUserDeviceMap(
session, key.chain_index, devicemap, devicesByUser, errorDevices,
);
this._getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices);
await this._shareKeyWithOlmSessions(session, key, payload, devicemap);
};
MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
session, key, payload, devicemap,
) {
const userDeviceMaps = this._splitDevices(devicemap);
for (let i = 0; i < userDeviceMaps.length; i++) {
try {
@@ -736,6 +726,52 @@ MegolmEncryption.prototype._shareKeyWithDevices = 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.
*
@@ -754,7 +790,7 @@ MegolmEncryption.prototype._notifyBlockedDevices = async function(
sender_key: this._olmDevice.deviceCurve25519Key,
};
const userDeviceMaps = this._splitBlockedDevices(devicesByUser);
const userDeviceMaps = this._splitDevices(devicesByUser);
for (let i = 0; i < userDeviceMaps.length; i++) {
try {
@@ -772,6 +808,38 @@ MegolmEncryption.prototype._notifyBlockedDevices = async function(
}
};
/**
* 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
*/
MegolmEncryption.prototype.prepareToEncrypt = function(room) {
if (this.encryptionPreparation) {
// We're already preparing something, so don't do anything else.
// FIXME: check if we need to restart
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
return;
}
logger.debug(`Preparing to encrypt events for ${this._roomId}`);
this.encryptionPreparation = (async () => {
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
// Drop unknown devices for now. When the message gets sent, we'll
// throw an error, but we'll still be prepared to send to the known
// devices.
this._removeUnknownDevices(devicesInRoom);
}
await this._ensureOutboundSession(devicesInRoom, blocked, true);
delete this.encryptionPreparation;
})();
};
/**
* @inheritdoc
*
@@ -779,41 +847,51 @@ MegolmEncryption.prototype._notifyBlockedDevices = async function(
* @param {string} eventType
* @param {object} content plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
* @return {Promise} Promise which resolves to the new event body
*/
MegolmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
const self = this;
logger.log(`Starting to encrypt event for ${this._roomId}`);
if (this.encryptionPreparation) {
// If we started sending keys, wait for it to be done.
// FIXME: check if we need to cancel
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
try {
await this.encryptionPreparation;
} catch (e) {
// ignore any errors -- if the preparation failed, we'll just
// restart everything here
}
}
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
// check if any of these devices are not yet known to the user.
// if so, warn the user so they can verify or ignore.
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
self._checkForUnknownDevices(devicesInRoom);
this._checkForUnknownDevices(devicesInRoom);
}
const session = await self._ensureOutboundSession(devicesInRoom, blocked);
const session = await this._ensureOutboundSession(devicesInRoom, blocked);
const payloadJson = {
room_id: self._roomId,
room_id: this._roomId,
type: eventType,
content: content,
};
const ciphertext = self._olmDevice.encryptGroupMessage(
const ciphertext = this._olmDevice.encryptGroupMessage(
session.sessionId, JSON.stringify(payloadJson),
);
const encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext,
session_id: session.sessionId,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
// XXX: Do we still need this now that m.new_device messages
// no longer exist since #483?
device_id: self._deviceId,
device_id: this._deviceId,
};
session.useCount++;
@@ -861,12 +939,33 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) {
}
};
/**
* Remove unknown devices from a set of devices. The devicesInRoom parameter
* will be modified.
*
* @param {Object} devicesInRoom userId -> {deviceId -> object}
* devices we should shared the session with.
*/
MegolmEncryption.prototype._removeUnknownDevices = function(devicesInRoom) {
for (const [userId, userDevices] of Object.entries(devicesInRoom)) {
for (const [deviceId, device] of Object.entries(userDevices)) {
if (device.isUnverified() && !device.isKnown()) {
delete userDevices[deviceId];
}
}
if (Object.keys(userDevices).length === 0) {
delete devicesInRoom[userId];
}
}
};
/**
* Get the list of unblocked devices for all users in the room
*
* @param {module:models/room} room
*
* @return {module:client.Promise} Promise which resolves to an array whose
* @return {Promise} Promise which resolves to an array whose
* first element is a map from userId to deviceId to deviceInfo indicating
* the devices that messages should be encrypted to, and whose second
* element is a map from userId to deviceId to data indicating the devices
@@ -904,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] = {};
@@ -1247,6 +1348,9 @@ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
if (content.code === "m.no_olm") {
const sender = event.getSender();
logger.warn(
`${sender}:${senderKey} was unable to establish an olm session with us`,
);
// if the sender says that they haven't been able to establish an olm
// session, let's proactively establish one
@@ -1258,21 +1362,30 @@ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
if (await this._olmDevice.getSessionIdForDevice(senderKey)) {
// a session has already been established, so we don't need to
// create a new one.
logger.debug("New session already created. Not creating a new one.");
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true);
this.retryDecryptionFromSender(senderKey);
return;
}
const device = this._crypto._deviceList.getDeviceByIdentityKey(
let device = this._crypto._deviceList.getDeviceByIdentityKey(
content.algorithm, senderKey,
);
if (!device) {
logger.info(
"Couldn't find device for identity key " + senderKey +
": not establishing session",
// if we don't know about the device, fetch the user's devices again
// and retry before giving up
await this._crypto.downloadKeys([sender], false);
device = this._crypto._deviceList.getDeviceByIdentityKey(
content.algorithm, senderKey,
);
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", false);
this.retryDecryptionFromSender(senderKey);
return;
if (!device) {
logger.info(
"Couldn't find device for identity key " + senderKey +
": not establishing session",
);
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", false);
this.retryDecryptionFromSender(senderKey);
return;
}
}
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, {[sender]: [device]}, false,
@@ -1481,7 +1594,6 @@ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionI
MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey) {
const senderPendingEvents = this._pendingEvents[senderKey];
logger.warn(senderPendingEvents);
if (!senderPendingEvents) {
return true;
}
@@ -1491,7 +1603,6 @@ MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey)
await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => {
await Promise.all([...pending].map(async (ev) => {
try {
logger.warn(ev.getId());
await ev.attemptDecryption(this._crypto);
} catch (e) {
// don't die if something goes wrong
+2 -2
View File
@@ -54,7 +54,7 @@ utils.inherits(OlmEncryption, EncryptionAlgorithm);
* @private
* @param {string[]} roomMembers list of currently-joined users in the room
* @return {module:client.Promise} Promise which resolves when setup is complete
* @return {Promise} Promise which resolves when setup is complete
*/
OlmEncryption.prototype._ensureSession = function(roomMembers) {
if (this._prepPromise) {
@@ -85,7 +85,7 @@ OlmEncryption.prototype._ensureSession = function(roomMembers) {
* @param {string} eventType
* @param {object} content plaintext event content
*
* @return {module:client.Promise} Promise which resolves to the new event body
* @return {Promise} Promise which resolves to the new event body
*/
OlmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
// pick the list of recipients based on the membership list.
+703 -248
View File
File diff suppressed because it is too large Load Diff
+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);
+84 -17
View File
@@ -113,6 +113,57 @@ export async function encryptMessageForDevice(
);
}
/**
* Get the existing olm sessions for the given devices, and the devices that
* don't have olm sessions.
*
* @param {module:crypto/OlmDevice} olmDevice
*
* @param {module:base-apis~MatrixBaseApis} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices to ensure sessions for
*
* @return {Promise} resolves to an array. The first element of the array is a
* a map of user IDs to arrays of deviceInfo, representing the devices that
* don't have established olm sessions. The second element of the array is
* a map from userId to deviceId to {@link module:crypto~OlmSessionResult}
*/
export async function getExistingOlmSessions(
olmDevice, baseApis, devicesByUser,
) {
const devicesWithoutSession = {};
const sessions = {};
const promises = [];
for (const [userId, devices] of Object.entries(devicesByUser)) {
for (const deviceInfo of devices) {
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
promises.push((async () => {
const sessionId = await olmDevice.getSessionIdForDevice(
key, true,
);
if (sessionId === null) {
devicesWithoutSession[userId] = devicesWithoutSession[userId] || [];
devicesWithoutSession[userId].push(deviceInfo);
} else {
sessions[userId] = sessions[userId] || {};
sessions[userId][deviceId] = {
device: deviceInfo,
sessionId: sessionId,
};
}
})());
}
}
await Promise.all(promises);
return [devicesWithoutSession, sessions];
}
/**
* Try to make sure we have established olm sessions for the given devices.
*
@@ -123,30 +174,37 @@ export async function encryptMessageForDevice(
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices to ensure sessions for
*
* @param {bolean} force If true, establish a new session even if one already exists.
* Optional.
* @param {boolean} [force=false] If true, establish a new session even if one
* already exists.
*
* @return {module:client.Promise} resolves once the sessions are complete, to
* @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,
olmDevice, baseApis, devicesByUser, force, otkTimeout, failedServers,
) {
if (typeof force === "number") {
failedServers = otkTimeout;
otkTimeout = force;
force = false;
}
const devicesWithoutSession = [
// [userId, deviceId], ...
];
const result = {};
const resolveSession = {};
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
for (const [userId, devices] of Object.entries(devicesByUser)) {
result[userId] = {};
const devices = devicesByUser[userId];
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
for (const deviceInfo of devices) {
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
if (!olmDevice._sessionsInProgress[key]) {
@@ -197,7 +255,7 @@ export async function ensureOlmSessionsForDevices(
let res;
try {
res = await baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm,
devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout,
);
} catch (e) {
for (const resolver of Object.values(resolveSession)) {
@@ -207,14 +265,14 @@ 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 in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
for (const [userId, devices] of Object.entries(devicesByUser)) {
const userRes = otk_res[userId] || {};
const devices = devicesByUser[userId];
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
@@ -407,6 +465,15 @@ export function encodeBase64(uint8Array) {
return Buffer.from(uint8Array).toString("base64");
}
/**
* Encode a typed array of uint8 as unpadded base64.
* @param {Uint8Array} uint8Array The data to encode.
* @return {string} The unpadded base64.
*/
export function encodeUnpaddedBase64(uint8Array) {
return encodeBase64(uint8Array).replace(/=+$/g, '');
}
/**
* Decode a base64 string to a typed array of uint8.
* @param {string} base64 The base64 to decode.
@@ -341,18 +341,39 @@ export class Backend {
};
}
getSecretStorePrivateKey(txn, func, type) {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get(`ssss_cache:${type}`);
getReq.onsuccess = function() {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, e);
}
};
}
storeCrossSigningKeys(txn, keys) {
const objectStore = txn.objectStore("account");
objectStore.put(keys, "crossSigningKeys");
}
storeSecretStorePrivateKey(txn, type, key) {
const objectStore = txn.objectStore("account");
objectStore.put(key, `ssss_cache:${type}`);
}
// Olm Sessions
countEndToEndSessions(txn, func) {
const objectStore = txn.objectStore("sessions");
const countReq = objectStore.count();
countReq.onsuccess = function() {
func(countReq.result);
try {
func(countReq.result);
} catch (e) {
abortWithException(txn, e);
}
};
}
@@ -402,16 +423,16 @@ export class Backend {
const objectStore = txn.objectStore("sessions");
const getReq = objectStore.openCursor();
getReq.onsuccess = function() {
const cursor = getReq.result;
if (cursor) {
func(cursor.value);
cursor.continue();
} else {
try {
try {
const cursor = getReq.result;
if (cursor) {
func(cursor.value);
cursor.continue();
} else {
func(null);
} catch (e) {
abortWithException(txn, e);
}
} catch (e) {
abortWithException(txn, e);
}
};
}
+72 -111
View File
@@ -46,6 +46,7 @@ export class IndexedDBCryptoStore {
this._indexedDB = indexedDB;
this._dbName = dbName;
this._backendPromise = null;
this._backend = null;
}
static exists(indexedDB, dbName) {
@@ -56,10 +57,12 @@ export class IndexedDBCryptoStore {
* Ensure the database exists and is up-to-date, or fall back to
* a local storage or in-memory store.
*
* This must be called before the store can be used.
*
* @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend,
* or a MemoryCryptoStore
*/
_connect() {
startup() {
if (this._backendPromise) {
return this._backendPromise;
}
@@ -135,6 +138,8 @@ export class IndexedDBCryptoStore {
);
return new MemoryCryptoStore();
}
}).then(backend => {
this._backend = backend;
});
return this._backendPromise;
@@ -189,9 +194,7 @@ export class IndexedDBCryptoStore {
* same instance as passed in, or the existing one.
*/
getOrAddOutgoingRoomKeyRequest(request) {
return this._connect().then((backend) => {
return backend.getOrAddOutgoingRoomKeyRequest(request);
});
return this._backend.getOrAddOutgoingRoomKeyRequest(request);
}
/**
@@ -205,9 +208,7 @@ export class IndexedDBCryptoStore {
* not found
*/
getOutgoingRoomKeyRequest(requestBody) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequest(requestBody);
});
return this._backend.getOutgoingRoomKeyRequest(requestBody);
}
/**
@@ -221,9 +222,7 @@ export class IndexedDBCryptoStore {
* requests in those states, an arbitrary one is chosen.
*/
getOutgoingRoomKeyRequestByState(wantedStates) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequestByState(wantedStates);
});
return this._backend.getOutgoingRoomKeyRequestByState(wantedStates);
}
/**
@@ -237,11 +236,9 @@ export class IndexedDBCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
*/
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, wantedStates,
);
});
return this._backend.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, wantedStates,
);
}
/**
@@ -257,11 +254,9 @@ export class IndexedDBCryptoStore {
* updated request, or null if no matching row was found
*/
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
return this._connect().then((backend) => {
return backend.updateOutgoingRoomKeyRequest(
requestId, expectedState, updates,
);
});
return this._backend.updateOutgoingRoomKeyRequest(
requestId, expectedState, updates,
);
}
/**
@@ -274,9 +269,7 @@ export class IndexedDBCryptoStore {
* @returns {Promise} resolves once the operation is completed
*/
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
return this._connect().then((backend) => {
return backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
});
return this._backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
}
// Olm Account
@@ -289,9 +282,7 @@ export class IndexedDBCryptoStore {
* @param {function(string)} func Called with the account pickle
*/
getAccount(txn, func) {
this._backendPromise.then(backend => {
backend.getAccount(txn, func);
});
this._backend.getAccount(txn, func);
}
/**
@@ -302,9 +293,7 @@ export class IndexedDBCryptoStore {
* @param {string} newData The new account pickle to store.
*/
storeAccount(txn, newData) {
this._backendPromise.then(backend => {
backend.storeAccount(txn, newData);
});
this._backend.storeAccount(txn, newData);
}
/**
@@ -316,9 +305,16 @@ export class IndexedDBCryptoStore {
* { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed
*/
getCrossSigningKeys(txn, func) {
this._backendPromise.then(backend => {
backend.getCrossSigningKeys(txn, func);
});
this._backend.getCrossSigningKeys(txn, func);
}
/**
* @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the private key
* @param {string} type A key type
*/
getSecretStorePrivateKey(txn, func, type) {
this._backend.getSecretStorePrivateKey(txn, func, type);
}
/**
@@ -328,9 +324,18 @@ export class IndexedDBCryptoStore {
* @param {string} keys keys object as getCrossSigningKeys()
*/
storeCrossSigningKeys(txn, keys) {
this._backendPromise.then(backend => {
backend.storeCrossSigningKeys(txn, keys);
});
this._backend.storeCrossSigningKeys(txn, keys);
}
/**
* Write the cross-signing private keys back to the store
*
* @param {*} txn An active transaction. See doTxn().
* @param {string} type The type of cross-signing private key to store
* @param {string} key keys object as getCrossSigningKeys()
*/
storeSecretStorePrivateKey(txn, type, key) {
this._backend.storeSecretStorePrivateKey(txn, type, key);
}
// Olm sessions
@@ -341,9 +346,7 @@ export class IndexedDBCryptoStore {
* @param {function(int)} func Called with the count of sessions
*/
countEndToEndSessions(txn, func) {
this._backendPromise.then(backend => {
backend.countEndToEndSessions(txn, func);
});
this._backend.countEndToEndSessions(txn, func);
}
/**
@@ -359,9 +362,7 @@ export class IndexedDBCryptoStore {
* a message.
*/
getEndToEndSession(deviceKey, sessionId, txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndSession(deviceKey, sessionId, txn, func);
});
this._backend.getEndToEndSession(deviceKey, sessionId, txn, func);
}
/**
@@ -376,9 +377,7 @@ export class IndexedDBCryptoStore {
* a message.
*/
getEndToEndSessions(deviceKey, txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndSessions(deviceKey, txn, func);
});
this._backend.getEndToEndSessions(deviceKey, txn, func);
}
/**
@@ -389,9 +388,7 @@ export class IndexedDBCryptoStore {
* and session keys.
*/
getAllEndToEndSessions(txn, func) {
this._backendPromise.then(backend => {
backend.getAllEndToEndSessions(txn, func);
});
this._backend.getAllEndToEndSessions(txn, func);
}
/**
@@ -402,29 +399,21 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
this._backendPromise.then(backend => {
backend.storeEndToEndSession(
deviceKey, sessionId, sessionInfo, txn,
);
});
this._backend.storeEndToEndSession(
deviceKey, sessionId, sessionInfo, txn,
);
}
storeEndToEndSessionProblem(deviceKey, type, fixed) {
return this._backendPromise.then(async (backend) => {
await backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
});
return this._backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
}
getEndToEndSessionProblem(deviceKey, timestamp) {
return this._backendPromise.then(async (backend) => {
return await backend.getEndToEndSessionProblem(deviceKey, timestamp);
});
return this._backend.getEndToEndSessionProblem(deviceKey, timestamp);
}
filterOutNotifiedErrorDevices(devices) {
return this._backendPromise.then(async (backend) => {
return await backend.filterOutNotifiedErrorDevices(devices);
});
return this._backend.filterOutNotifiedErrorDevices(devices);
}
// Inbound group sessions
@@ -439,11 +428,9 @@ export class IndexedDBCryptoStore {
* to Base64 end-to-end session.
*/
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, txn, func,
);
});
this._backend.getEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, txn, func,
);
}
/**
@@ -454,9 +441,7 @@ export class IndexedDBCryptoStore {
* sessionData}, then once with null to indicate the end of the list.
*/
getAllEndToEndInboundGroupSessions(txn, func) {
this._backendPromise.then(backend => {
backend.getAllEndToEndInboundGroupSessions(txn, func);
});
this._backend.getAllEndToEndInboundGroupSessions(txn, func);
}
/**
@@ -469,11 +454,9 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backendPromise.then(backend => {
backend.addEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
});
this._backend.addEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
/**
@@ -486,21 +469,17 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backendPromise.then(backend => {
backend.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
});
this._backend.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
this._backendPromise.then(backend => {
backend.storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
);
});
this._backend.storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
// End-to-end device tracking
@@ -516,9 +495,7 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndDeviceData(deviceData, txn) {
this._backendPromise.then(backend => {
backend.storeEndToEndDeviceData(deviceData, txn);
});
this._backend.storeEndToEndDeviceData(deviceData, txn);
}
/**
@@ -529,9 +506,7 @@ export class IndexedDBCryptoStore {
* device data
*/
getEndToEndDeviceData(txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndDeviceData(txn, func);
});
this._backend.getEndToEndDeviceData(txn, func);
}
// End to End Rooms
@@ -543,9 +518,7 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndRoom(roomId, roomInfo, txn) {
this._backendPromise.then(backend => {
backend.storeEndToEndRoom(roomId, roomInfo, txn);
});
this._backend.storeEndToEndRoom(roomId, roomInfo, txn);
}
/**
@@ -554,9 +527,7 @@ export class IndexedDBCryptoStore {
* @param {function(Object)} func Function called with the end to end encrypted rooms
*/
getEndToEndRooms(txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndRooms(txn, func);
});
this._backend.getEndToEndRooms(txn, func);
}
// session backups
@@ -568,9 +539,7 @@ export class IndexedDBCryptoStore {
* @returns {Promise} resolves to an array of inbound group sessions
*/
getSessionsNeedingBackup(limit) {
return this._connect().then((backend) => {
return backend.getSessionsNeedingBackup(limit);
});
return this._backend.getSessionsNeedingBackup(limit);
}
/**
@@ -579,9 +548,7 @@ export class IndexedDBCryptoStore {
* @returns {Promise} resolves to the number of sessions
*/
countSessionsNeedingBackup(txn) {
return this._connect().then((backend) => {
return backend.countSessionsNeedingBackup(txn);
});
return this._backend.countSessionsNeedingBackup(txn);
}
/**
@@ -591,9 +558,7 @@ export class IndexedDBCryptoStore {
* @returns {Promise} resolves when the sessions are unmarked
*/
unmarkSessionsNeedingBackup(sessions, txn) {
return this._connect().then((backend) => {
return backend.unmarkSessionsNeedingBackup(sessions, txn);
});
return this._backend.unmarkSessionsNeedingBackup(sessions, txn);
}
/**
@@ -603,9 +568,7 @@ export class IndexedDBCryptoStore {
* @returns {Promise} resolves when the sessions are marked
*/
markSessionsNeedingBackup(sessions, txn) {
return this._connect().then((backend) => {
return backend.markSessionsNeedingBackup(sessions, txn);
});
return this._backend.markSessionsNeedingBackup(sessions, txn);
}
/**
@@ -630,9 +593,7 @@ export class IndexedDBCryptoStore {
* exception will propagate to the caller of the getFoo method.
*/
doTxn(mode, stores, func) {
return this._connect().then((backend) => {
return backend.doTxn(mode, stores, func);
});
return this._backend.doTxn(mode, stores, func);
}
}
@@ -367,12 +367,23 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
func(keys);
}
getSecretStorePrivateKey(txn, func, type) {
const key = getJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`);
func(key ? Uint8Array.from(key) : key);
}
storeCrossSigningKeys(txn, keys) {
setJsonItem(
this.store, KEY_CROSS_SIGNING_KEYS, keys,
);
}
storeSecretStorePrivateKey(txn, type, key) {
setJsonItem(
this.store, E2E_PREFIX + `ssss_cache.${type}`, Array.from(key),
);
}
doTxn(mode, stores, func) {
return Promise.resolve(func(null));
}
+32 -5
View File
@@ -33,6 +33,8 @@ export class MemoryCryptoStore {
this._outgoingRoomKeyRequests = [];
this._account = null;
this._crossSigningKeys = null;
this._privateKeys = {};
this._backupKeys = {};
// Map of {devicekey -> {sessionId -> session pickle}}
this._sessions = {};
@@ -51,6 +53,18 @@ export class MemoryCryptoStore {
this._sessionsNeedingBackup = {};
}
/**
* Ensure the database exists and is up-to-date.
*
* This must be called before the store can be used.
*
* @return {Promise} resolves to the store.
*/
async startup() {
// No startup work to do for the memory store.
return this;
}
/**
* Delete all data from this store.
*
@@ -243,10 +257,19 @@ export class MemoryCryptoStore {
func(this._crossSigningKeys);
}
getSecretStorePrivateKey(txn, func, type) {
const result = this._privateKeys[type];
return func(result || null);
}
storeCrossSigningKeys(txn, keys) {
this._crossSigningKeys = keys;
}
storeSecretStorePrivateKey(txn, type, key) {
this._privateKeys[type] = key;
}
// Olm Sessions
countEndToEndSessions(txn, func) {
@@ -263,11 +286,15 @@ export class MemoryCryptoStore {
}
getAllEndToEndSessions(txn, func) {
for (const deviceSessions of Object.values(this._sessions)) {
for (const sess of Object.values(deviceSessions)) {
func(sess);
}
}
Object.entries(this._sessions).forEach(([deviceKey, deviceSessions]) => {
Object.entries(deviceSessions).forEach(([sessionId, session]) => {
func({
...session,
deviceKey,
sessionId,
});
});
});
}
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
+118 -8
View File
@@ -24,9 +24,18 @@ import {EventEmitter} from 'events';
import {logger} from '../../logger';
import {DeviceInfo} from '../deviceinfo';
import {newTimeoutError} from "./Error";
import {CrossSigningInfo} from "../CrossSigning";
import {decodeBase64} from "../olmlib";
const timeoutException = new Error("Verification timed out");
export class SwitchStartEventError extends Error {
constructor(startEvent) {
super();
this.startEvent = startEvent;
}
}
export class VerificationBase extends EventEmitter {
/**
* Base class for verification methods.
@@ -67,9 +76,21 @@ export class VerificationBase extends EventEmitter {
this._done = false;
this._promise = null;
this._transactionTimeoutTimer = null;
}
// At this point, the verification request was received so start the timeout timer.
this._resetTimer();
static keyRequestTimeoutMs = 1000 * 60;
get initiatedByMe() {
// if there is no start event yet,
// we probably want to send it,
// which happens if we initiate
if (!this.startEvent) {
return true;
}
const sender = this.startEvent.getSender();
const content = this.startEvent.getContent();
return sender === this._baseApis.getUserId() &&
content.from_device === this._baseApis.getDeviceId();
}
_resetTimer() {
@@ -107,6 +128,24 @@ export class VerificationBase extends EventEmitter {
});
}
canSwitchStartEvent() {
return false;
}
switchStartEvent(event) {
if (this.canSwitchStartEvent(event)) {
logger.log("Verification Base: switching verification start event",
{restartingFlow: !!this._rejectEvent});
if (this._rejectEvent) {
const reject = this._rejectEvent;
this._rejectEvent = undefined;
reject(new SwitchStartEventError(event));
} else {
this.startEvent = event;
}
}
}
handleEvent(e) {
if (this._done) {
return;
@@ -122,8 +161,18 @@ export class VerificationBase extends EventEmitter {
} else if (e.getType() === "m.key.verification.cancel") {
const reject = this._reject;
this._reject = undefined;
reject(new Error("Other side cancelled verification"));
} else {
// there is only promise to reject if verify has been called
if (reject) {
const content = e.getContent();
const {reason, code} = content;
reject(new Error(`Other side cancelled verification ` +
`because ${reason} (${code})`));
}
} else if (this._expectedEvent) {
// only cancel if there is an event expected.
// if there is no event expected, it means verify() wasn't called
// and we're just replaying the timeline events when syncing
// after a refresh when the events haven't been stored in the cache yet.
const exception = new Error(
"Unexpected message: expecting " + this._expectedEvent
+ " but got " + e.getType(),
@@ -141,11 +190,65 @@ export class VerificationBase extends EventEmitter {
done() {
this._endTimer(); // always kill the activity timer
if (!this._done) {
if (this._channel.needsDoneMessage) {
// verification in DM requires a done message
this._send("m.key.verification.done", {});
}
this.request.onVerifierFinished();
this._resolve();
//#region Cross-signing keys request
// If this is a self-verification, ask the other party for keys
if (this._baseApis.getUserId() !== this.userId) {
return;
}
console.log("VerificationBase.done: Self-verification done; requesting keys");
/* This happens asynchronously, and we're not concerned about
* waiting for it. We return here in order to test. */
return new Promise((resolve, reject) => {
const client = this._baseApis;
const original = client._crypto._crossSigningInfo;
const storage = client._crypto._secretStorage;
/* We already have all of the infrastructure we need to validate and
* cache cross-signing keys, so instead of replicating that, here we
* set up callbacks that request them from the other device and call
* CrossSigningInfo.getCrossSigningKey() to validate/cache */
const crossSigning = new CrossSigningInfo(
original.userId,
{ getCrossSigningKey: async (type) => {
console.debug("VerificationBase.done: requesting secret",
type, this.deviceId);
const { promise } =
storage.request(`m.cross_signing.${type}`, [this.deviceId]);
const result = await promise;
const decoded = decodeBase64(result);
return Uint8Array.from(decoded);
} },
original._cacheCallbacks,
);
crossSigning.keys = original.keys;
// XXX: get all keys out if we get one key out
// https://github.com/vector-im/riot-web/issues/12604
// then change here to reject on the timeout
/* Requests can be ignored, so don't wait around forever */
const timeout = new Promise((resolve, reject) => {
setTimeout(
resolve,
VerificationBase.keyRequestTimeoutMs,
new Error("Timeout"),
);
});
/* We call getCrossSigningKey() for its side-effects */
return Promise.race([
Promise.all([
crossSigning.getCrossSigningKey("self_signing"),
crossSigning.getCrossSigningKey("user_signing"),
]),
timeout,
]).then(resolve, reject);
}).catch((e) => {
console.warn("VerificationBase: failure while requesting keys:", e);
});
//#endregion
}
}
@@ -263,6 +366,13 @@ export class VerificationBase extends EventEmitter {
throw new Error("No devices could be verified");
}
logger.info(
"Verification completed! Marking devices verified: ",
verifiedDevices,
);
// TODO: There should probably be a batch version of this, otherwise it's going
// to upload each signature in a separate API call which is silly because the
// API supports as many signatures as you like.
for (const deviceId of verifiedDevices) {
await this._baseApis.setDeviceVerified(userId, deviceId);
}
+2 -4
View File
@@ -23,12 +23,10 @@ limitations under the License.
import {MatrixEvent} from "../../models/event";
export function newVerificationError(code, reason, extradata) {
extradata = extradata || {};
extradata.code = code;
extradata.reason = reason;
const content = Object.assign({}, {code, reason}, extradata);
return new MatrixEvent({
type: "m.key.verification.cancel",
content: extradata,
content,
});
}
+43
View File
@@ -0,0 +1,43 @@
/*
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.
*/
/**
* Verification method that is illegal to have (cannot possibly
* do verification with this method).
* @module crypto/verification/IllegalMethod
*/
import {VerificationBase as Base} from "./Base";
/**
* @class crypto/verification/IllegalMethod/IllegalMethod
* @extends {module:crypto/verification/Base}
*/
export class IllegalMethod extends Base {
static factory(...args) {
return new IllegalMethod(...args);
}
static get NAME() {
// Typically the name will be something else, but to complete
// the contract we offer a default one here.
return "org.matrix.illegal_method";
}
async _doVerification() {
throw new Error("Verification is not possible with this method");
}
}
+61 -71
View File
@@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
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.
@@ -21,103 +22,92 @@ limitations under the License.
import {VerificationBase as Base} from "./Base";
import {
errorFactory,
newKeyMismatchError,
newUserCancelledError,
newUserMismatchError,
} from './Error';
const MATRIXTO_REGEXP = /^(?:https?:\/\/)?(?:www\.)?matrix\.to\/#\/([#@!+][^?]+)\?(.+)$/;
const KEY_REGEXP = /^key_([^:]+:.+)$/;
const newQRCodeError = errorFactory("m.qr_code.invalid", "Invalid QR code");
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
/**
* @class crypto/verification/QRCode/ShowQRCode
* @class crypto/verification/QRCode/ReciprocateQRCode
* @extends {module:crypto/verification/Base}
*/
export class ShowQRCode extends Base {
_doVerification() {
if (!this._done) {
const url = "https://matrix.to/#/" + this._baseApis.getUserId()
+ "?device=" + encodeURIComponent(this._baseApis.deviceId)
+ "&action=verify&key_ed25519%3A"
+ encodeURIComponent(this._baseApis.deviceId) + "="
+ encodeURIComponent(this._baseApis.getDeviceEd25519Key());
this.emit("show_qr_code", {
url: url,
});
}
}
}
ShowQRCode.NAME = "m.qr_code.show.v1";
/**
* @class crypto/verification/QRCode/ScanQRCode
* @extends {module:crypto/verification/Base}
*/
export class ScanQRCode extends Base {
export class ReciprocateQRCode extends Base {
static factory(...args) {
return new ScanQRCode(...args);
return new ReciprocateQRCode(...args);
}
static get NAME() {
return "m.reciprocate.v1";
}
async _doVerification() {
const code = await new Promise((resolve, reject) => {
this.emit("scan", {
done: resolve,
cancel: () => reject(newUserCancelledError()),
});
});
const match = code.match(MATRIXTO_REGEXP);
let deviceId;
const keys = {};
if (!match) {
throw newQRCodeError();
}
const userId = match[1];
const params = match[2].split("&").map(
(x) => x.split("=", 2).map(decodeURIComponent),
);
let action;
for (const [name, value] of params) {
if (name === "device") {
deviceId = value;
} else if (name === "action") {
action = value;
} else {
const keyMatch = name.match(KEY_REGEXP);
if (keyMatch) {
keys[keyMatch[1]] = value;
}
}
}
if (!deviceId || action !== "verify" || Object.keys(keys).length === 0) {
throw newQRCodeError();
if (!this.startEvent) {
// TODO: Support scanning QR codes
throw new Error("It is not currently possible to start verification" +
"with this method yet.");
}
const targetUserId = this.startEvent.getSender();
if (!this.userId) {
await new Promise((resolve, reject) => {
console.log("Asking to confirm user ID");
this.userId = await new Promise((resolve, reject) => {
this.emit("confirm_user_id", {
userId: userId,
confirm: resolve,
userId: targetUserId,
confirm: resolve, // takes a userId
cancel: () => reject(newUserMismatchError()),
});
});
} else if (this.userId !== userId) {
} else if (targetUserId !== this.userId) {
throw newUserMismatchError({
expected: this.userId,
actual: userId,
actual: targetUserId,
});
}
await this._verifyKeys(userId, keys, (keyId, device, key) => {
if (device.keys[keyId] !== key) {
if (this.startEvent.getContent()['secret'] !== this.request.encodedSharedSecret) {
throw newKeyMismatchError();
}
// If we've gotten this far, verify the user's master cross signing key
const xsignInfo = this._baseApis.getStoredCrossSigningForUser(this.userId);
if (!xsignInfo) throw new Error("Missing cross signing info");
const masterKey = xsignInfo.getId("master");
const masterKeyId = `ed25519:${masterKey}`;
const keys = {[masterKeyId]: masterKey};
const devices = (await this._baseApis.getStoredDevicesForUser(this.userId)) || [];
const targetDevice = devices.find(d => {
return d.deviceId === this.request.targetDevice.deviceId;
});
if (!targetDevice) throw new Error("Device not found, somehow");
keys[`ed25519:${targetDevice.deviceId}`] = targetDevice.getFingerprint();
if (this.request.requestingUserId === this.request.receivingUserId) {
delete keys[masterKeyId];
}
await this._verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
const targetKey = keys[keyId];
if (!targetKey) throw newKeyMismatchError();
if (keyInfo !== targetKey) {
console.error("key ID from key info does not match");
throw newKeyMismatchError();
}
for (const deviceKeyId in device.keys) {
if (!deviceKeyId.startsWith("ed25519")) continue;
const deviceTargetKey = keys[deviceKeyId];
if (!deviceTargetKey) throw newKeyMismatchError();
if (device.keys[deviceKeyId] !== deviceTargetKey) {
console.error("master key does not match");
throw newKeyMismatchError();
}
}
// Otherwise it is probably fine
});
}
}
ScanQRCode.NAME = "m.qr_code.scan.v1";
+80 -23
View File
@@ -19,7 +19,7 @@ limitations under the License.
* @module crypto/verification/SAS
*/
import {VerificationBase as Base} from "./Base";
import {VerificationBase as Base, SwitchStartEventError} from "./Base";
import anotherjson from 'another-json';
import {
errorFactory,
@@ -28,6 +28,9 @@ import {
newUnknownMethodError,
newUserCancelledError,
} from './Error';
import {logger} from '../../logger';
const START_TYPE = "m.key.verification.start";
const EVENTS = [
"m.key.verification.accept",
@@ -163,6 +166,15 @@ const macMethods = {
"hmac-sha256": "calculate_mac_long_kdf",
};
function calculateMAC(olmSAS, method) {
return function(...args) {
const macFunction = olmSAS[macMethods[method]];
const mac = macFunction.apply(olmSAS, args);
logger.log("SAS calculateMAC:", method, args, mac);
return mac;
};
}
/* lists of algorithms/methods that are supported. The key agreement, hashes,
* and MAC lists should be sorted in order of preference (most preferred
* first).
@@ -201,16 +213,37 @@ export class SAS extends Base {
// make sure user's keys are downloaded
await this._baseApis.downloadKeys([this.userId]);
if (this.startEvent) {
return await this._doRespondVerification();
} else {
return await this._doSendVerification();
}
let retry = false;
do {
try {
if (this.initiatedByMe) {
return await this._doSendVerification();
} else {
return await this._doRespondVerification();
}
} catch (err) {
if (err instanceof SwitchStartEventError) {
// this changes what initiatedByMe returns
this.startEvent = err.startEvent;
retry = true;
} else {
throw err;
}
}
} while (retry);
}
async _doSendVerification() {
const type = "m.key.verification.start";
const initialMessage = this._channel.completeContent(type, {
canSwitchStartEvent(event) {
if (event.getType() !== START_TYPE) {
return false;
}
const content = event.getContent();
return content && content.method === SAS.NAME &&
this._waitingForAccept;
}
async _sendStart() {
const startContent = this._channel.completeContent(START_TYPE, {
method: SAS.NAME,
from_device: this._baseApis.deviceId,
key_agreement_protocols: KEY_AGREEMENT_LIST,
@@ -219,11 +252,33 @@ export class SAS extends Base {
// FIXME: allow app to specify what SAS methods can be used
short_authentication_string: SAS_LIST,
});
// add the transaction id to the message beforehand because
// it needs to be included in the commitment hash later on
this._channel.sendCompleted(type, initialMessage);
await this._channel.sendCompleted(START_TYPE, startContent);
return startContent;
}
let e = await this._waitForEvent("m.key.verification.accept");
async _doSendVerification() {
this._waitingForAccept = true;
let startContent;
if (this.startEvent) {
startContent = this._channel.completedContentFromEvent(this.startEvent);
} else {
startContent = await this._sendStart();
}
// we might have switched to a different start event,
// but was we didn't call _waitForEvent there was no
// call that could throw yet. So check manually that
// we're still on the initiator side
if (!this.initiatedByMe) {
throw new SwitchStartEventError(this.startEvent);
}
let e;
try {
e = await this._waitForEvent("m.key.verification.accept");
} finally {
this._waitingForAccept = false;
}
let content = e.getContent();
const sasMethods
= intersection(content.short_authentication_string, SAS_SET);
@@ -248,7 +303,7 @@ export class SAS extends Base {
e = await this._waitForEvent("m.key.verification.key");
// FIXME: make sure event is properly formed
content = e.getContent();
const commitmentStr = content.key + anotherjson.stringify(initialMessage);
const commitmentStr = content.key + anotherjson.stringify(startContent);
// TODO: use selected hash function (when we support multiple)
if (olmutil.sha256(commitmentStr) !== hashCommitment) {
throw newMismatchedCommitmentError();
@@ -261,7 +316,7 @@ export class SAS extends Base {
+ this._channel.transactionId;
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
const verifySAS = new Promise((resolve, reject) => {
this.emit("show_sas", {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS, macMethod);
@@ -269,7 +324,8 @@ export class SAS extends Base {
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
});
};
this.emit("show_sas", this.sasEvent);
});
@@ -345,7 +401,7 @@ export class SAS extends Base {
+ this._channel.transactionId;
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
const verifySAS = new Promise((resolve, reject) => {
this.emit("show_sas", {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS, macMethod);
@@ -353,7 +409,8 @@ export class SAS extends Base {
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
});
};
this.emit("show_sas", this.sasEvent);
});
@@ -384,7 +441,7 @@ export class SAS extends Base {
+ this._channel.transactionId;
const deviceKeyId = `ed25519:${this._baseApis.deviceId}`;
mac[deviceKeyId] = olmSAS[macMethods[method]](
mac[deviceKeyId] = calculateMAC(olmSAS, method)(
this._baseApis.getDeviceEd25519Key(),
baseInfo + deviceKeyId,
);
@@ -393,14 +450,14 @@ export class SAS extends Base {
const crossSigningId = this._baseApis.getCrossSigningId();
if (crossSigningId) {
const crossSigningKeyId = `ed25519:${crossSigningId}`;
mac[crossSigningKeyId] = olmSAS[macMethods[method]](
mac[crossSigningKeyId] = calculateMAC(olmSAS, method)(
crossSigningId,
baseInfo + crossSigningKeyId,
);
keyList.push(crossSigningKeyId);
}
const keys = olmSAS[macMethods[method]](
const keys = calculateMAC(olmSAS, method)(
keyList.sort().join(","),
baseInfo + "KEY_IDS",
);
@@ -413,7 +470,7 @@ export class SAS extends Base {
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this._channel.transactionId;
if (content.keys !== olmSAS[macMethods[method]](
if (content.keys !== calculateMAC(olmSAS, method)(
Object.keys(content.mac).sort().join(","),
baseInfo + "KEY_IDS",
)) {
@@ -421,7 +478,7 @@ export class SAS extends Base {
}
await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
if (keyInfo !== olmSAS[macMethods[method]](
if (keyInfo !== calculateMAC(olmSAS, method)(
device.keys[keyId],
baseInfo + keyId,
)) {
+148 -18
View File
@@ -15,7 +15,13 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {REQUEST_TYPE, START_TYPE, VerificationRequest} from "./VerificationRequest";
import {
VerificationRequest,
REQUEST_TYPE,
READY_TYPE,
START_TYPE,
} from "./VerificationRequest";
import {logger} from '../../../logger';
const MESSAGE_TYPE = "m.room.message";
const M_REFERENCE = "m.reference";
@@ -31,10 +37,10 @@ export class InRoomChannel {
* @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user.
* @param {string} userId id of user that the verification request is directed at, should be present in the room.
*/
constructor(client, roomId, userId) {
constructor(client, roomId, userId = null) {
this._client = client;
this._roomId = roomId;
this._userId = userId;
this.userId = userId;
this._requestEventId = null;
}
@@ -43,16 +49,41 @@ export class InRoomChannel {
return true;
}
get receiveStartFromOtherDevices() {
return true;
}
get roomId() {
return this._roomId;
}
/** The transaction id generated/used by this verification channel */
get transactionId() {
return this._requestEventId;
}
static getOtherPartyUserId(event, client) {
const type = InRoomChannel.getEventType(event);
if (type !== REQUEST_TYPE) {
return;
}
const ownUserId = client.getUserId();
const sender = event.getSender();
const content = event.getContent();
const receiver = content.to;
if (sender === ownUserId) {
return receiver;
} else if (receiver === ownUserId) {
return sender;
}
}
/**
* @param {MatrixEvent} event the event to get the timestamp of
* @return {number} the timestamp when the event was sent
*/
static getTimestamp(event) {
getTimestamp(event) {
return event.getTs();
}
@@ -97,19 +128,26 @@ export class InRoomChannel {
}
const type = InRoomChannel.getEventType(event);
const content = event.getContent();
// from here on we're fairly sure that this is supposed to be
// part of a verification request, so be noisy when rejecting something
if (type === REQUEST_TYPE) {
if (typeof content.to !== "string" || !content.to.length) {
if (!content || typeof content.to !== "string" || !content.to.length) {
logger.log("InRoomChannel: validateEvent: " +
"no valid to " + (content && content.to));
return false;
}
const ownUserId = client.getUserId();
// ignore requests that are not direct to or sent by the syncing user
if (event.getSender() !== ownUserId && content.to !== ownUserId) {
if (!InRoomChannel.getOtherPartyUserId(event, client)) {
logger.log("InRoomChannel: validateEvent: " +
`not directed to or sent by me: ${event.getSender()}` +
`, ${content && content.to}`);
return false;
}
}
return VerificationRequest.validateEvent(
type, event, InRoomChannel.getTimestamp(event), client);
return VerificationRequest.validateEvent(type, event, client);
}
/**
@@ -130,28 +168,54 @@ export class InRoomChannel {
}
}
}
return type;
if (type && type !== REQUEST_TYPE) {
return type;
} else {
return "";
}
}
/**
* Changes the state of the channel, request, and verifier in response to a key verification event.
* @param {MatrixEvent} event to handle
* @param {VerificationRequest} request the request to forward handling to
* @param {bool} isLiveEvent whether this is an even received through sync or not
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
*/
async handleEvent(event, request) {
async handleEvent(event, request, isLiveEvent) {
const type = InRoomChannel.getEventType(event);
// do validations that need state (roomId, userId),
// ignore if invalid
if (event.getRoomId() !== this._roomId || event.getSender() !== this._userId) {
if (event.getRoomId() !== this._roomId) {
return;
}
// set transactionId when receiving a .request
if (!this._requestEventId && type === REQUEST_TYPE) {
this._requestEventId = event.getId();
// set userId if not set already
if (this.userId === null) {
const userId = InRoomChannel.getOtherPartyUserId(event, this._client);
if (userId) {
this.userId = userId;
}
}
// ignore events not sent by us or the other party
const ownUserId = this._client.getUserId();
const sender = event.getSender();
if (this.userId !== null) {
if (sender !== ownUserId && sender !== this.userId) {
logger.log(`InRoomChannel: ignoring verification event from ` +
`non-participating sender ${sender}`);
return;
}
}
if (this._requestEventId === null) {
this._requestEventId = InRoomChannel.getTransactionId(event);
}
return await request.handleEvent(type, event, InRoomChannel.getTimestamp(event));
const isRemoteEcho = !!event.getUnsigned().transaction_id;
const isSentByUs = event.getSender() === this._client.getUserId();
return await request.handleEvent(
type, event, isLiveEvent, isRemoteEcho, isSentByUs);
}
/**
@@ -180,7 +244,7 @@ export class InRoomChannel {
*/
completeContent(type, content) {
content = Object.assign({}, content);
if (type === REQUEST_TYPE || type === START_TYPE) {
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
content.from_device = this._client.getDeviceId();
}
if (type === REQUEST_TYPE) {
@@ -191,7 +255,7 @@ export class InRoomChannel {
"verification. You will need to use legacy key " +
"verification to verify keys.",
msgtype: REQUEST_TYPE,
to: this._userId,
to: this.userId,
from_device: content.from_device,
methods: content.methods,
};
@@ -232,3 +296,69 @@ export class InRoomChannel {
}
}
}
export class InRoomRequests {
constructor() {
this._requestsByRoomId = new Map();
}
getRequest(event) {
const roomId = event.getRoomId();
const txnId = InRoomChannel.getTransactionId(event);
return this._getRequestByTxnId(roomId, txnId);
}
getRequestByChannel(channel) {
return this._getRequestByTxnId(channel.roomId, channel.transactionId);
}
_getRequestByTxnId(roomId, txnId) {
const requestsByTxnId = this._requestsByRoomId.get(roomId);
if (requestsByTxnId) {
return requestsByTxnId.get(txnId);
}
}
setRequest(event, request) {
this._setRequest(
event.getRoomId(),
InRoomChannel.getTransactionId(event),
request,
);
}
setRequestByChannel(channel, request) {
this._setRequest(channel.roomId, channel.transactionId, request);
}
_setRequest(roomId, txnId, request) {
let requestsByTxnId = this._requestsByRoomId.get(roomId);
if (!requestsByTxnId) {
requestsByTxnId = new Map();
this._requestsByRoomId.set(roomId, requestsByTxnId);
}
requestsByTxnId.set(txnId, request);
}
removeRequest(event) {
const roomId = event.getRoomId();
const requestsByTxnId = this._requestsByRoomId.get(roomId);
if (requestsByTxnId) {
requestsByTxnId.delete(InRoomChannel.getTransactionId(event));
if (requestsByTxnId.size === 0) {
this._requestsByRoomId.delete(roomId);
}
}
}
findRequestInProgress(roomId) {
const requestsByTxnId = this._requestsByRoomId.get(roomId);
if (requestsByTxnId) {
for (const request of requestsByTxnId.values()) {
if (request.pending) {
return request;
}
}
}
}
}
@@ -1,59 +0,0 @@
/*
Copyright 2019 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.
*/
/** a key verification channel that wraps over an actual channel to pass it to a verifier,
* to notify the VerificationRequest when the verifier tries to send anything over the channel.
* This way, the VerificationRequest can update its state based on events sent by the verifier.
* Anything that is not sending is just routing through to the wrapped channel.
*/
export class RequestCallbackChannel {
constructor(request, channel) {
this._request = request;
this._channel = channel;
}
get transactionId() {
return this._channel.transactionId;
}
get needsDoneMessage() {
return this._channel.needsDoneMessage;
}
handleEvent(event, request) {
return this._channel.handleEvent(event, request);
}
completedContentFromEvent(event) {
return this._channel.completedContentFromEvent(event);
}
completeContent(type, content) {
return this._channel.completeContent(type, content);
}
async send(type, uncompletedContent) {
this._request.handleVerifierSend(type, uncompletedContent);
const result = await this._channel.send(type, uncompletedContent);
return result;
}
async sendCompleted(type, content) {
this._request.handleVerifierSend(type, content);
const result = await this._channel.sendCompleted(type, content);
return result;
}
}
@@ -20,11 +20,14 @@ import {logger} from '../../../logger';
import {
CANCEL_TYPE,
PHASE_STARTED,
PHASE_READY,
REQUEST_TYPE,
READY_TYPE,
START_TYPE,
VerificationRequest,
} from "./VerificationRequest";
import {errorFromEvent, newUnexpectedMessageError} from "../Error";
import {MatrixEvent} from "../../../models/event";
/**
* A key verification channel that sends verification events over to_device messages.
@@ -34,12 +37,34 @@ export class ToDeviceChannel {
// userId and devices of user we're about to verify
constructor(client, userId, devices, transactionId = null, deviceId = null) {
this._client = client;
this._userId = userId;
this.userId = userId;
this._devices = devices;
this.transactionId = transactionId;
this._deviceId = deviceId;
}
isToDevices(devices) {
if (devices.length === this._devices.length) {
for (const device of devices) {
const d = this._devices.find(d => d.deviceId === device.deviceId);
if (!d) {
return false;
}
}
return true;
} else {
return false;
}
}
get deviceId() {
return this._deviceId;
}
get needsDoneMessage() {
return false;
}
static getEventType(event) {
return event.getType();
}
@@ -80,10 +105,12 @@ export class ToDeviceChannel {
}
const content = event.getContent();
if (!content) {
logger.warn("ToDeviceChannel.validateEvent: invalid: no content");
return false;
}
if (!content.transaction_id) {
logger.warn("ToDeviceChannel.validateEvent: invalid: no transaction_id");
return false;
}
@@ -91,6 +118,7 @@ export class ToDeviceChannel {
if (type === REQUEST_TYPE) {
if (!Number.isFinite(content.timestamp)) {
logger.warn("ToDeviceChannel.validateEvent: invalid: no timestamp");
return false;
}
if (event.getSender() === client.getUserId() &&
@@ -98,19 +126,19 @@ export class ToDeviceChannel {
) {
// ignore requests from ourselves, because it doesn't make sense for a
// device to verify itself
logger.warn("ToDeviceChannel.validateEvent: invalid: from own device");
return false;
}
}
return VerificationRequest.validateEvent(
type, event, ToDeviceChannel.getTimestamp(event), client);
return VerificationRequest.validateEvent(type, event, client);
}
/**
* @param {MatrixEvent} event the event to get the timestamp of
* @return {number} the timestamp when the event was sent
*/
static getTimestamp(event) {
getTimestamp(event) {
const content = event.getContent();
return content && content.timestamp;
}
@@ -119,12 +147,13 @@ export class ToDeviceChannel {
* Changes the state of the channel, request, and verifier in response to a key verification event.
* @param {MatrixEvent} event to handle
* @param {VerificationRequest} request the request to forward handling to
* @param {bool} isLiveEvent whether this is an even received through sync or not
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
*/
async handleEvent(event, request) {
async handleEvent(event, request, isLiveEvent) {
const type = event.getType();
const content = event.getContent();
if (type === REQUEST_TYPE || type === START_TYPE) {
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
if (!this.transactionId) {
this.transactionId = content.transaction_id;
}
@@ -143,14 +172,17 @@ export class ToDeviceChannel {
return this._sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
}
}
const wasStarted = request.phase === PHASE_STARTED ||
request.phase === PHASE_READY;
const wasStarted = request.phase === PHASE_STARTED;
await request.handleEvent(
event.getType(), event, ToDeviceChannel.getTimestamp(event));
const isStarted = request.phase === PHASE_STARTED;
await request.handleEvent(event.getType(), event, isLiveEvent, false, false);
// the request has picked a start event, tell the other devices about it
if (type === START_TYPE && !wasStarted && isStarted && this._deviceId) {
const isStarted = request.phase === PHASE_STARTED ||
request.phase === PHASE_READY;
const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
// the request has picked a ready or start event, tell the other devices about it
if (isAcceptingEvent && !wasStarted && isStarted && this._deviceId) {
const nonChosenDevices = this._devices.filter(d => d !== this._deviceId);
if (nonChosenDevices.length) {
const message = this.completeContent({
@@ -186,7 +218,7 @@ export class ToDeviceChannel {
if (this.transactionId) {
content.transaction_id = this.transactionId;
}
if (type === REQUEST_TYPE || type === START_TYPE) {
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
content.from_device = this._client.getDeviceId();
}
if (type === REQUEST_TYPE) {
@@ -216,12 +248,28 @@ export class ToDeviceChannel {
* @param {object} content
* @returns {Promise} the promise of the request
*/
sendCompleted(type, content) {
async sendCompleted(type, content) {
let result;
if (type === REQUEST_TYPE) {
return this._sendToDevices(type, content, this._devices);
result = await this._sendToDevices(type, content, this._devices);
} else {
return this._sendToDevices(type, content, [this._deviceId]);
result = await this._sendToDevices(type, content, [this._deviceId]);
}
// the VerificationRequest state machine requires remote echos of the event
// the client sends itself, so we fake this for to_device messages
const remoteEchoEvent = new MatrixEvent({
sender: this._client.getUserId(),
content,
type,
});
await this._request.handleEvent(
type,
remoteEchoEvent,
/*isLiveEvent=*/true,
/*isRemoteEcho=*/true,
/*isSentByUs=*/true,
);
return result;
}
_sendToDevices(type, content, devices) {
@@ -231,7 +279,7 @@ export class ToDeviceChannel {
msgMap[deviceId] = content;
}
return this._client.sendToDevice(type, {[this._userId]: msgMap});
return this._client.sendToDevice(type, {[this.userId]: msgMap});
} else {
return Promise.resolve();
}
@@ -245,3 +293,71 @@ export class ToDeviceChannel {
return randomString(32);
}
}
export class ToDeviceRequests {
constructor() {
this._requestsByUserId = new Map();
}
getRequest(event) {
return this.getRequestBySenderAndTxnId(
event.getSender(),
ToDeviceChannel.getTransactionId(event),
);
}
getRequestByChannel(channel) {
return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId);
}
getRequestBySenderAndTxnId(sender, txnId) {
const requestsByTxnId = this._requestsByUserId.get(sender);
if (requestsByTxnId) {
return requestsByTxnId.get(txnId);
}
}
setRequest(event, request) {
this.setRequestBySenderAndTxnId(
event.getSender(),
ToDeviceChannel.getTransactionId(event),
request,
);
}
setRequestByChannel(channel, request) {
this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request);
}
setRequestBySenderAndTxnId(sender, txnId, request) {
let requestsByTxnId = this._requestsByUserId.get(sender);
if (!requestsByTxnId) {
requestsByTxnId = new Map();
this._requestsByUserId.set(sender, requestsByTxnId);
}
requestsByTxnId.set(txnId, request);
}
removeRequest(event) {
const userId = event.getSender();
const requestsByTxnId = this._requestsByUserId.get(userId);
if (requestsByTxnId) {
requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event));
if (requestsByTxnId.size === 0) {
this._requestsByUserId.delete(userId);
}
}
}
findRequestInProgress(userId, devices) {
const requestsByTxnId = this._requestsByUserId.get(userId);
if (requestsByTxnId) {
for (const request of requestsByTxnId.values()) {
if (request.pending && request.channel.isToDevices(devices)) {
return request;
}
}
}
}
}
@@ -16,7 +16,6 @@ limitations under the License.
*/
import {logger} from '../../../logger';
import {RequestCallbackChannel} from "./RequestCallbackChannel";
import {EventEmitter} from 'events';
import {
errorFactory,
@@ -24,6 +23,7 @@ import {
newUnexpectedMessageError,
newUnknownMethodError,
} from "../Error";
import * as olmlib from "../../olmlib";
// the recommended amount of time before a verification request
// should be (automatically) cancelled without user interaction
@@ -41,11 +41,11 @@ export const REQUEST_TYPE = EVENT_PREFIX + "request";
export const START_TYPE = EVENT_PREFIX + "start";
export const CANCEL_TYPE = EVENT_PREFIX + "cancel";
export const DONE_TYPE = EVENT_PREFIX + "done";
// export const READY_TYPE = EVENT_PREFIX + "ready";
export const READY_TYPE = EVENT_PREFIX + "ready";
export const PHASE_UNSENT = 1;
export const PHASE_REQUESTED = 2;
// const PHASE_READY = 3;
export const PHASE_READY = 3;
export const PHASE_STARTED = 4;
export const PHASE_CANCELLED = 5;
export const PHASE_DONE = 6;
@@ -58,17 +58,22 @@ export const PHASE_DONE = 6;
* @event "change" whenever the state of the request object has changed.
*/
export class VerificationRequest extends EventEmitter {
constructor(channel, verificationMethods, userId, client) {
constructor(channel, verificationMethods, client) {
super();
this.channel = channel;
this.channel._request = this;
this._verificationMethods = verificationMethods;
this._client = client;
this._commonMethods = [];
this._setPhase(PHASE_UNSENT, false);
this._requestEvent = null;
this._otherUserId = userId;
this._initiatedByMe = null;
this._startTimestamp = null;
this._eventsByUs = new Map();
this._eventsByThem = new Map();
this._observeOnly = false;
this._timeoutTimer = null;
this._sharedSecret = null; // used for QR codes
this._accepting = false;
this._declining = false;
this._verifierHasFinished = false;
}
/**
@@ -76,37 +81,38 @@ export class VerificationRequest extends EventEmitter {
* Invoked by the same static method in either channel.
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
* @param {MatrixEvent} event the event to validate. Don't call getType() on it but use the `type` parameter instead.
* @param {number} timestamp the timestamp in milliseconds when this event was sent.
* @param {MatrixClient} client the client to get the current user and device id from
* @returns {bool} whether the event is valid and should be passed to handleEvent
*/
static validateEvent(type, event, timestamp, client) {
static validateEvent(type, event, client) {
const content = event.getContent();
if (!type.startsWith(EVENT_PREFIX)) {
if (!type || !type.startsWith(EVENT_PREFIX)) {
return false;
}
if (type === REQUEST_TYPE) {
if (!Array.isArray(content.methods)) {
return false;
}
// from here on we're fairly sure that this is supposed to be
// part of a verification request, so be noisy when rejecting something
if (!content) {
logger.log("VerificationRequest: validateEvent: no content");
return false;
}
if (type === REQUEST_TYPE || type === START_TYPE) {
if (typeof content.from_device !== "string" ||
content.from_device.length === 0
) {
if (type === REQUEST_TYPE || type === READY_TYPE) {
if (!Array.isArray(content.methods)) {
logger.log("VerificationRequest: validateEvent: " +
"fail because methods");
return false;
}
}
// a timestamp is not provided on all to_device events
if (Number.isFinite(timestamp)) {
const elapsed = Date.now() - timestamp;
// ignore if event is too far in the past or too far in the future
if (elapsed > (VERIFICATION_REQUEST_TIMEOUT - VERIFICATION_REQUEST_MARGIN) ||
elapsed < -(VERIFICATION_REQUEST_TIMEOUT / 2)) {
logger.log("received verification that is too old or from the future");
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
if (typeof content.from_device !== "string" ||
content.from_device.length === 0
) {
logger.log("VerificationRequest: validateEvent: "+
"fail because from_device");
return false;
}
}
@@ -114,20 +120,56 @@ export class VerificationRequest extends EventEmitter {
return true;
}
/** once the phase is PHASE_STARTED, common methods supported by both sides */
get invalid() {
return this.phase === PHASE_UNSENT;
}
/** returns whether the phase is PHASE_REQUESTED */
get requested() {
return this.phase === PHASE_REQUESTED;
}
/** returns whether the phase is PHASE_CANCELLED */
get cancelled() {
return this.phase === PHASE_CANCELLED;
}
/** returns whether the phase is PHASE_READY */
get ready() {
return this.phase === PHASE_READY;
}
/** returns whether the phase is PHASE_STARTED */
get started() {
return this.phase === PHASE_STARTED;
}
/** returns whether the phase is PHASE_DONE */
get done() {
return this.phase === PHASE_DONE;
}
/** once the phase is PHASE_STARTED (and !initiatedByMe) or PHASE_READY: common methods supported by both sides */
get methods() {
return this._commonMethods;
}
/** the timeout of the request, provided for compatibility with previous verification code */
/** The current remaining amount of ms before the request should be automatically cancelled */
get timeout() {
const elapsed = Date.now() - this._startTimestamp;
return Math.max(0, VERIFICATION_REQUEST_TIMEOUT - elapsed);
const requestEvent = this._getEventByEither(REQUEST_TYPE);
if (requestEvent) {
const elapsed = Date.now() - this.channel.getTimestamp(requestEvent);
return Math.max(0, VERIFICATION_REQUEST_TIMEOUT - elapsed);
}
return 0;
}
/** the m.key.verification.request event that started this request, provided for compatibility with previous verification code */
get event() {
return this._requestEvent;
/**
* The key verification request event.
* @returns {MatrixEvent} The request event, or falsey if not found.
*/
get requestEvent() {
return this._getEventByEither(REQUEST_TYPE);
}
/** current phase of the request. Some properties might only be defined in a current phase. */
@@ -140,11 +182,59 @@ export class VerificationRequest extends EventEmitter {
return this._verifier;
}
get canAccept() {
return this.phase < PHASE_READY && !this._accepting && !this._declining;
}
get accepting() {
return this._accepting;
}
get declining() {
return this._declining;
}
/** whether this request has sent it's initial event and needs more events to complete */
get pending() {
return this._phase !== PHASE_UNSENT
&& this._phase !== PHASE_DONE
&& this._phase !== PHASE_CANCELLED;
return !this.observeOnly &&
this._phase !== PHASE_DONE &&
this._phase !== PHASE_CANCELLED;
}
/** Checks whether the other party supports a given verification method.
* This is useful when setting up the QR code UI, as it is somewhat asymmetrical:
* if the other party supports SCAN_QR, we should show a QR code in the UI, and vice versa.
* For methods that need to be supported by both ends, use the `methods` property.
* @param {string} method the method to check
* @return {bool} whether or not the other party said the supported the method */
otherPartySupportsMethod(method) {
if (!this.ready && !this.started) {
return false;
}
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();
if (!content) {
return false;
}
const {methods} = content;
if (!Array.isArray(methods)) {
return false;
}
return methods.includes(method);
}
/** Whether this request was initiated by the syncing user.
@@ -152,27 +242,108 @@ export class VerificationRequest extends EventEmitter {
* For ToDeviceChannel, this is who sent the .start event
*/
get initiatedByMe() {
return this._initiatedByMe;
// event created by us but no remote echo has been received yet
const noEventsYet = (this._eventsByUs.size + this._eventsByThem.size) === 0;
if (this._phase === PHASE_UNSENT && noEventsYet) {
return true;
}
const hasMyRequest = this._eventsByUs.has(REQUEST_TYPE);
const hasTheirRequest = this._eventsByThem.has(REQUEST_TYPE);
if (hasMyRequest && !hasTheirRequest) {
return true;
}
if (!hasMyRequest && hasTheirRequest) {
return false;
}
const hasMyStart = this._eventsByUs.has(START_TYPE);
const hasTheirStart = this._eventsByThem.has(START_TYPE);
if (hasMyStart && !hasTheirStart) {
return true;
}
return false;
}
/** the id of the user that initiated the request */
/** The id of the user that initiated the request */
get requestingUserId() {
if (this.initiatedByMe) {
return this._client.getUserId();
} else {
return this._otherUserId;
return this.otherUserId;
}
}
/** the id of the user that (will) receive(d) the request */
/** The id of the user that (will) receive(d) the request */
get receivingUserId() {
if (this.initiatedByMe) {
return this._otherUserId;
return this.otherUserId;
} else {
return this._client.getUserId();
}
}
/** The user id of the other party in this request */
get otherUserId() {
return this.channel.userId;
}
/**
* The id of the user that cancelled the request,
* only defined when phase is PHASE_CANCELLED
*/
get cancellingUserId() {
const myCancel = this._eventsByUs.get(CANCEL_TYPE);
const theirCancel = this._eventsByThem.get(CANCEL_TYPE);
if (myCancel && (!theirCancel || myCancel.getId() < theirCancel.getId())) {
return myCancel.getSender();
}
if (theirCancel) {
return theirCancel.getSender();
}
return undefined;
}
/**
* The cancellation code e.g m.user which is responsible for cancelling this verification
*/
get cancellationCode() {
const ev = this._getEventByEither(CANCEL_TYPE);
return ev ? ev.getContent().code : null;
}
get observeOnly() {
return this._observeOnly;
}
/**
* The unpadded base64 encoded shared secret. Primarily used for QR code
* verification.
*/
get encodedSharedSecret() {
if (!this._sharedSecret) this._generateSharedSecret();
return this._sharedSecret;
}
/**
* Gets which device the verification should be started with
* given the events sent so far in the verification. This is the
* same algorithm used to determine which device to send the
* verification to when no specific device is specified.
* @returns {{userId: *, deviceId: *}} The device information
*/
get targetDevice() {
const theirFirstEvent =
this._eventsByThem.get(REQUEST_TYPE) ||
this._eventsByThem.get(READY_TYPE) ||
this._eventsByThem.get(START_TYPE);
const theirFirstContent = theirFirstEvent.getContent();
const fromDevice = theirFirstContent.from_device;
return {
userId: this.otherUserId,
deviceId: fromDevice,
};
}
/* Start the key verification, creating a verifier and sending a .start event.
* If no previous events have been sent, pass in `targetDevice` to set who to direct this request to.
* @param {string} method the name of the verification method to use.
@@ -182,8 +353,13 @@ export class VerificationRequest extends EventEmitter {
*/
beginKeyVerification(method, targetDevice = null) {
// need to allow also when unsent in case of to_device
if (!this._verifier) {
if (this._hasValidPreStartPhase()) {
if (!this.observeOnly && !this._verifier) {
const validStartPhase =
this.phase === PHASE_REQUESTED ||
this.phase === PHASE_READY ||
(this.phase === PHASE_UNSENT &&
this.channel.constructor.canCreateRequest(START_TYPE));
if (validStartPhase) {
// when called on a request that was initiated with .request event
// check the method is supported by both sides
if (this._commonMethods.length && !this._commonMethods.includes(method)) {
@@ -203,12 +379,10 @@ export class VerificationRequest extends EventEmitter {
* @returns {Promise} resolves when the event has been sent.
*/
async sendRequest() {
if (this._phase === PHASE_UNSENT) {
this._initiatedByMe = true;
this._setPhase(PHASE_REQUESTED, false);
if (!this.observeOnly && this._phase === PHASE_UNSENT) {
const methods = [...this._verificationMethods.keys()];
await this.channel.send(REQUEST_TYPE, {methods});
this.emit("change");
this._generateSharedSecret();
}
}
@@ -219,34 +393,67 @@ export class VerificationRequest extends EventEmitter {
* @returns {Promise} resolves when the event has been sent.
*/
async cancel({reason = "User declined", code = "m.user"} = {}) {
if (this._phase !== PHASE_CANCELLED) {
if (!this.observeOnly && this._phase !== PHASE_CANCELLED) {
this._declining = true;
this.emit("change");
if (this._verifier) {
return this._verifier.cancel(errorFactory(code, reason));
return this._verifier.cancel(errorFactory(code, reason)());
} else {
this._setPhase(PHASE_CANCELLED, false);
this._cancellingUserId = this._client.getUserId();
await this.channel.send(CANCEL_TYPE, {code, reason});
}
this.emit("change");
}
}
/** @returns {Promise} with the verifier once it becomes available. Can be used after calling `sendRequest`. */
waitForVerifier() {
if (this.verifier) {
return Promise.resolve(this.verifier);
} else {
return new Promise(resolve => {
const checkVerifier = () => {
if (this.verifier) {
this.off("change", checkVerifier);
resolve(this.verifier);
}
};
this.on("change", checkVerifier);
});
/**
* Accepts the request, sending a .ready event to the other party
* @returns {Promise} resolves when the event has been sent.
*/
async accept() {
if (!this.observeOnly && this.phase === PHASE_REQUESTED && !this.initiatedByMe) {
const methods = [...this._verificationMethods.keys()];
this._accepting = true;
this.emit("change");
await this.channel.send(READY_TYPE, {methods});
this._generateSharedSecret();
}
}
_generateSharedSecret() {
const secretBytes = new Uint8Array(8);
global.crypto.getRandomValues(secretBytes);
this._sharedSecret = olmlib.encodeUnpaddedBase64(secretBytes);
}
/**
* Can be used to listen for state changes until the callback returns true.
* @param {Function} fn callback to evaluate whether the request is in the desired state.
* Takes the request as an argument.
* @returns {Promise} that resolves once the callback returns true
* @throws {Error} when the request is cancelled
*/
waitFor(fn) {
return new Promise((resolve, reject) => {
const check = () => {
let handled = false;
if (fn(this)) {
resolve(this);
handled = true;
} else if (this.cancelled) {
reject(new Error("cancelled"));
handled = true;
}
if (handled) {
this.off("change", check);
}
return handled;
};
if (!check()) {
this.on("change", check);
}
});
}
_setPhase(phase, notify = true) {
this._phase = phase;
if (notify) {
@@ -254,155 +461,342 @@ export class VerificationRequest extends EventEmitter {
}
}
_getEventByEither(type) {
return this._eventsByThem.get(type) || this._eventsByUs.get(type);
}
_getEventBy(type, byThem) {
if (byThem) {
return this._eventsByThem.get(type);
} else {
return this._eventsByUs.get(type);
}
}
_calculatePhaseTransitions() {
const transitions = [{phase: PHASE_UNSENT}];
const phase = () => transitions[transitions.length - 1].phase;
// always pass by .request first to be sure channel.userId has been set
const hasRequestByThem = this._eventsByThem.has(REQUEST_TYPE);
const requestEvent = this._getEventBy(REQUEST_TYPE, hasRequestByThem);
if (requestEvent) {
transitions.push({phase: PHASE_REQUESTED, event: requestEvent});
}
const readyEvent =
requestEvent && this._getEventBy(READY_TYPE, !hasRequestByThem);
if (readyEvent && phase() === PHASE_REQUESTED) {
transitions.push({phase: PHASE_READY, event: readyEvent});
}
let startEvent;
if (readyEvent || !requestEvent) {
const theirStartEvent = this._eventsByThem.get(START_TYPE);
const ourStartEvent = this._eventsByUs.get(START_TYPE);
// any party can send .start after a .ready or unsent
if (theirStartEvent && ourStartEvent) {
startEvent = theirStartEvent.getSender() < ourStartEvent.getSender() ?
theirStartEvent : ourStartEvent;
} else {
startEvent = theirStartEvent ? theirStartEvent : ourStartEvent;
}
} else {
startEvent = this._getEventBy(START_TYPE, !hasRequestByThem);
}
if (startEvent) {
const fromRequestPhase = phase() === PHASE_REQUESTED &&
requestEvent.getSender() !== startEvent.getSender();
const fromUnsentPhase = phase() === PHASE_UNSENT &&
this.channel.constructor.canCreateRequest(START_TYPE);
if (fromRequestPhase || phase() === PHASE_READY || fromUnsentPhase) {
transitions.push({phase: PHASE_STARTED, event: startEvent});
}
}
const ourDoneEvent = this._eventsByUs.get(DONE_TYPE);
if (this._verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
transitions.push({phase: PHASE_DONE});
}
const cancelEvent = this._getEventByEither(CANCEL_TYPE);
if (cancelEvent && phase() !== PHASE_DONE) {
transitions.push({phase: PHASE_CANCELLED, event: cancelEvent});
return transitions;
}
return transitions;
}
_transitionToPhase(transition) {
const {phase, event} = transition;
// get common methods
if (phase === PHASE_REQUESTED || phase === PHASE_READY) {
if (!this._wasSentByOwnDevice(event)) {
const content = event.getContent();
this._commonMethods =
content.methods.filter(m => this._verificationMethods.has(m));
}
}
// detect if we're not a party in the request, and we should just observe
if (!this.observeOnly) {
// if requested or accepted by one of my other devices
if (phase === PHASE_REQUESTED ||
phase === PHASE_STARTED ||
phase === PHASE_READY
) {
if (
this.channel.receiveStartFromOtherDevices &&
this._wasSentByOwnUser(event) &&
!this._wasSentByOwnDevice(event)
) {
this._observeOnly = true;
}
}
}
// create verifier
if (phase === PHASE_STARTED) {
const {method} = event.getContent();
if (!this._verifier && !this.observeOnly) {
this._verifier = this._createVerifier(method, event);
}
}
}
_applyPhaseTransitions() {
const transitions = this._calculatePhaseTransitions();
const existingIdx = transitions.findIndex(t => t.phase === this.phase);
// trim off phases we already went through, if any
const newTransitions = transitions.slice(existingIdx + 1);
// transition to all new phases
for (const transition of newTransitions) {
this._transitionToPhase(transition);
}
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.
* @param {MatrixEvent} event the event to handle. Don't call getType() on it but use the `type` parameter instead.
* @param {number} timestamp the timestamp in milliseconds when this event was sent.
* @param {bool} isLiveEvent whether this is an even received through sync or not
* @param {bool} isRemoteEcho whether this is the remote echo of an event sent by the same device
* @param {bool} isSentByUs whether this event is sent by a party that can accept and/or observe the request like one of our peers.
* For InRoomChannel this means any device for the syncing user. For ToDeviceChannel, just the syncing device.
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
*/
async handleEvent(type, event, timestamp) {
const content = event.getContent();
if (type === REQUEST_TYPE || type === START_TYPE) {
if (this._startTimestamp === null) {
this._startTimestamp = timestamp;
}
}
if (type === REQUEST_TYPE) {
await this._handleRequest(content, event);
} else if (type === START_TYPE) {
await this._handleStart(content, event);
async handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs) {
// if reached phase cancelled or done, ignore anything else that comes
if (this.done || this.cancelled) {
return;
}
const wasObserveOnly = this._observeOnly;
if (this._verifier) {
if (type === CANCEL_TYPE || (this._verifier.events
&& this._verifier.events.includes(type))) {
this._verifier.handleEvent(event);
this._adjustObserveOnly(event, isLiveEvent);
if (!this.observeOnly && !isRemoteEcho) {
if (await this._cancelOnError(type, event)) {
return;
}
}
if (type === CANCEL_TYPE) {
this._handleCancel();
} else if (type === DONE_TYPE) {
this._handleDone();
const oldPhase = this.phase;
this._addEvent(type, event, isSentByUs);
// this will create if needed the verifier so needs to happen before calling it
const newTransitions = this._applyPhaseTransitions();
try {
// only pass events from the other side to the verifier,
// no remote echos of our own events
if (this._verifier && !this.observeOnly) {
const newEventWinsRace = this._isWinningStartRace(event);
if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) {
this._verifier.switchStartEvent(event);
} else if (!isRemoteEcho) {
if (type === CANCEL_TYPE || (this._verifier.events
&& this._verifier.events.includes(type))) {
this._verifier.handleEvent(event);
}
}
}
if (newTransitions.length) {
const lastTransition = newTransitions[newTransitions.length - 1];
const {phase} = lastTransition;
this._setupTimeout(phase);
// set phase as last thing as this emits the "change" event
this._setPhase(phase);
} else if (this._observeOnly !== wasObserveOnly) {
this.emit("change");
}
} finally {
// log events we processed so we can see from rageshakes what events were added to a request
logger.log(`Verification request ${this.channel.transactionId}: ` +
`${type} event with id:${event.getId()}, ` +
`content:${JSON.stringify(event.getContent())} ` +
`deviceId:${this.channel.deviceId}, ` +
`sender:${event.getSender()}, isSentByUs:${isSentByUs}, ` +
`isLiveEvent:${isLiveEvent}, isRemoteEcho:${isRemoteEcho}, ` +
`phase:${oldPhase}=>${this.phase}, ` +
`observeOnly:${wasObserveOnly}=>${this._observeOnly}`);
}
}
async _handleRequest(content, event) {
if (this._phase === PHASE_UNSENT) {
const otherMethods = content.methods;
this._commonMethods = otherMethods.
filter(m => this._verificationMethods.has(m));
this._requestEvent = event;
this._initiatedByMe = this._wasSentByMe(event);
this._setPhase(PHASE_REQUESTED);
} else if (this._phase !== PHASE_REQUESTED) {
logger.warn("Ignoring flagged verification request from " +
event.getSender());
await this.cancel(errorFromEvent(newUnexpectedMessageError()));
_setupTimeout(phase) {
const shouldTimeout = !this._timeoutTimer && !this.observeOnly &&
phase === PHASE_REQUESTED && this.initiatedByMe;
if (shouldTimeout) {
this._timeoutTimer = setTimeout(this._cancelOnTimeout, this.timeout);
}
if (this._timeoutTimer) {
const shouldClear = phase === PHASE_STARTED ||
phase === PHASE_READY ||
phase === PHASE_DONE ||
phase === PHASE_CANCELLED;
if (shouldClear) {
clearTimeout(this._timeoutTimer);
this._timeoutTimer = null;
}
}
}
_hasValidPreStartPhase() {
return this._phase === PHASE_REQUESTED ||
(
this.channel.constructor.canCreateRequest(START_TYPE) &&
this._phase === PHASE_UNSENT
);
}
_cancelOnTimeout = () => {
try {
this.cancel({reason: "Other party didn't accept in time", code: "m.timeout"});
} catch (err) {
logger.error("Error while cancelling verification request", err);
}
};
async _handleStart(content, event) {
if (this._hasValidPreStartPhase()) {
const {method} = content;
async _cancelOnError(type, event) {
if (type === START_TYPE) {
const method = event.getContent().method;
if (!this._verificationMethods.has(method)) {
await this.cancel(errorFromEvent(newUnknownMethodError()));
} else {
// if not in requested phase
if (this.phase === PHASE_UNSENT) {
this._initiatedByMe = this._wasSentByMe(event);
return true;
}
}
const isUnexpectedRequest = type === REQUEST_TYPE && this.phase !== PHASE_UNSENT;
const isUnexpectedReady = type === READY_TYPE && this.phase !== PHASE_REQUESTED;
// only if phase has passed from PHASE_UNSENT should we cancel, because events
// are allowed to come in in any order (at least with InRoomChannel). So we only know
// we're dealing with a valid request we should participate in once we've moved to PHASE_REQUESTED
// before that, we could be looking at somebody elses verification request and we just
// happen to be in the room
if (this.phase !== PHASE_UNSENT && (isUnexpectedRequest || isUnexpectedReady)) {
logger.warn(`Cancelling, unexpected ${type} verification ` +
`event from ${event.getSender()}`);
const reason = `Unexpected ${type} event in phase ${this.phase}`;
await this.cancel(errorFromEvent(newUnexpectedMessageError({reason})));
return true;
}
return false;
}
_adjustObserveOnly(event, isLiveEvent) {
// don't send out events for historical requests
if (!isLiveEvent) {
this._observeOnly = true;
}
// a timestamp is not provided on all to_device events
const timestamp = this.channel.getTimestamp(event);
if (Number.isFinite(timestamp)) {
const elapsed = Date.now() - timestamp;
// don't allow interaction on old requests
if (elapsed > (VERIFICATION_REQUEST_TIMEOUT - VERIFICATION_REQUEST_MARGIN) ||
elapsed < -(VERIFICATION_REQUEST_TIMEOUT / 2)
) {
this._observeOnly = true;
}
}
}
_addEvent(type, event, isSentByUs) {
if (isSentByUs) {
this._eventsByUs.set(type, event);
} else {
this._eventsByThem.set(type, event);
}
// once we know the userId of the other party (from the .request event)
// see if any event by anyone else crept into this._eventsByThem
if (type === REQUEST_TYPE) {
for (const [type, event] of this._eventsByThem.entries()) {
if (event.getSender() !== this.otherUserId) {
this._eventsByThem.delete(type);
}
this._verifier = this._createVerifier(method, event);
this._setPhase(PHASE_STARTED);
}
}
}
/**
* Called by RequestCallbackChannel when the verifier sends an event
* @param {string} type the "symbolic" event type
* @param {object} content the completed or uncompleted content for the event to be sent
*/
handleVerifierSend(type, content) {
if (type === CANCEL_TYPE) {
this._handleCancel();
} else if (type === START_TYPE) {
if (this._phase === PHASE_UNSENT || this._phase === PHASE_REQUESTED) {
// if unsent, we're sending a (first) .start event and hence requesting the verification.
// in any other situation, the request was initiated by the other party.
this._initiatedByMe = this.phase === PHASE_UNSENT;
this._setPhase(PHASE_STARTED);
}
}
}
_handleCancel() {
if (this._phase !== PHASE_CANCELLED) {
this._setPhase(PHASE_CANCELLED);
}
}
_handleDone() {
if (this._phase === PHASE_STARTED) {
this._setPhase(PHASE_DONE);
}
}
_createVerifier(method, startEvent = null, targetDevice = null) {
const startSentByMe = startEvent && this._wasSentByMe(startEvent);
const {userId, deviceId} = this._getVerifierTarget(startEvent, targetDevice);
if (!targetDevice) {
targetDevice = this.targetDevice;
}
const {userId, deviceId} = targetDevice;
const VerifierCtor = this._verificationMethods.get(method);
if (!VerifierCtor) {
console.warn("could not find verifier constructor for method", method);
logger.warn("could not find verifier constructor for method", method);
return;
}
// invokes handleVerifierSend when verifier sends something
const callbackMedium = new RequestCallbackChannel(this, this.channel);
return new VerifierCtor(
callbackMedium,
this.channel,
this._client,
userId,
deviceId,
startSentByMe ? null : startEvent,
startEvent,
this,
);
}
_getVerifierTarget(startEvent, targetDevice) {
// targetDevice should be set when creating a verifier for to_device before the .start event has been sent,
// so the userId and deviceId are provided
if (targetDevice) {
return targetDevice;
} else {
let targetEvent;
if (startEvent && !this._wasSentByMe(startEvent)) {
targetEvent = startEvent;
} else if (this._requestEvent && !this._wasSentByMe(this._requestEvent)) {
targetEvent = this._requestEvent;
} else {
throw new Error(
"can't determine who the verifier should be targeted at. " +
"No .request or .start event and no targetDevice");
}
const userId = targetEvent.getSender();
const content = targetEvent.getContent();
const deviceId = content && content.from_device;
return {userId, deviceId};
}
_wasSentByOwnUser(event) {
return event.getSender() === this._client.getUserId();
}
// only for .request and .start
_wasSentByMe(event) {
if (event.getSender() !== this._client.getUserId()) {
// only for .request, .ready or .start
_wasSentByOwnDevice(event) {
if (!this._wasSentByOwnUser(event)) {
return false;
}
const content = event.getContent();
@@ -411,4 +805,16 @@ export class VerificationRequest extends EventEmitter {
}
return true;
}
onVerifierFinished() {
if (this.channel.needsDoneMessage) {
// verification in DM requires a done message
this.channel.send("m.key.verification.done", {});
}
this._verifierHasFinished = true;
const newTransitions = this._applyPhaseTransitions();
if (newTransitions.length) {
this._setPhase(newTransitions[newTransitions.length - 1].phase);
}
}
}
+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;
}
}
+9 -8
View File
@@ -47,6 +47,7 @@ export const PREFIX_UNSTABLE = "/_matrix/client/unstable";
/**
* URI path for v1 of the the identity API
* @deprecated Use v2.
*/
export const PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
@@ -155,7 +156,7 @@ MatrixHttpApi.prototype = {
* data has been uploaded, with an object containing the fields `loaded`
* (number of bytes transferred) and `total` (total size, if known).
*
* @return {module:client.Promise} Resolves to response object, as
* @return {Promise} Resolves to response object, as
* determined by this.opts.onlyData, opts.rawResponse, and
* opts.onlyContentUri. Rejects with an error (usually a MatrixError).
*/
@@ -436,7 +437,7 @@ MatrixHttpApi.prototype = {
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
* @param {Object} [data] The HTTP JSON body.
*
* @param {Object|Number=} opts additional options. If a number is specified,
* this is treated as `opts.localTimeoutMs`.
@@ -449,7 +450,7 @@ MatrixHttpApi.prototype = {
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* @return {Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
@@ -518,7 +519,7 @@ MatrixHttpApi.prototype = {
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
* @param {Object} [data] The HTTP JSON body.
*
* @param {Object=} opts additional options
*
@@ -530,7 +531,7 @@ MatrixHttpApi.prototype = {
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* @return {Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
@@ -557,7 +558,7 @@ MatrixHttpApi.prototype = {
* @param {Object=} queryParams A dict of query params (these will NOT be
* urlencoded). If unspecified, there will be no query params.
*
* @param {Object} data The HTTP JSON body.
* @param {Object} [data] The HTTP JSON body.
*
* @param {Object=} opts additional options
*
@@ -569,7 +570,7 @@ MatrixHttpApi.prototype = {
*
* @param {Object=} opts.headers map of additional request headers
*
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* @return {Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
@@ -633,7 +634,7 @@ MatrixHttpApi.prototype = {
* @param {function=} opts.bodyParser function to parse the body of the
* response before passing it to the promise and callback.
*
* @return {module:client.Promise} a promise which resolves to either the
* @return {Promise} a promise which resolves to either the
* response object (if this.opts.onlyData is truthy), or the parsed
* body. Rejects
*/
+9 -1
View File
@@ -16,9 +16,17 @@ limitations under the License.
import * as matrixcs from "./matrix";
import * as utils from "./utils";
import request from "request";
matrixcs.request(import("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;
+3 -3
View File
@@ -47,14 +47,14 @@ const MSISDN_STAGE_TYPE = "m.login.msisdn";
* @param {object?} opts.authData error response from the last request. If
* null, a request will be made with no auth before starting.
*
* @param {function(object?): module:client.Promise} opts.doRequest
* @param {function(object?): Promise} opts.doRequest
* called with the new auth dict to submit the request. Also passes a
* second deprecated arg which is a flag set to true if this request
* is a background request. The busyChanged callback should be used
* instead of the backfround flag. Should return a promise which resolves
* to the successful response or rejects with a MatrixError.
*
* @param {function(bool): module:client.Promise} opts.busyChanged
* @param {function(bool): Promise} opts.busyChanged
* called whenever the interactive auth logic becomes busy submitting
* information provided by the user or finsihes. After this has been
* called with true the UI should indicate that a request is in progress
@@ -132,7 +132,7 @@ InteractiveAuth.prototype = {
/**
* begin the authentication process.
*
* @return {module:client.Promise} which resolves to the response on success,
* @return {Promise} which resolves to the response on success,
* or rejects with the error on failure. Rejects with NoAuthFlowFoundError if
* no suitable authentication flow can be found
*/
+19
View File
@@ -29,6 +29,25 @@ import log from "loglevel";
// Part of #332 is introducing a logging library in the first place.
const DEFAULT_NAMESPACE = "matrix";
// because rageshakes in react-sdk hijack the console log, also at module load time,
// initializing the logger here races with the initialization of rageshakes.
// to avoid the issue, we override the methodFactory of loglevel that binds to the
// console methods at initialization time by a factory that looks up the console methods
// when logging so we always get the current value of console methods.
log.methodFactory = function(methodName, logLevel, loggerName) {
return function(...args) {
const supportedByConsole = methodName === "error" ||
methodName === "warn" ||
methodName === "trace" ||
methodName === "info";
if (supportedByConsole) {
return console[methodName](...args);
} else {
return console.log(...args);
}
};
};
/**
* Drop-in replacement for <code>console</code> using {@link https://www.npmjs.com/package/loglevel|loglevel}.
* Can be tailored down to specific use cases if needed.
+3 -3
View File
@@ -103,11 +103,11 @@ export function setCryptoStoreFactory(fac) {
}
/**
* Construct a Matrix Client. Similar to {@link module:client~MatrixClient}
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
* @param {(Object|string)} opts The configuration options for this client. If
* this is a string, it is assumed to be the base URL. These configuration
* options will be passed directly to {@link module:client~MatrixClient}.
* options will be passed directly to {@link module:client.MatrixClient}.
* @param {Object} opts.store If not set, defaults to
* {@link module:store/memory.MemoryStore}.
* @param {Object} opts.scheduler If not set, defaults to
@@ -122,7 +122,7 @@ export function setCryptoStoreFactory(fac) {
* in-memory otherwise).
*
* @return {MatrixClient} A new matrix client.
* @see {@link module:client~MatrixClient} for the full list of options for
* @see {@link module:client.MatrixClient} for the full list of options for
* <code>opts</code>.
*/
export function createClient(opts) {
+7 -5
View File
@@ -490,8 +490,9 @@ EventTimelineSet.prototype.addEventsToTimeline = function(events, toStartOfTimel
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
* @param {boolean} fromCache whether the sync response came from cache
*/
EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy, fromCache) {
if (this._filter) {
const events = this._filter.filterRoomTimeline([event]);
if (!events.length) {
@@ -529,7 +530,7 @@ EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
return;
}
this.addEventToTimeline(event, this._liveTimeline, false);
this.addEventToTimeline(event, this._liveTimeline, false, fromCache);
};
/**
@@ -541,11 +542,12 @@ EventTimelineSet.prototype.addLiveEvent = function(event, duplicateStrategy) {
* @param {MatrixEvent} event
* @param {EventTimeline} timeline
* @param {boolean} toStartOfTimeline
* @param {boolean} fromCache whether the sync response came from cache
*
* @fires module:client~MatrixClient#event:"Room.timeline"
*/
EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
toStartOfTimeline) {
toStartOfTimeline, fromCache) {
const eventId = event.getId();
timeline.addEvent(event, toStartOfTimeline);
this._eventIdToTimeline[eventId] = timeline;
@@ -555,7 +557,7 @@ EventTimelineSet.prototype.addEventToTimeline = function(event, timeline,
const data = {
timeline: timeline,
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline,
liveEvent: !toStartOfTimeline && timeline == this._liveTimeline && !fromCache,
};
this.emit("Room.timeline", event, this.room,
Boolean(toStartOfTimeline), false, data);
@@ -828,7 +830,7 @@ EventTimelineSet.prototype.aggregateRelations = function(event) {
*
* @param {object} data more data about the event
*
* @param {module:event-timeline.EventTimeline} data.timeline the timeline the
* @param {module:models/event-timeline.EventTimeline} data.timeline the timeline the
* event was added to/removed from
*
* @param {boolean} data.liveEvent true if the event was a real-time event
+11 -1
View File
@@ -22,7 +22,7 @@ limitations under the License.
*/
import {EventEmitter} from 'events';
import * as utils from '../utils.js';
import * as utils from '../utils';
import {logger} from '../logger';
/**
@@ -154,6 +154,12 @@ export const MatrixEvent = function(
* attempt may succeed)
*/
this._retryDecryption = false;
/* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
* `Crypto` will set this the `VerificationRequest` for the event
* so it can be easily accessed from the timeline.
*/
this.verificationRequest = null;
};
utils.inherits(MatrixEvent, EventEmitter);
@@ -1054,6 +1060,10 @@ utils.extend(MatrixEvent.prototype, {
encrypted: this.event,
};
},
setVerificationRequest: function(request) {
this.verificationRequest = request;
},
});
+20 -6
View File
@@ -861,11 +861,23 @@ Room.prototype.getAliases = function() {
Room.prototype.getCanonicalAlias = function() {
const canonicalAlias = this.currentState.getStateEvents("m.room.canonical_alias", "");
if (canonicalAlias) {
return canonicalAlias.getContent().alias;
return canonicalAlias.getContent().alias || null;
}
return null;
};
/**
* Get this room's alternative aliases
* @return {array} The room's alternative aliases, or an empty array
*/
Room.prototype.getAltAliases = function() {
const canonicalAlias = this.currentState.getStateEvents("m.room.canonical_alias", "");
if (canonicalAlias) {
return canonicalAlias.getContent().alt_aliases || [];
}
return [];
};
/**
* Add events to a timeline
*
@@ -1067,10 +1079,11 @@ Room.prototype.removeFilteredTimelineSet = function(filter) {
*
* @param {MatrixEvent} event Event to be added
* @param {string?} duplicateStrategy 'ignore' or 'replace'
* @param {boolean} fromCache whether the sync response came from cache
* @fires module:client~MatrixClient#event:"Room.timeline"
* @private
*/
Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
Room.prototype._addLiveEvent = function(event, duplicateStrategy, fromCache) {
if (event.isRedaction()) {
const redactId = event.event.redacts;
@@ -1117,7 +1130,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
// add to our timeline sets
for (let i = 0; i < this._timelineSets.length; i++) {
this._timelineSets[i].addLiveEvent(event, duplicateStrategy);
this._timelineSets[i].addLiveEvent(event, duplicateStrategy, fromCache);
}
// synthesize and inject implicit read receipts
@@ -1427,9 +1440,10 @@ Room.prototype._revertRedactionLocalEcho = function(redactionEvent) {
* this function will be ignored entirely, preserving the existing event in the
* timeline. Events are identical based on their event ID <b>only</b>.
*
* @param {boolean} fromCache whether the sync response came from cache
* @throws If <code>duplicateStrategy</code> is not falsey, 'replace' or 'ignore'.
*/
Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
Room.prototype.addLiveEvents = function(events, duplicateStrategy, fromCache) {
let i;
if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
@@ -1455,7 +1469,7 @@ Room.prototype.addLiveEvents = function(events, duplicateStrategy) {
for (i = 0; i < events.length; i++) {
// TODO: We should have a filter to say "only add state event
// types X Y Z to the timeline".
this._addLiveEvent(events[i], duplicateStrategy);
this._addLiveEvent(events[i], duplicateStrategy, fromCache);
}
};
@@ -1819,7 +1833,7 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
let alias = room.getCanonicalAlias();
if (!alias) {
const aliases = room.getAliases();
const aliases = room.getAltAliases();
if (aliases.length) {
alias = aliases[0];
+9 -4
View File
@@ -388,8 +388,10 @@ SyncApi.prototype._peekPoll = function(peekRoom, token) {
});
// strip out events which aren't for the given room_id (e.g presence)
// and also ephemeral events (which we're assuming is anything without
// and event ID because the /events API doesn't separate them).
const events = res.chunk.filter(function(e) {
return e.room_id === peekRoom.roomId;
return e.room_id === peekRoom.roomId && e.event_id;
}).map(self.client.getEventMapper());
peekRoom.addLiveEvents(events);
@@ -688,6 +690,7 @@ SyncApi.prototype._syncFromCache = async function(savedSync) {
oldSyncToken: null,
nextSyncToken,
catchingUp: false,
fromCache: true,
};
const data = {
@@ -1237,7 +1240,8 @@ SyncApi.prototype._processSyncResponse = async function(
}
}
self._processRoomEvents(room, stateEvents, timelineEvents);
self._processRoomEvents(room, stateEvents,
timelineEvents, syncEventData.fromCache);
// set summary after processing events,
// because it will trigger a name calculation
@@ -1564,10 +1568,11 @@ SyncApi.prototype._resolveInvites = function(room) {
* @param {MatrixEvent[]} stateEventList A list of state events. This is the state
* at the *START* of the timeline list if it is supplied.
* @param {MatrixEvent[]} [timelineEventList] A list of timeline events. Lower index
* @param {boolean} fromCache whether the sync response came from cache
* is earlier in time. Higher index is later.
*/
SyncApi.prototype._processRoomEvents = function(room, stateEventList,
timelineEventList) {
timelineEventList, fromCache) {
// If there are no events in the timeline yet, initialise it with
// the given state events
const liveTimeline = room.getLiveTimeline();
@@ -1621,7 +1626,7 @@ SyncApi.prototype._processRoomEvents = function(room, stateEventList,
// if the timeline has any state events in it.
// This also needs to be done before running push rules on the events as they need
// to be decorated with sender etc.
room.addLiveEvents(timelineEventList || []);
room.addLiveEvents(timelineEventList || [], null, fromCache);
};
/**
+61 -31
View File
@@ -89,7 +89,7 @@ export function TimelineWindow(client, timelineSet, opts) {
* given event
* @param {number} [initialWindowSize = 20] Size of the initial window
*
* @return {module:client.Promise}
* @return {Promise}
*/
TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
const self = this;
@@ -147,6 +147,62 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
}
};
/**
* Get the TimelineIndex of the window in the given direction.
*
* @param {string} direction EventTimeline.BACKWARDS to get the TimelineIndex
* at the start of the window; EventTimeline.FORWARDS to get the TimelineIndex at
* the end.
*
* @return {TimelineIndex} The requested timeline index if one exists, null
* otherwise.
*/
TimelineWindow.prototype.getTimelineIndex = function(direction) {
if (direction == EventTimeline.BACKWARDS) {
return this._start;
} else if (direction == EventTimeline.FORWARDS) {
return this._end;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
};
/**
* Try to extend the window using events that are already in the underlying
* TimelineIndex.
*
* @param {string} direction EventTimeline.BACKWARDS to try extending it
* backwards; EventTimeline.FORWARDS to try extending it forwards.
* @param {number} size number of events to try to extend by.
*
* @return {boolean} true if the window was extended, false otherwise.
*/
TimelineWindow.prototype.extend = function(direction, size) {
const tl = this.getTimelineIndex(direction);
if (!tl) {
debuglog("TimelineWindow: no timeline yet");
return false;
}
const count = (direction == EventTimeline.BACKWARDS) ?
tl.retreat(size) : tl.advance(size);
if (count) {
this._eventCount += count;
debuglog("TimelineWindow: increased cap by " + count +
" (now " + this._eventCount + ")");
// remove some events from the other end, if necessary
const excess = this._eventCount - this._windowLimit;
if (excess > 0) {
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
}
return true;
}
return false;
};
/**
* Check if this window can be extended
*
@@ -161,14 +217,7 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
* @return {boolean} true if we can paginate in the given direction
*/
TimelineWindow.prototype.canPaginate = function(direction) {
let tl;
if (direction == EventTimeline.BACKWARDS) {
tl = this._start;
} else if (direction == EventTimeline.FORWARDS) {
tl = this._end;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
const tl = this.getTimelineIndex(direction);
if (!tl) {
debuglog("TimelineWindow: no timeline yet");
@@ -208,7 +257,7 @@ TimelineWindow.prototype.canPaginate = function(direction) {
* @param {number} [requestLimit = 5] limit for the number of API requests we
* should make.
*
* @return {module:client.Promise} Resolves to a boolean which is true if more events
* @return {Promise} Resolves to a boolean which is true if more events
* were successfully retrieved.
*/
TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
@@ -224,14 +273,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
requestLimit = DEFAULT_PAGINATE_LOOP_LIMIT;
}
let tl;
if (direction == EventTimeline.BACKWARDS) {
tl = this._start;
} else if (direction == EventTimeline.FORWARDS) {
tl = this._end;
} else {
throw new Error("Invalid direction '" + direction + "'");
}
const tl = this.getTimelineIndex(direction);
if (!tl) {
debuglog("TimelineWindow: no timeline yet");
@@ -243,18 +285,7 @@ TimelineWindow.prototype.paginate = function(direction, size, makeRequest,
}
// try moving the cap
const count = (direction == EventTimeline.BACKWARDS) ?
tl.retreat(size) : tl.advance(size);
if (count) {
this._eventCount += count;
debuglog("TimelineWindow: increased cap by " + count +
" (now " + this._eventCount + ")");
// remove some events from the other end, if necessary
const excess = this._eventCount - this._windowLimit;
if (excess > 0) {
this.unpaginate(excess, direction != EventTimeline.BACKWARDS);
}
if (this.extend(direction, size)) {
return Promise.resolve(true);
}
@@ -490,4 +521,3 @@ TimelineIndex.prototype.advance = function(delta) {
TimelineIndex.prototype.retreat = function(delta) {
return this.advance(delta * -1) * -1;
};
+69 -41
View File
@@ -28,7 +28,7 @@ import unhomoglyph from 'unhomoglyph';
* {"foo": "bar", "baz": "taz"}
* @return {string} The encoded string e.g. foo=bar&baz=taz
*/
export function encodeParams(params) {
export function encodeParams(params: Record<string, string>): string {
let qs = "";
for (const key in params) {
if (!params.hasOwnProperty(key)) {
@@ -48,7 +48,8 @@ export function encodeParams(params) {
* variables with. E.g. { "$bar": "baz" }.
* @return {string} The result of replacing all template variables e.g. '/foo/baz'.
*/
export function encodeUri(pathTemplate, variables) {
export function encodeUri(pathTemplate: string,
variables: Record<string, string>): string {
for (const key in variables) {
if (!variables.hasOwnProperty(key)) {
continue;
@@ -67,7 +68,7 @@ export function encodeUri(pathTemplate, variables) {
* the array with the signature <code>fn(element){...}</code>
* @return {Array} A new array with the results of the function.
*/
export function map(array, fn) {
export function map<T, S>(array: T[], fn: (t: T) => S): S[] {
const results = new Array(array.length);
for (let i = 0; i < array.length; i++) {
results[i] = fn(array[i]);
@@ -83,8 +84,9 @@ export function map(array, fn) {
* looks like <code>fn(element, index, array){...}</code>.
* @return {Array} A new array with the results of the function.
*/
export function filter(array, fn) {
const results = [];
export function filter<T>(array: T[],
fn: (t: T, i?: number, a?: T[]) => boolean): T[] {
const results: T[] = [];
for (let i = 0; i < array.length; i++) {
if (fn(array[i], i, array)) {
results.push(array[i]);
@@ -98,15 +100,15 @@ export function filter(array, fn) {
* @param {Object} obj The object to get the keys for.
* @return {string[]} The keys of the object.
*/
export function keys(obj) {
const keys = [];
export function keys(obj: object): string[] {
const result = [];
for (const key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
}
keys.push(key);
result.push(key);
}
return keys;
return result;
}
/**
@@ -114,15 +116,15 @@ export function keys(obj) {
* @param {Object} obj The object to get the values for.
* @return {Array<*>} The values of the object.
*/
export function values(obj) {
const values = [];
export function values<T>(obj: Record<string, T>): T[] {
const result = [];
for (const key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
}
values.push(obj[key]);
result.push(obj[key]);
}
return values;
return result;
}
/**
@@ -131,7 +133,7 @@ export function values(obj) {
* @param {Function} fn The function to invoke for each element. Has the
* function signature <code>fn(element, index)</code>.
*/
export function forEach(array, fn) {
export function forEach<T>(array: T[], fn: (t: T, i: number) => void) {
for (let i = 0; i < array.length; i++) {
fn(array[i], i);
}
@@ -148,7 +150,11 @@ export function forEach(array, fn) {
* @return {*} The first value in the array which returns <code>true</code> for
* the given function.
*/
export function findElement(array, fn, reverse) {
export function findElement<T>(
array: T[],
fn: (t: T, i?: number, a?: T[]) => boolean,
reverse?: boolean
) {
let i;
if (reverse) {
for (i = array.length - 1; i >= 0; i--) {
@@ -175,7 +181,11 @@ export function findElement(array, fn, reverse) {
* @param {boolean} reverse True to search in reverse order.
* @return {boolean} True if an element was removed.
*/
export function removeElement(array, fn, reverse) {
export function removeElement<T>(
array: T[],
fn: (t: T, i?: number, a?: T[]) => boolean,
reverse?: boolean
) {
let i;
let removed;
if (reverse) {
@@ -203,8 +213,8 @@ export function removeElement(array, fn, reverse) {
* @param {*} value The thing to check.
* @return {boolean} True if it is a function.
*/
export function isFunction(value) {
return Object.prototype.toString.call(value) == "[object Function]";
export function isFunction(value: any) {
return Object.prototype.toString.call(value) === "[object Function]";
}
/**
@@ -212,7 +222,7 @@ export function isFunction(value) {
* @param {*} value The thing to check.
* @return {boolean} True if it is an array.
*/
export function isArray(value) {
export function isArray(value: any) {
return Array.isArray ? Array.isArray(value) :
Boolean(value && value.constructor === Array);
}
@@ -223,10 +233,11 @@ export function isArray(value) {
* @param {string[]} keys The list of keys that 'obj' must have.
* @throws If the object is missing keys.
*/
export function checkObjectHasKeys(obj, keys) {
for (let i = 0; i < keys.length; i++) {
if (!obj.hasOwnProperty(keys[i])) {
throw new Error("Missing required key: " + keys[i]);
// note using 'keys' here would shadow the 'keys' function defined above
export function checkObjectHasKeys(obj: object, keys_: string[]) {
for (let i = 0; i < keys_.length; i++) {
if (!obj.hasOwnProperty(keys_[i])) {
throw new Error("Missing required key: " + keys_[i]);
}
}
}
@@ -237,7 +248,7 @@ export function checkObjectHasKeys(obj, keys) {
* @param {string[]} allowedKeys The list of allowed key names.
* @throws If there are extra keys.
*/
export function checkObjectHasNoAdditionalKeys(obj, allowedKeys) {
export function checkObjectHasNoAdditionalKeys(obj: object, allowedKeys: string[]): void {
for (const key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
@@ -254,7 +265,7 @@ export function checkObjectHasNoAdditionalKeys(obj, allowedKeys) {
* @param {Object} obj The object to deep copy.
* @return {Object} A copy of the object without any references to the original.
*/
export function deepCopy(obj) {
export function deepCopy(obj: object): object {
return JSON.parse(JSON.stringify(obj));
}
@@ -266,7 +277,7 @@ export function deepCopy(obj) {
*
* @return {boolean} true if the two objects are equal
*/
export function deepCompare(x, y) {
export function deepCompare(x: any, y: any): boolean {
// Inspired by
// http://stackoverflow.com/questions/1068834/object-comparison-in-javascript#1144249
@@ -377,7 +388,7 @@ export function runPolyfills() {
// SOURCE:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
if (!Array.prototype.filter) {
Array.prototype.filter = function(fun/*, thisArg*/) {
Array.prototype.filter = function(fun: Function/*, thisArg*/) {
if (this === void 0 || this === null) {
throw new TypeError();
}
@@ -453,7 +464,7 @@ export function runPolyfills() {
// 8. Repeat, while k < len
while (k < len) {
var kValue, mappedValue;
let kValue, mappedValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
@@ -538,7 +549,7 @@ export function runPolyfills() {
// 7. Repeat, while k < len
while (k < len) {
var kValue;
let kValue;
// a. Let Pk be ToString(k).
// This is implicit for LHS operands of the in operator
@@ -572,7 +583,7 @@ export function runPolyfills() {
* prototype.
* @param {function} superCtor Constructor function to inherit prototype from.
*/
export function inherits(ctor, superCtor) {
export function inherits(ctor: Function, superCtor: Function) {
// Add util.inherits from Node.js
// Source:
// https://github.com/joyent/node/blob/master/lib/util.js
@@ -596,7 +607,7 @@ export function inherits(ctor, superCtor) {
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
ctor.super_ = superCtor;
(ctor as any).super_ = superCtor;
ctor.prototype = Object.create(superCtor.prototype, {
constructor: {
value: ctor,
@@ -617,7 +628,7 @@ export function inherits(ctor, superCtor) {
* @param {any} SuperType The type to act as a super instance
* @param {any} params Arguments to supply to the super type's constructor
*/
export function polyfillSuper(thisArg, SuperType, ...params) {
export function polyfillSuper(thisArg: any, SuperType: any, ...params: any[]) {
try {
SuperType.call(thisArg, ...params);
} catch (e) {
@@ -633,7 +644,7 @@ export function polyfillSuper(thisArg, SuperType, ...params) {
* @param {*} value the value to test
* @return {boolean} whether or not value is a finite number without type-coercion
*/
export function isNumber(value) {
export function isNumber(value: any): boolean {
return typeof value === 'number' && isFinite(value);
}
@@ -643,7 +654,7 @@ export function isNumber(value) {
* @param {string} str the string to remove hidden characters from
* @return {string} a string with the hidden characters removed
*/
export function removeHiddenChars(str) {
export function removeHiddenChars(str: string): string {
return unhomoglyph(str.normalize('NFD').replace(removeHiddenCharsRegex, ''));
}
@@ -656,11 +667,11 @@ export function removeHiddenChars(str) {
// Zero width no-break space (BOM) U+FEFF
const removeHiddenCharsRegex = /[\u2000-\u200F\u202A-\u202F\u0300-\u036f\uFEFF\s]/g;
export function escapeRegExp(string) {
export function escapeRegExp(string: string): string {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function globToRegexp(glob, extended) {
export function globToRegexp(glob: string, extended: any): string {
extended = typeof(extended) === 'boolean' ? extended : true;
// From
// https://github.com/matrix-org/synapse/blob/abbee6b29be80a77e05730707602f3bbfc3f38cb/synapse/push/__init__.py#L132
@@ -679,7 +690,7 @@ export function globToRegexp(glob, extended) {
return pat;
}
export function ensureNoTrailingSlash(url) {
export function ensureNoTrailingSlash(url: string): string {
if (url && url.endsWith("/")) {
return url.substr(0, url.length - 1);
} else {
@@ -688,13 +699,13 @@ export function ensureNoTrailingSlash(url) {
}
// Returns a promise which resolves with a given value after the given number of ms
export function sleep(ms, value) {
export function sleep<T>(ms: number, value: T): Promise<T> {
return new Promise((resolve => {
setTimeout(resolve, ms, value);
}));
}
export function isNullOrUndefined(val) {
export function isNullOrUndefined(val: any): boolean {
return val === null || val === undefined;
}
@@ -711,12 +722,29 @@ export function defer() {
return {resolve, reject, promise};
}
export async function promiseMapSeries(promises, fn) {
export async function promiseMapSeries<T>(
promises: Promise<T>[],
fn: (t: T) => void
): Promise<void> {
for (const o of await promises) {
await fn(await o);
}
}
export function promiseTry(fn) {
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;
}
+1027 -1225
View File
File diff suppressed because it is too large Load Diff