Compare commits

..

523 Commits

Author SHA1 Message Date
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
RiotRobot d84e0b166b v4.0.0 2020-01-27 11:28:17 +00:00
RiotRobot d1d46009cd Prepare changelog for v4.0.0 2020-01-27 11:28:17 +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 925c6ffc3e Merge pull request #1170 from matrix-org/travis/fix-build-release
Move & upgrade babel runtime into dependencies (like it wants)
2020-01-23 15:46:30 -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 ffcb1c2513 Move & upgrade babel runtime into dependencies (like it wants)
https://babeljs.io/docs/en/babel-runtime
2020-01-23 15:44:59 -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 8aa1c1545e Merge pull request #1164 from matrix-org/dbkr/prepublish_rel
Add a prepare script
2020-01-22 20:59:40 +00:00
David Baker 7c84f421c5 Turns out prepublish is deprecated and should be prepare 2020-01-22 20:58:25 +00:00
David Baker 42a1dea7ad Add a prepublish script
So we actually build the lib directory before publishing it
2020-01-22 20:58:18 +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
RiotRobot 43fee73924 v4.0.0-rc.1 2020-01-20 14:24:36 +00:00
RiotRobot b72e9cb36c Prepare changelog for v4.0.0-rc.1 2020-01-20 14:24:35 +00: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
J. Ryan Stinnett 737dcc1d29 Merge pull request #1154 from matrix-org/jryans/complete-sec-confused
Convert secret storage to new account data API
2020-01-20 11:30:55 +00:00
Travis Ralston dba08d230e Merge pull request #1157 from aaronraimist/v5-safe
Add v5 as a safe room version
2020-01-18 19:29:44 -07:00
Aaron Raimist 15fb363874 Add v5 as a safe room version
Signed-off-by: Aaron Raimist <aaron@raim.ist>
2020-01-18 16:37:19 -06: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
J. Ryan Stinnett ceb4581f91 Convert secret storage to new account data API
This converts all secret storage to use a newer account data API which uses
cached data in stored when available, but also knows how to ask the homeserver
in case it's invoked during early client startup before the initial sync.

As a consequence, it means most secret storage APIs are now async.

Part of https://github.com/vector-im/riot-web/issues/11901
2020-01-17 17:56:05 +00: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
J. Ryan Stinnett 8217c0f05f Merge pull request #1153 from matrix-org/jryans/cross-signing-setup
Add API to get account data from server
2020-01-16 16:42:12 +00:00
J. Ryan Stinnett c5c27b3cb0 Add API to get account data from server
This adds an API account data getter that bypasses the local store and goes
directly to the homeserver.

Part of https://github.com/vector-im/riot-web/issues/11214
2020-01-15 21:17:53 +00:00
Travis Ralston 04bbfae08e Merge pull request #1151 from matrix-org/travis/sourcemaps
Fix sourcemaps by refactoring the build system
2020-01-15 10:11:25 -07:00
Travis Ralston b3efa73eda Fix conflict in megolm.js 2020-01-15 09:06:30 -07:00
Travis Ralston f3efac059c Merge branch 'develop' into travis/sourcemaps 2020-01-15 09:03:21 -07:00
Hubert Chathi 9fb4ed2ec0 Merge pull request #1146 from uhoreg/reporting_olm_error
record, report, and notify about olm errors
2020-01-15 09:37:43 -05:00
Hubert Chathi f19013143a fix indexedDB storage and retry decryption when we get an olm error 2020-01-14 23:47:05 -05:00
Hubert Chathi ea3ee9bea5 Merge pull request #1148 from matrix-org/erikj/per_user_device_messages
Send device messages for the same user in same API call.
2020-01-14 21:33:46 -05:00
Travis Ralston ccca6f4b6d Re-add dist script usage to js-sdk and release script 2020-01-14 14:44:06 -07:00
David Baker 6a583d2ba6 Merge pull request #1150 from matrix-org/dbkr/dont_error_on_unknown_devices
Add an option to ignore unverified devices
2020-01-14 17:11:47 +00:00
Travis Ralston 4049a32871 Fix imports in crypto index post-merge 2020-01-14 10:08:26 -07:00
Travis Ralston 331c9ce1ff [CONFLICT CHUNKS] Merge branch 'develop' into travis/sourcemaps 2020-01-14 10:04:36 -07:00
David Baker 81ab2aca37 spelling
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2020-01-14 14:10:17 +00:00
David Baker 564b8276bf Merge pull request #1144 from matrix-org/dbkr/key_backup_resign
Sign key backup with cross-signing key on upgrade
2020-01-14 11:30:06 +00:00
Erik Johnston b4a93d2dc3 Also apply cahnge to '_splitBlockedDevices' 2020-01-14 10:09:19 +00:00
Erik Johnston 260040b919 Rename var to match new function 2020-01-14 10:07:54 +00:00
J. Ryan Stinnett 8dbef8b68e Merge pull request #1145 from aaronraimist/lock
Emoji verification: Change name of 🔒 to lock
2020-01-13 21:32:49 +00:00
Travis Ralston 458b2d422d Merge branch 'develop' into travis/sourcemaps 2020-01-13 11:17:27 -07:00
David Baker ee51357dbc Add an option to ignore unverified devices
Hopefully all necessary information is on the docstring.

Default behaviour remains unchanged.
2020-01-13 17:37:38 +00:00
Hubert Chathi fa679e873d Merge pull request #1147 from uhoreg/separate_encrypted_content
use a separate object for each encrypted content
2020-01-13 10:17:32 -05:00
Erik Johnston ed3fded8e8 Send device messages for the same user in same API call.
Currently we split the device messages up to limit the number per call,
but that can end up splitting messages to a given users device over
separate API calls. This is fine, but means that the server can't e.g.
bundle them into a single EDU for remote users or sanity check that the
client is sending to the right set of devices (i.e. its device list
cache isn't wrong).
2020-01-13 13:43:00 +00:00
RiotRobot 92df82bfa9 Merge branch 'master' into develop 2020-01-13 12:55:18 +00:00
Hubert Chathi 0a9959bffb use a separate object for each encrypted content
so that we don't duplicate the ciphertext for everyone
2020-01-11 15:49:01 -05:00
Hubert Chathi b3a16cb852 lint (and add a comment) 2020-01-10 13:36:11 -05:00
Aaron Raimist 9beb259333 Emoji verification: Change name of 🔒 to lock
Signed-off-by: Aaron Raimist <aaron@raim.ist>
2020-01-09 21:55:43 -06:00
Hubert Chathi 63c57e8e02 record, report, and notify about olm errors 2020-01-09 22:19:35 -05:00
David Baker 0448a7ea68 Sign key backup with cross-signing key on upgrade
Add a signature from the cross-signing master key to the key
backup when upgrading the key backup into cross-signing.

For https://github.com/vector-im/riot-web/issues/11747
2020-01-09 20:46:36 +00:00
Travis Ralston 5bd005b28a Merge pull request #1143 from matrix-org/travis/sourcemaps-dev
Sourcemaps: develop -> feature branch
2020-01-07 15:30:56 -07:00
Travis Ralston 3aec6367d1 Fix OlmDevice import for "algorithms" to fix tests 2020-01-07 15:09:46 -07:00
Travis Ralston cea3831c20 Fix merge conflicts
This is done outside of the merge to highlight the changes, hopefully.
2020-01-07 14:43:36 -07:00
Travis Ralston 18ccceca2d [CONFLICT CHUNKS] Merge branch 'develop' into travis/sourcemaps-dev 2020-01-07 14:37:17 -07:00
Travis Ralston fffcdcb514 Merge pull request #1134 from matrix-org/travis/babel7-wp-media
Use a safer import/export scheme for the ContentRepo utilities
2020-01-07 14:27:36 -07:00
Hubert Chathi efadf374d6 Merge pull request #1142 from uhoreg/reporting_no_key_fix
Fix error handling in decryptGroupMessage
2020-01-07 11:36:53 -05:00
Hubert Chathi 55ecb40190 don't keep processing if we have an error 2020-01-07 11:25:30 -05:00
Hubert Chathi 01f6b3dfc6 notify devices when we don't send them keys (#1135)
and handle incoming notifications
2020-01-06 17:47:22 -05:00
Travis Ralston f4d1c5c006 Switch back to plain export functions instead of class 2020-01-03 12:16:54 -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
Travis Ralston e52e2f10bf Merge pull request #1131 from matrix-org/travis/babel7-wp-main
Add additional properties to package.json for riot-web's webpack
2020-01-02 10:51:52 -07:00
Travis Ralston dfc19e79f1 Merge pull request #1133 from matrix-org/travis/babel7-wp-idb
Fix import for indexeddb crypto store
2019-12-27 12:04:11 -07:00
Travis Ralston f59bd3da7a Merge pull request #1132 from matrix-org/travis/babel7-wp-request
Use the right request when creating clients
2019-12-27 11:56:52 -07:00
Travis Ralston 50791e3aa7 Make ContentRepo a class for easier importing
Exporting it the way we were was causing problems for webpack way down
the line, so we export it differently here to get around that. We also
have to fix all the import references so we import the right thing.
2019-12-22 20:51:30 -07:00
Travis Ralston 8211b2358f Fix import for indexeddb crypto store 2019-12-22 20:48:24 -07:00
Travis Ralston f2e1f3393d Add additional properties to package.json for riot-web's webpack
See https://github.com/vector-im/riot-web/pull/11679/commits/a1c9551bc8a1a6d61afed7e87ff7cebb3042a5ac
2019-12-22 20:47:45 -07:00
Travis Ralston 0ffec0a32d Use the right request when creating clients 2019-12-22 20:45:04 -07:00
Travis Ralston 1e5e705458 Regen lockfile 2019-12-19 17:10:32 -07:00
Travis Ralston f2af6ea60d Merge pull request #1127 from matrix-org/travis/babel7-btargets
Target NodeJS 10, minified browser bundle, and other publishing/package things
2019-12-19 17:08:57 -07:00
Travis Ralston de9187fee2 Merge branch 'travis/sourcemaps' into travis/babel7-btargets 2019-12-19 17:08:50 -07:00
Travis Ralston 5eed091185 Merge pull request #1126 from matrix-org/travis/babel7-updated-sourcemaps
Re-focus sourcemap generation
2019-12-19 17:08:03 -07:00
Travis Ralston 06644b5748 Merge pull request #1125 from matrix-org/travis/babel7-cleanup
Remove ancient polyfill for prototype inheritance
2019-12-19 17:07:56 -07:00
Travis Ralston bb853f65e0 Merge pull request #1124 from matrix-org/travis/babel7-test-sourcemaps
Remove "source-map-support" from tests because it makes sourcemaps worse
2019-12-19 17:07:49 -07:00
Travis Ralston eb830dd014 Merge pull request #1123 from matrix-org/travis/babel7-strict
Remove ancient "use strict" annotations
2019-12-19 17:07:41 -07:00
Travis Ralston de82d1e90c Merge pull request #1122 from matrix-org/travis/babel7-src-es6
Use ES6 imports/exports instead of older CommonJS ones
2019-12-19 17:07:29 -07:00
Travis Ralston 53e838083c Fix terser for new sourcemap handling 2019-12-19 14:04:01 -07:00
Travis Ralston 975368de8f Externalize our sourcemaps for the browser bundle 2019-12-19 13:58:08 -07:00
Travis Ralston 89173be055 Update README to describe build targets better 2019-12-19 13:42:47 -07:00
Travis Ralston b376a7c399 Improve minification
uglifify was being applied inline, which only resulted in one asset. With this we've gone back to using `terser` to generate the minified version of the browser bundle, which also exports sourcemaps interpretted from browserify. 

The babelify options have changed to ensure that browser-safe code gets published so terser can more effectively parse it. It doesn't like things like classes it seems, but is fine with let/const and such. The preset-env preset automatically knows it is targeting a browser.
2019-12-19 12:25:28 -07:00
Travis Ralston 2df262d877 Reword concerns about default exports 2019-12-19 11:56:47 -07:00
Travis Ralston ffb0e27efa We don't need no dist 2019-12-18 11:37:21 -07:00
Travis Ralston e71c4b3bc4 Publish src to npm as well
So downstream projects can use it via /src if they really want to.
2019-12-17 19:35:00 -07:00
Travis Ralston 85a0adb004 Reorder babel arguments to leave more of it to the config 2019-12-17 19:31:53 -07:00
Travis Ralston f1475cd3d7 Target NodeJS 10 and minified browser bundle
We release with Node 10 currently, so we should use that. The browser bundle is minfied because we want to keep it, so we might as well shave about 1mb off of it.
2019-12-17 16:18:11 -07:00
Travis Ralston 8c14812537 Re-focus sourcemap generation
We'll let babel decide where best to put the sourcemaps. We previously needed inline sourcemaps for browserify to work, though `babelify` takes care of this now that we use `src/` (without sourcemaps in `lib/` being inline, the transform wouldn't work).

Typescript sourcemaps have also been enabled as a mental reminder that they will be exported. Babel is the only thing that uses the tsconfig for generation right now, and it appears to ignore the sourcemaps field.
2019-12-17 16:14:14 -07:00
Travis Ralston 27aedf0563 Remove ancient polyfill for prototype inheritance
None of our targets care about this.
2019-12-17 16:07:56 -07:00
Travis Ralston 95c2c1643e Remove "source-map-support" from tests because it makes sourcemaps worse
Now that we're pointing at `src/` for tests, we can stop trying to load source maps from random places. With this dependency used, source maps are off by a few lines.
2019-12-17 15:45:15 -07:00
Travis Ralston f952f6742f Remove ancient "use strict" annotations
We don't need these anymore. Theoretically this commit could go to develop, but for safety it's going to `travis/sourcemaps` first.
2019-12-17 15:43:02 -07:00
Travis Ralston f3a10a8166 Appease the linter's line length limit 2019-12-17 15:27:17 -07:00
Travis Ralston 0790201cca Add tsify so the browser bundle can use src/ 2019-12-17 15:21:22 -07:00
Travis Ralston 5938c49453 Move index files for outputs and update pipeline
Having them in `src/` helps IDEs do autocomplete a bit more nicely, and helps us not get confused about which one is referencing which. They have also been converted to TypeScript for typings to be generated.
2019-12-17 15:16:37 -07:00
Travis Ralston 14fb080f80 Document ES6 changes 2019-12-17 15:16:37 -07:00
Travis Ralston 034b8db070 Convert tests to ES6
The earlier commit, d3ce0cb82f, has most of the juicy details on this. In addition to d3ce's changes, we also:
* Use `TestClient` in many integration tests due to subtle behaviour changes in imports when switching to ES6. Namely the behaviour where setting the request function is less reliable in the way we did it, but `TestClient` is very reliable.
* We now use the Olm loader more often to avoid having to maintain so much duplicate code. This makes the imports slightly easier to read.
2019-12-17 15:16:37 -07:00
Travis Ralston d3ce0cb82f Convert src to ES6
The bulk of this is just export/import changes, though there's a couple pieces to highlight:
* We no longer use default exports. This is because it's discouraged by the JS community, though not in any official capacity.
* We now use `polyfillSuper` for some prototype inheritance because the tests, and sometimes webpack, break on "cannot call EncryptionAlgorithm without 'new'". It's very much a workaround, and definitely not needed when we use real classes.

There is some import shuffling to help keep the imports clean - this was done by my IDE.
2019-12-17 15:14:22 -07:00
Travis Ralston 4dbda8dffd Merge pull request #1113 from matrix-org/travis/babel7-watcher
[BREAKING] Refactor the entire build process
2019-12-12 16:09:18 -07:00
Travis Ralston 01f32e0f45 Undo change to utils import 2019-12-12 14:14:19 -07:00
Travis Ralston 86c530e967 Leave the description alone 2019-12-12 10:27:02 -07:00
Travis Ralston dcd6626fe6 Fix readme for new lack of minification 2019-12-12 10:26:44 -07:00
Travis Ralston 601cefe975 Fix the release script for new build process
It doesn't seem to care what the version is, so just build the SDK normally.
2019-12-12 10:25:19 -07:00
Travis Ralston 1984cf02cf Merge branch 'develop' into travis/babel7-watcher 2019-12-11 17:22:42 -07:00
Travis Ralston 4bdabbfbe9 [BREAKING] Refactor the entire build process
For https://github.com/vector-im/riot-web/issues/8880

Features:
* Export modern JS
* Export typings
* Export source maps that actually mean something
* No longer supporting minified builds

This is a step towards being a boring SDK and not anticipating an install location. 

This commit requires a major version bump of the SDK.
2019-12-10 13:25:07 -07:00
137 changed files with 8938 additions and 4725 deletions
+2 -4
View File
@@ -1,11 +1,9 @@
{
"sourceMaps": true,
"presets": [
["@babel/preset-env", {
"targets": {
"browsers": [
"last 2 versions"
],
"node": 12
"node": 10
},
"modules": "commonjs"
}],
-34
View File
@@ -1,34 +0,0 @@
steps:
- label: ":eslint: Lint"
command:
- "yarn install"
- "yarn lint"
plugins:
- docker#v3.0.1:
image: "node:10"
- label: ":jest: Tests"
command:
- "yarn install"
- "yarn test"
plugins:
- docker#v3.0.1:
image: "node:10"
- label: "📃 Docs"
command:
- "yarn install"
- "yarn gendoc"
plugins:
- docker#v3.0.1:
image: "node:10"
- wait
- label: "🐴 Trigger matrix-react-sdk"
trigger: "matrix-react-sdk"
branches: "develop"
build:
branch: "develop"
message: "[js-sdk] ${BUILDKITE_MESSAGE}"
async: true
+324
View File
@@ -1,3 +1,327 @@
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)
* Move & upgrade babel runtime into dependencies (like it wants)
[\#1170](https://github.com/matrix-org/matrix-js-sdk/pull/1170)
* Add a prepare script
[\#1164](https://github.com/matrix-org/matrix-js-sdk/pull/1164)
Changes in [4.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v4.0.0-rc.1) (2020-01-20)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v3.0.0...v4.0.0-rc.1)
BREAKING CHANGES
================
* The js-sdk node module now exports ES6 rather than ES5. If you
wish to supports target that aren't compatible with ES6, you
will need to transpile the js-sdk to a suitable dialect.
All Changes
===========
* Convert secret storage to new account data API
[\#1154](https://github.com/matrix-org/matrix-js-sdk/pull/1154)
* Add v5 as a safe room version
[\#1157](https://github.com/matrix-org/matrix-js-sdk/pull/1157)
* Add API to get account data from server
[\#1153](https://github.com/matrix-org/matrix-js-sdk/pull/1153)
* Fix sourcemaps by refactoring the build system
[\#1151](https://github.com/matrix-org/matrix-js-sdk/pull/1151)
* record, report, and notify about olm errors
[\#1146](https://github.com/matrix-org/matrix-js-sdk/pull/1146)
* Send device messages for the same user in same API call.
[\#1148](https://github.com/matrix-org/matrix-js-sdk/pull/1148)
* Add an option to ignore unverified devices
[\#1150](https://github.com/matrix-org/matrix-js-sdk/pull/1150)
* Sign key backup with cross-signing key on upgrade
[\#1144](https://github.com/matrix-org/matrix-js-sdk/pull/1144)
* Emoji verification: Change name of 🔒 to lock
[\#1145](https://github.com/matrix-org/matrix-js-sdk/pull/1145)
* use a separate object for each encrypted content
[\#1147](https://github.com/matrix-org/matrix-js-sdk/pull/1147)
* Sourcemaps: develop -> feature branch
[\#1143](https://github.com/matrix-org/matrix-js-sdk/pull/1143)
* Use a safer import/export scheme for the ContentRepo utilities
[\#1134](https://github.com/matrix-org/matrix-js-sdk/pull/1134)
* Fix error handling in decryptGroupMessage
[\#1142](https://github.com/matrix-org/matrix-js-sdk/pull/1142)
* Add additional properties to package.json for riot-web's webpack
[\#1131](https://github.com/matrix-org/matrix-js-sdk/pull/1131)
* Fix import for indexeddb crypto store
[\#1133](https://github.com/matrix-org/matrix-js-sdk/pull/1133)
* Use the right request when creating clients
[\#1132](https://github.com/matrix-org/matrix-js-sdk/pull/1132)
* Target NodeJS 10, minified browser bundle, and other publishing/package
things
[\#1127](https://github.com/matrix-org/matrix-js-sdk/pull/1127)
* Re-focus sourcemap generation
[\#1126](https://github.com/matrix-org/matrix-js-sdk/pull/1126)
* Remove ancient polyfill for prototype inheritance
[\#1125](https://github.com/matrix-org/matrix-js-sdk/pull/1125)
* Remove "source-map-support" from tests because it makes sourcemaps worse
[\#1124](https://github.com/matrix-org/matrix-js-sdk/pull/1124)
* Remove ancient "use strict" annotations
[\#1123](https://github.com/matrix-org/matrix-js-sdk/pull/1123)
* Use ES6 imports/exports instead of older CommonJS ones
[\#1122](https://github.com/matrix-org/matrix-js-sdk/pull/1122)
* [BREAKING] Refactor the entire build process
[\#1113](https://github.com/matrix-org/matrix-js-sdk/pull/1113)
Changes in [3.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.0.0) (2020-01-13)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v3.0.0-rc.1...v3.0.0)
+10 -2
View File
@@ -36,8 +36,16 @@ minutes.
Code style
~~~~~~~~~~
The code-style for matrix-js-sdk is not formally documented, but contributors
are encouraged to read the code style document for matrix-react-sdk
The js-sdk aims to target TypeScript/ES6. All new files should be written in
TypeScript and existing files should use ES6 principles where possible.
Members should not be exported as a default export in general - it causes problems
with the architecture of the SDK (index file becomes less clear) and could
introduce naming problems (as default exports get aliased upon import). In
general, avoid using `export default`.
The remaining code-style for matrix-js-sdk is not formally documented, but
contributors are encouraged to read the code style document for matrix-react-sdk
(`<https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md>`_)
and follow the principles set out there.
+20 -16
View File
@@ -9,12 +9,16 @@ Quickstart
In a browser
------------
Download either the full or minified version from
Download the browser version from
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
``<script>`` to your page. There will be a global variable ``matrixcs``
attached to ``window`` through which you can access the SDK. See below for how to
include libolm to enable end-to-end-encryption.
The browser bundle supports recent versions of browsers. Typically this is ES2015
or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
[browserlists](https://github.com/browserslist/browserslist).
Please check [the working browser example](examples/browser) for more information.
In Node.js
@@ -22,13 +26,18 @@ In Node.js
Ensure you have the latest LTS version of Node.js installed.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://yarnpkg.com/docs/install/) if you do not have it already.
This SDK targets Node 10 for compatibility, which translates to ES6. If you're using
a bundler like webpack you'll likely have to transpile dependencies, including this
SDK, to match your target browsers.
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://yarnpkg.com/docs/install/)
if you do not have it already.
``yarn add matrix-js-sdk``
```javascript
var sdk = require("matrix-js-sdk");
var client = sdk.createClient("https://matrix.org");
import * as sdk from "matrix-js-sdk";
const client = sdk.createClient("https://matrix.org");
client.publicRooms(function(err, data) {
console.log("Public Rooms: %s", JSON.stringify(data));
});
@@ -59,7 +68,7 @@ client.once('sync', function(state, prevState, res) {
To send a message:
```javascript
var content = {
const content = {
"body": "message text",
"msgtype": "m.text"
};
@@ -191,10 +200,10 @@ This section provides some useful code snippets which demonstrate the
core functionality of the SDK. These examples assume the SDK is setup like this:
```javascript
var sdk = require("matrix-js-sdk");
var myUserId = "@example:localhost";
var myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
var matrixClient = sdk.createClient({
import * as sdk from "matrix-js-sdk";
const myUserId = "@example:localhost";
const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
const matrixClient = sdk.createClient({
baseUrl: "http://localhost:8008",
accessToken: myAccessToken,
userId: myUserId
@@ -247,11 +256,11 @@ Output:
```javascript
matrixClient.on("RoomState.members", function(event, state, member) {
var room = matrixClient.getRoom(state.roomId);
const room = matrixClient.getRoom(state.roomId);
if (!room) {
return;
}
var memberList = state.getMembers();
const memberList = state.getMembers();
console.log(room.name);
console.log(Array(room.name.length + 1).join("=")); // underline
for (var i = 0; i < memberList.length; i++) {
@@ -351,11 +360,6 @@ To build a browser version from scratch when developing::
$ yarn build
```
To constantly do builds when files are modified (using ``watchify``)::
```
$ yarn watch
```
To run tests (Jasmine)::
```
$ yarn test
-35
View File
@@ -1,35 +0,0 @@
var matrixcs = require("./lib/matrix");
const request = require('browser-request');
const queryString = require('qs');
matrixcs.request(function(opts, fn) {
// We manually fix the query string for browser-request because
// it doesn't correctly handle cases like ?via=one&via=two. Instead
// we mimic `request`'s query string interface to make it all work
// as expected.
// browser-request will happily take the constructed string as the
// query string without trying to modify it further.
opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions);
return request(opts, fn);
});
// just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled.
var indexedDB;
try {
indexedDB = global.indexedDB;
} catch(e) {}
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
if (indexedDB) {
matrixcs.setCryptoStoreFactory(
function() {
return new matrixcs.IndexedDBCryptoStore(
indexedDB, "matrix-js-sdk:crypto"
);
}
);
}
module.exports = matrixcs; // keep export for browserify package deps
global.matrixcs = matrixcs;
-1
View File
@@ -1,4 +1,3 @@
"use strict";
console.log("Loading browser sdk");
var client = matrixcs.createClient("http://matrix.org");
+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',
}
)
}
}
-2
View File
@@ -1,5 +1,3 @@
"use strict";
var myUserId = "@example:localhost";
var myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
var sdk = require("matrix-js-sdk");
-1
View File
@@ -1,4 +1,3 @@
"use strict";
console.log("Loading browser sdk");
var BASE_URL = "https://matrix.org";
var TOKEN = "accesstokengoeshere";
-6
View File
@@ -1,6 +0,0 @@
var matrixcs = require("./lib/matrix");
matrixcs.request(require("request"));
module.exports = matrixcs;
var utils = require("./lib/utils");
utils.runPolyfills();
+38 -40
View File
@@ -1,21 +1,24 @@
{
"name": "matrix-js-sdk",
"version": "3.0.0",
"version": "5.2.0-rc.1",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test:watch": "jest spec/ --coverage --testEnvironment node --watch",
"test": "jest spec/ --coverage --testEnvironment node",
"gendoc": "jsdoc -c jsdoc.json -P package.json",
"start": "yarn start:init && yarn start:watch",
"start:watch": "babel src -s -w --skip-initial-build -d lib --verbose --extensions \".ts,.js\"",
"start:init": "babel src -s -d lib --verbose --extensions \".ts,.js\"",
"prepare": "yarn build",
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
"clean": "rimraf lib dist",
"build": "babel src -s -d lib --verbose --extensions \".ts,.js\" && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && terser -c -m -o dist/browser-matrix.min.js --source-map \"content='dist/browser-matrix.js.map'\" dist/browser-matrix.js",
"dist": "yarn build",
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
"lint": "eslint --max-warnings 93 src spec",
"prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt"
"build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:compile-browser && yarn build:minify-browser && yarn build:types",
"build:types": "tsc --emitDeclarationOnly",
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
"gendoc": "jsdoc -c jsdoc.json -P package.json",
"lint": "yarn lint:types && yarn lint:ts && yarn lint:js",
"lint:js": "eslint --max-warnings 93 src spec",
"lint:types": "tsc --noEmit",
"lint:ts": "tslint --project ./tsconfig.json -t stylish",
"test": "jest spec/ --coverage --testEnvironment node",
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
},
"repository": {
"type": "git",
@@ -24,30 +27,26 @@
"keywords": [
"matrix-org"
],
"browser": "browser-index.js",
"main": "./lib/index.js",
"typings": "./lib/index.d.ts",
"browser": "./lib/browser-index.js",
"matrix_src_main": "./src/index.ts",
"matrix_src_browser": "./src/browser-index.js",
"author": "matrix.org",
"license": "Apache-2.0",
"files": [
".babelrc",
".eslintrc.js",
"spec/.eslintrc.js",
"lib",
"src",
"git-revision.txt",
"CHANGELOG.md",
"CONTRIBUTING.rst",
"LICENSE",
"README.md",
"RELEASING.md",
"examples",
"git-hooks",
"git-revision.txt",
"index.js",
"browser-index.js",
"lib",
"package.json",
"release.sh",
"spec",
"src"
"release.sh"
],
"dependencies": {
"@babel/runtime": "^7.8.3",
"another-json": "^0.2.0",
"browser-request": "^0.3.3",
"bs58": "^4.0.1",
@@ -64,35 +63,34 @@
"@babel/plugin-proposal-numeric-separator": "^7.7.4",
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
"@babel/plugin-transform-runtime": "^7.7.6",
"@babel/plugin-transform-runtime": "^7.8.3",
"@babel/preset-env": "^7.7.6",
"@babel/preset-typescript": "^7.7.4",
"@babel/register": "^7.7.4",
"@babel/runtime": "^7.7.6",
"@types/node": "12",
"babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0",
"babelify": "^10.0.0",
"better-docs": "^1.4.7",
"browserify": "^16.2.3",
"browserify-shim": "^3.8.13",
"browserify": "^16.5.0",
"eslint": "^5.12.0",
"eslint-config-google": "^0.7.1",
"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",
"rimraf": "^3.0.0",
"source-map-support": "^0.5.13",
"sourceify": "^1.0.0",
"terser": "^4.3.8",
"typescript": "^3.7.3",
"watchify": "^3.11.1"
"terser": "^4.4.3",
"tsify": "^4.0.1",
"tslint": "^5.20.1",
"typescript": "^3.7.3"
},
"browserify": {
"transform": [
"sourceify"
]
"jest": {
"testEnvironment": "node"
}
}
+11 -2
View File
@@ -1,6 +1,6 @@
#!/bin/bash
#
# Script to perform a release of matrix-js-sdk.
# Script to perform a release of matrix-js-sdk and downstream projects.
#
# Requires:
# github-changelog-generator; install via:
@@ -9,6 +9,8 @@
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
# npm; typically installed by Node.js
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
#
# Note: this script is also used to release matrix-react-sdk and riot-web.
set -e
@@ -294,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"
+2 -3
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
@@ -18,7 +19,7 @@ limitations under the License.
* A mock implementation of the webstorage api
* @constructor
*/
function MockStorageApi() {
export function MockStorageApi() {
this.data = {};
this.keys = [];
this.length = 0;
@@ -52,5 +53,3 @@ MockStorageApi.prototype = {
},
};
/** */
module.exports = MockStorageApi;
+12 -12
View File
@@ -16,16 +16,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
// load olm before the sdk if possible
import './olm-loader';
import sdk from '..';
import testUtils from './test-utils';
import MockHttpBackend from 'matrix-mock-request';
import LocalStorageCryptoStore from '../lib/crypto/store/localStorage-crypto-store';
import logger from '../lib/logger';
import {LocalStorageCryptoStore} from '../src/crypto/store/localStorage-crypto-store';
import {logger} from '../src/logger';
import {WebStorageSessionStore} from "../src/store/session/webstorage";
import {syncPromise} from "./test-utils";
import {createClient} from "../src/matrix";
import {MockStorageApi} from "./MockStorageApi";
/**
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
@@ -39,16 +39,16 @@ import logger from '../lib/logger';
* session store. If undefined, we will create a MockStorageApi.
* @param {object} options additional options to pass to the client
*/
export default function TestClient(
export function TestClient(
userId, deviceId, accessToken, sessionStoreBackend, options,
) {
this.userId = userId;
this.deviceId = deviceId;
if (sessionStoreBackend === undefined) {
sessionStoreBackend = new testUtils.MockStorageApi();
sessionStoreBackend = new MockStorageApi();
}
const sessionStore = new sdk.WebStorageSessionStore(sessionStoreBackend);
const sessionStore = new WebStorageSessionStore(sessionStoreBackend);
this.httpBackend = new MockHttpBackend();
@@ -65,7 +65,7 @@ export default function TestClient(
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
options.cryptoStore = this.cryptoStore;
}
this.client = sdk.createClient(options);
this.client = createClient(options);
this.deviceKeys = null;
this.oneTimeKeys = {};
@@ -97,7 +97,7 @@ TestClient.prototype.start = function() {
return Promise.all([
this.httpBackend.flushAllExpected(),
testUtils.syncPromise(this.client),
syncPromise(this.client),
]).then(() => {
logger.log(this + ': started');
});
@@ -225,7 +225,7 @@ TestClient.prototype.flushSync = function() {
logger.log(`${this}: flushSync`);
return Promise.all([
this.httpBackend.flush('/sync', 1),
testUtils.syncPromise(this.client),
syncPromise(this.client),
]).then(() => {
logger.log(`${this}: flushSync completed`);
});
+4 -3
View File
@@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
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.
@@ -15,9 +16,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import TestClient from '../TestClient';
import testUtils from '../test-utils';
import logger from '../../lib/logger';
import {TestClient} from '../TestClient';
import * as testUtils from '../test-utils';
import {logger} from '../../src/logger';
const ROOM_ID = "!room:id";
+7 -9
View File
@@ -2,6 +2,7 @@
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018 New Vector Ltd
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.
@@ -24,17 +25,14 @@ limitations under the License.
* See also `megolm.spec.js`.
*/
"use strict";
import 'source-map-support/register';
// load olm before the sdk if possible
import '../olm-loader';
const sdk = require("../..");
const utils = require("../../lib/utils");
const testUtils = require("../test-utils");
const TestClient = require('../TestClient').default;
import logger from '../../lib/logger';
import {logger} from '../../src/logger';
import * as testUtils from "../test-utils";
import * as utils from "../../src/utils";
import {TestClient} from "../TestClient";
import {CRYPTO_ENABLED} from "../../src/client";
let aliTestClient;
const roomId = "!room:localhost";
@@ -400,7 +398,7 @@ function firstSync(testClient) {
describe("MatrixClient crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
if (!CRYPTO_ENABLED) {
return;
}
+5 -13
View File
@@ -1,24 +1,16 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
import * as utils from "../test-utils";
import {TestClient} from "../TestClient";
describe("MatrixClient events", function() {
const baseUrl = "http://localhost.or.something";
let client;
let httpBackend;
const selfUserId = "@alice:localhost";
const selfAccessToken = "aseukfgwef";
beforeEach(function() {
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: selfUserId,
accessToken: selfAccessToken,
});
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
client = testClient.client;
httpBackend = testClient.httpBackend;
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
+26 -36
View File
@@ -1,12 +1,8 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
const EventTimeline = sdk.EventTimeline;
import logger from '../../lib/logger';
import * as utils from "../test-utils";
import {EventTimeline} from "../../src/matrix";
import {logger} from "../../src/logger";
import {TestClient} from "../TestClient";
const baseUrl = "http://localhost.or.something";
const userId = "@alice:localhost";
const userName = "Alice";
const accessToken = "aseukfgwef";
@@ -103,8 +99,9 @@ describe("getEventTimeline support", function() {
let client;
beforeEach(function() {
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
const testClient = new TestClient(userId, "DEVICE", accessToken);
client = testClient.client;
httpBackend = testClient.httpBackend;
});
afterEach(function() {
@@ -115,12 +112,6 @@ describe("getEventTimeline support", function() {
});
it("timeline support must be enabled to work", function() {
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
});
return startClient(httpBackend, client).then(function() {
const room = client.getRoom(roomId);
const timelineSet = room.getTimelineSets()[0];
@@ -131,12 +122,15 @@ describe("getEventTimeline support", function() {
});
it("timeline support works when enabled", function() {
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
timelineSupport: true,
});
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{timelineSupport: true},
);
client = testClient.client;
httpBackend = testClient.httpBackend;
return startClient(httpBackend, client).then(() => {
const room = client.getRoom(roomId);
@@ -151,11 +145,7 @@ describe("getEventTimeline support", function() {
it("scrollback should be able to scroll back to before a gappy /sync",
function() {
// need a client with timelineSupport disabled to make this work
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
});
let room;
return startClient(httpBackend, client).then(function() {
@@ -223,15 +213,15 @@ describe("MatrixClient event timelines", function() {
let httpBackend = null;
beforeEach(function() {
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
timelineSupport: true,
});
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{timelineSupport: true},
);
client = testClient.client;
httpBackend = testClient.httpBackend;
return startClient(httpBackend, client);
});
+8 -24
View File
@@ -1,39 +1,23 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const publicGlobals = require("../../lib/matrix");
const Room = publicGlobals.Room;
const MemoryStore = publicGlobals.MemoryStore;
const Filter = publicGlobals.Filter;
const utils = require("../test-utils");
const MockStorageApi = require("../MockStorageApi");
import * as utils from "../test-utils";
import {CRYPTO_ENABLED} from "../../src/client";
import {Filter, MemoryStore, Room} from "../../src/matrix";
import {TestClient} from "../TestClient";
describe("MatrixClient", function() {
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
let store = null;
let sessionStore = null;
const userId = "@alice:localhost";
const accessToken = "aseukfgwef";
beforeEach(function() {
httpBackend = new HttpBackend();
store = new MemoryStore();
const mockStorage = new MockStorageApi();
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
deviceId: "aliceDevice",
accessToken: accessToken,
const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, {
store: store,
sessionStore: sessionStore,
});
httpBackend = testClient.httpBackend;
client = testClient.client;
});
afterEach(function() {
@@ -303,7 +287,7 @@ describe("MatrixClient", function() {
describe("downloadKeys", function() {
if (!sdk.CRYPTO_ENABLED) {
if (!CRYPTO_ENABLED) {
return;
}
+7 -8
View File
@@ -1,9 +1,8 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const MatrixClient = sdk.MatrixClient;
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
import * as utils from "../test-utils";
import HttpBackend from "matrix-mock-request";
import {MatrixClient} from "../../src/matrix";
import {MatrixScheduler} from "../../src/scheduler";
import {MemoryStore} from "../../src/store/memory";
describe("MatrixClient opts", function() {
const baseUrl = "http://localhost.or.something";
@@ -71,7 +70,7 @@ describe("MatrixClient opts", function() {
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
scheduler: new sdk.MatrixScheduler(),
scheduler: new MatrixScheduler(),
});
});
@@ -124,7 +123,7 @@ describe("MatrixClient opts", function() {
beforeEach(function() {
client = new MatrixClient({
request: httpBackend.requestFn,
store: new sdk.MemoryStore(),
store: new MemoryStore(),
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
+15 -17
View File
@@ -1,12 +1,9 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const EventStatus = sdk.EventStatus;
import {EventStatus} from "../../src/matrix";
import {MatrixScheduler} from "../../src/scheduler";
import {Room} from "../../src/models/room";
import {TestClient} from "../TestClient";
describe("MatrixClient retrying", function() {
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
let scheduler;
@@ -16,16 +13,17 @@ describe("MatrixClient retrying", function() {
let room;
beforeEach(function() {
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
scheduler = new sdk.MatrixScheduler();
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
scheduler: scheduler,
});
room = new sdk.Room(roomId);
scheduler = new MatrixScheduler();
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{scheduler},
);
httpBackend = testClient.httpBackend;
client = testClient.client;
room = new Room(roomId);
client.store.storeRoom(room);
});
+15 -16
View File
@@ -1,12 +1,9 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const EventStatus = sdk.EventStatus;
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
import * as utils from "../test-utils";
import {EventStatus} from "../../src/models/event";
import {TestClient} from "../TestClient";
describe("MatrixClient room timelines", function() {
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
const userId = "@alice:localhost";
@@ -101,15 +98,17 @@ describe("MatrixClient room timelines", function() {
}
beforeEach(function() {
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: userId,
accessToken: accessToken,
// these tests should work with or without timelineSupport
timelineSupport: true,
});
// these tests should work with or without timelineSupport
const testClient = new TestClient(
userId,
"DEVICE",
accessToken,
undefined,
{timelineSupport: true},
);
httpBackend = testClient.httpBackend;
client = testClient.client;
setNextSyncData();
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
+7 -15
View File
@@ -1,13 +1,9 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const HttpBackend = require("matrix-mock-request");
const utils = require("../test-utils");
const MatrixEvent = sdk.MatrixEvent;
const EventTimeline = sdk.EventTimeline;
import {MatrixEvent} from "../../src/models/event";
import {EventTimeline} from "../../src/models/event-timeline";
import * as utils from "../test-utils";
import {TestClient} from "../TestClient";
describe("MatrixClient syncing", function() {
const baseUrl = "http://localhost.or.something";
let client = null;
let httpBackend = null;
const selfUserId = "@alice:localhost";
@@ -20,13 +16,9 @@ describe("MatrixClient syncing", function() {
const roomTwo = "!bar:localhost";
beforeEach(function() {
httpBackend = new HttpBackend();
sdk.request(httpBackend.requestFn);
client = sdk.createClient({
baseUrl: baseUrl,
userId: selfUserId,
accessToken: selfAccessToken,
});
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
httpBackend = testClient.httpBackend;
client = testClient.client;
httpBackend.when("GET", "/pushrules").respond(200, {});
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
});
+12 -8
View File
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
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.
@@ -14,14 +15,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
const anotherjson = require('another-json');
const utils = require('../../lib/utils');
const testUtils = require('../test-utils');
const TestClient = require('../TestClient').default;
import logger from '../../lib/logger';
import anotherjson from "another-json";
import * as utils from "../../src/utils";
import * as testUtils from "../test-utils";
import {TestClient} from "../TestClient";
import {logger} from "../../src/logger";
const ROOM_ID = "!room:id";
@@ -617,6 +615,9 @@ describe("megolm", function() {
).respond(200, {
event_id: '$event_id',
});
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
).respond(200, {});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
@@ -714,6 +715,9 @@ describe("megolm", function() {
event_id: '$event_id',
};
});
aliceTestClient.httpBackend.when(
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
).respond(200, {});
return Promise.all([
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
+2 -1
View File
@@ -1,5 +1,6 @@
/*
Copyright 2017 Vector creations Ltd
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.
@@ -14,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../lib/logger';
import {logger} from '../src/logger';
// try to load the olm library.
try {
+28 -30
View File
@@ -1,10 +1,8 @@
"use strict";
// load olm before the sdk if possible
import './olm-loader';
import logger from '../lib/logger';
import sdk from '..';
const MatrixEvent = sdk.MatrixEvent;
import {logger} from '../src/logger';
import {MatrixEvent} from "../src/models/event";
/**
* Return a promise that is resolved when the client next emits a
@@ -13,7 +11,7 @@ const MatrixEvent = sdk.MatrixEvent;
* @param {Number=} count Number of syncs to wait for (default 1)
* @return {Promise} Resolves once the client has emitted a SYNCING event
*/
module.exports.syncPromise = function(client, count) {
export function syncPromise(client, count) {
if (count === undefined) {
count = 1;
}
@@ -24,7 +22,7 @@ module.exports.syncPromise = function(client, count) {
const p = new Promise((resolve, reject) => {
const cb = (state) => {
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
if (state == 'SYNCING') {
if (state === 'SYNCING') {
resolve();
} else {
client.once('sync', cb);
@@ -34,9 +32,9 @@ module.exports.syncPromise = function(client, count) {
});
return p.then(() => {
return module.exports.syncPromise(client, count-1);
return syncPromise(client, count-1);
});
};
}
/**
* Create a spy for an object and automatically spy its methods.
@@ -44,7 +42,7 @@ module.exports.syncPromise = function(client, count) {
* @param {string} name The name of the class
* @return {Object} An instantiated object with spied methods/properties.
*/
module.exports.mock = function(constr, name) {
export function mock(constr, name) {
// Based on
// http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
const HelperConstr = new Function(); // jshint ignore:line
@@ -65,7 +63,7 @@ module.exports.mock = function(constr, name) {
}
}
return result;
};
}
/**
* Create an Event.
@@ -78,7 +76,7 @@ module.exports.mock = function(constr, name) {
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object} a JSON object representing this event.
*/
module.exports.mkEvent = function(opts) {
export function mkEvent(opts) {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
@@ -97,14 +95,14 @@ module.exports.mkEvent = function(opts) {
event.state_key = "";
}
return opts.event ? new MatrixEvent(event) : event;
};
}
/**
* Create an m.presence event.
* @param {Object} opts Values for the presence.
* @return {Object|MatrixEvent} The event
*/
module.exports.mkPresence = function(opts) {
export function mkPresence(opts) {
if (!opts.user) {
throw new Error("Missing user");
}
@@ -120,7 +118,7 @@ module.exports.mkPresence = function(opts) {
},
};
return opts.event ? new MatrixEvent(event) : event;
};
}
/**
* Create an m.room.member event.
@@ -135,7 +133,7 @@ module.exports.mkPresence = function(opts) {
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
module.exports.mkMembership = function(opts) {
export function mkMembership(opts) {
opts.type = "m.room.member";
if (!opts.skey) {
opts.skey = opts.sender || opts.user;
@@ -152,8 +150,8 @@ module.exports.mkMembership = function(opts) {
if (opts.url) {
opts.content.avatar_url = opts.url;
}
return module.exports.mkEvent(opts);
};
return mkEvent(opts);
}
/**
* Create an m.room.message event.
@@ -164,7 +162,7 @@ module.exports.mkMembership = function(opts) {
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
module.exports.mkMessage = function(opts) {
export function mkMessage(opts) {
opts.type = "m.room.message";
if (!opts.msg) {
opts.msg = "Random->" + Math.random();
@@ -176,8 +174,8 @@ module.exports.mkMessage = function(opts) {
msgtype: "m.text",
body: opts.msg,
};
return module.exports.mkEvent(opts);
};
return mkEvent(opts);
}
/**
@@ -185,10 +183,10 @@ module.exports.mkMessage = function(opts) {
*
* @constructor
*/
module.exports.MockStorageApi = function() {
export function MockStorageApi() {
this.data = {};
};
module.exports.MockStorageApi.prototype = {
}
MockStorageApi.prototype = {
get length() {
return Object.keys(this.data).length;
},
@@ -213,7 +211,7 @@ module.exports.MockStorageApi.prototype = {
* @param {MatrixEvent} event
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
*/
module.exports.awaitDecryption = function(event) {
export function awaitDecryption(event) {
if (!event.isBeingDecrypted()) {
return Promise.resolve(event);
}
@@ -226,19 +224,19 @@ module.exports.awaitDecryption = function(event) {
resolve(ev);
});
});
};
}
const HttpResponse = module.exports.HttpResponse = function(
export function HttpResponse(
httpLookups, acceptKeepalives, ignoreUnhandledSync,
) {
this.httpLookups = httpLookups;
this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives;
this.ignoreUnhandledSync = ignoreUnhandledSync;
this.pendingLookup = null;
};
}
HttpResponse.prototype.request = function HttpResponse(
HttpResponse.prototype.request = function(
cb, method, path, qp, data, prefix,
) {
if (path === HttpResponse.KEEP_ALIVE_PATH && this.acceptKeepalives) {
@@ -351,7 +349,7 @@ HttpResponse.defaultResponses = function(userId) {
];
};
module.exports.setHttpResponses = function setHttpResponses(
export function setHttpResponses(
client, responses, acceptKeepalives, ignoreUnhandledSyncs,
) {
const httpResponseObj = new HttpResponse(
@@ -367,4 +365,4 @@ module.exports.setHttpResponses = function setHttpResponses(
client._http.authedRequestWithPrefix.mockImplementation(httpReq);
client._http.requestWithPrefix.mockImplementation(httpReq);
client._http.request.mockImplementation(httpReq);
};
}
+3 -7
View File
@@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
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.
@@ -13,15 +14,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const AutoDiscovery = sdk.AutoDiscovery;
import MockHttpBackend from "matrix-mock-request";
import * as sdk from "../../src";
import {AutoDiscovery} from "../../src/autodiscovery";
describe("AutoDiscovery", function() {
let httpBackend = null;
+12 -14
View File
@@ -1,6 +1,4 @@
"use strict";
import 'source-map-support/register';
const ContentRepo = require("../../lib/content-repo");
import {getHttpUriForMxc, getIdenticonUri} from "../../src/content-repo";
describe("ContentRepo", function() {
const baseUrl = "https://my.home.server";
@@ -9,7 +7,7 @@ describe("ContentRepo", function() {
it("should do nothing to HTTP URLs when allowing direct links", function() {
const httpUrl = "http://example.com/image.jpeg";
expect(
ContentRepo.getHttpUriForMxc(
getHttpUriForMxc(
baseUrl, httpUrl, undefined, undefined, undefined, true,
),
).toEqual(httpUrl);
@@ -17,25 +15,25 @@ describe("ContentRepo", function() {
it("should return the empty string HTTP URLs by default", function() {
const httpUrl = "http://example.com/image.jpeg";
expect(ContentRepo.getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
expect(getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
});
it("should return a download URL if no width/height/resize are specified",
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
);
});
it("should return the empty string for null input", function() {
expect(ContentRepo.getHttpUriForMxc(null)).toEqual("");
expect(getHttpUriForMxc(null)).toEqual("");
});
it("should return a thumbnail URL if a width/height/resize is specified",
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32&height=64&method=crop",
);
@@ -44,7 +42,7 @@ describe("ContentRepo", function() {
it("should put fragments from mxc:// URIs after any query parameters",
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32#automade",
);
@@ -53,7 +51,7 @@ describe("ContentRepo", function() {
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
);
});
@@ -61,25 +59,25 @@ describe("ContentRepo", function() {
describe("getIdenticonUri", function() {
it("should do nothing for null input", function() {
expect(ContentRepo.getIdenticonUri(null)).toEqual(null);
expect(getIdenticonUri(null)).toEqual(null);
});
it("should set w/h by default to 96", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual(
expect(getIdenticonUri(baseUrl, "foobar")).toEqual(
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
"?width=96&height=96",
);
});
it("should be able to set custom w/h", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
expect(getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
"?width=32&height=64",
);
});
it("should URL encode the identicon string", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
expect(getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/unstable/identicon/foo%23bar" +
"?width=32&height=64",
);
+11 -17
View File
@@ -1,26 +1,20 @@
import 'source-map-support/register';
import '../olm-loader';
import Crypto from '../../lib/crypto';
import WebStorageSessionStore from '../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../MockStorageApi';
import TestClient from '../TestClient';
import {MatrixEvent} from '../../lib/models/event';
import Room from '../../lib/models/room';
import olmlib from '../../lib/crypto/olmlib';
import {Crypto} from "../../src/crypto";
import {WebStorageSessionStore} from "../../src/store/session/webstorage";
import {MemoryCryptoStore} from "../../src/crypto/store/memory-crypto-store";
import {MockStorageApi} from "../MockStorageApi";
import {TestClient} from "../TestClient";
import {MatrixEvent} from "../../src/models/event";
import {Room} from "../../src/models/room";
import * as olmlib from "../../src/crypto/olmlib";
import {sleep} from "../../src/utils";
const EventEmitter = require("events").EventEmitter;
const sdk = require("../..");
import {EventEmitter} from "events";
import {CRYPTO_ENABLED} from "../../src/client";
const Olm = global.Olm;
describe("Crypto", function() {
if (!sdk.CRYPTO_ENABLED) {
if (!CRYPTO_ENABLED) {
return;
}
+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);
});
});
+5 -4
View File
@@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
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.
@@ -15,10 +16,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import DeviceList from '../../../lib/crypto/DeviceList';
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
import utils from '../../../lib/utils';
import logger from '../../../lib/logger';
import {logger} from "../../../src/logger";
import * as utils from "../../../src/utils";
import {MemoryCryptoStore} from "../../../src/crypto/store/memory-crypto-store";
import {DeviceList} from "../../../src/crypto/DeviceList";
const signedDeviceList = {
"failures": {},
+361 -12
View File
@@ -1,15 +1,16 @@
import '../../../olm-loader';
import * as algorithms from "../../../../src/crypto/algorithms";
import {MemoryCryptoStore} from "../../../../src/crypto/store/memory-crypto-store";
import {MockStorageApi} from "../../../MockStorageApi";
import * as testUtils from "../../../test-utils";
import {OlmDevice} from "../../../../src/crypto/OlmDevice";
import {Crypto} from "../../../../src/crypto";
import {logger} from "../../../../src/logger";
import {MatrixEvent} from "../../../../src/models/event";
import {TestClient} from "../../../TestClient";
import {Room} from "../../../../src/models/room";
import * as olmlib from "../../../../src/crypto/olmlib";
import sdk from '../../../..';
import algorithms from '../../../../lib/crypto/algorithms';
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../../MockStorageApi';
import testUtils from '../../../test-utils';
import OlmDevice from '../../../../lib/crypto/OlmDevice';
import Crypto from '../../../../lib/crypto';
import logger from '../../../../lib/logger';
const MatrixEvent = sdk.MatrixEvent;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
@@ -296,6 +297,10 @@ describe("MegolmDecryption", function() {
},
}));
mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false,
});
const megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
@@ -319,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();
@@ -342,4 +347,348 @@ describe("MegolmDecryption", function() {
expect(ct2.session_id).toEqual(ct1.session_id);
});
});
it("notifies devices that have been blocked", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const bobClient1 = (new TestClient(
"@bob:example.com", "bobdevice1",
)).client;
const bobClient2 = (new TestClient(
"@bob:example.com", "bobdevice2",
)).client;
await Promise.all([
aliceClient.initCrypto(),
bobClient1.initCrypto(),
bobClient2.initCrypto(),
]);
const aliceDevice = aliceClient._crypto._olmDevice;
const bobDevice1 = bobClient1._crypto._olmDevice;
const bobDevice2 = bobClient2._crypto._olmDevice;
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const room = new Room(roomId, aliceClient, "@alice:example.com", {});
room.getEncryptionTargetMembers = async function() {
return [{userId: "@bob:example.com"}];
};
room.setBlacklistUnverifiedDevices(true);
aliceClient.store.storeRoom(room);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
const BOB_DEVICES = {
bobdevice1: {
user_id: "@bob:example.com",
device_id: "bobdevice1",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice1.deviceEd25519Key,
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key,
},
verified: 0,
},
bobdevice2: {
user_id: "@bob:example.com",
device_id: "bobdevice2",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice2.deviceEd25519Key,
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key,
},
verified: -1,
},
};
aliceClient._crypto._deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
return this._getDevicesFromStore(userIds);
};
let run = false;
aliceClient.sendToDevice = async (msgtype, contentMap) => {
run = true;
expect(msgtype).toBe("org.matrix.room_key.withheld");
delete contentMap["@bob:example.com"].bobdevice1.session_id;
delete contentMap["@bob:example.com"].bobdevice2.session_id;
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice1: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: 'm.unverified',
reason:
'The sender has disabled encrypting to unverified devices.',
sender_key: aliceDevice.deviceCurve25519Key,
},
bobdevice2: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
code: 'm.blacklisted',
reason: 'The sender has blocked you.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
});
};
const event = new MatrixEvent({
type: "m.room.message",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$event",
content: {
msgtype: "m.text",
body: "secret",
},
});
await aliceClient._crypto.encryptEvent(event, room);
expect(run).toBe(true);
aliceClient.stopClient();
bobClient1.stopClient();
bobClient2.stopClient();
});
it("notifies devices when unable to create olm session", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await Promise.all([
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const aliceDevice = aliceClient._crypto._olmDevice;
const bobDevice = bobClient._crypto._olmDevice;
const encryptionCfg = {
"algorithm": "m.megolm.v1.aes-sha2",
};
const roomId = "!someroom";
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
aliceClient.store.storeRoom(aliceRoom);
bobClient.store.storeRoom(bobRoom);
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
await bobClient.setRoomEncryption(roomId, encryptionCfg);
aliceRoom.getEncryptionTargetMembers = async () => {
return [
{
userId: "@alice:example.com",
membership: "join",
},
{
userId: "@bob:example.com",
membership: "join",
},
];
};
const BOB_DEVICES = {
bobdevice: {
user_id: "@bob:example.com",
device_id: "bobdevice",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:bobdevice": bobDevice.deviceEd25519Key,
"curve25519:bobdevice": bobDevice.deviceCurve25519Key,
},
known: true,
verified: 1,
},
};
aliceClient._crypto._deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
return this._getDevicesFromStore(userIds);
};
aliceClient.claimOneTimeKeys = async () => {
// Bob has no one-time keys
return {
one_time_keys: {},
};
};
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",
sender: "@alice:example.com",
room_id: roomId,
event_id: "$event",
content: {},
});
await aliceClient._crypto.encryptEvent(event, aliceRoom);
await sendPromise;
});
it("throws an error describing why it doesn't have a key", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await Promise.all([
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const bobDevice = bobClient._crypto._olmDevice;
const roomId = "!someroom";
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
type: "org.matrix.room_key.withheld",
sender: "@bob:example.com",
content: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
session_id: "session_id",
sender_key: bobDevice.deviceCurve25519Key,
code: "m.blacklisted",
reason: "You have been blocked",
},
}));
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
room_id: roomId,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "blablabla",
device_id: "bobdevice",
sender_key: bobDevice.deviceCurve25519Key,
session_id: "session_id",
},
}))).rejects.toThrow("The sender has blocked you.");
});
it("throws an error describing the lack of an olm session", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await Promise.all([
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
aliceClient._crypto.downloadKeys = async () => {};
const bobDevice = bobClient._crypto._olmDevice;
const roomId = "!someroom";
const now = Date.now();
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
type: "org.matrix.room_key.withheld",
sender: "@bob:example.com",
content: {
algorithm: "m.megolm.v1.aes-sha2",
room_id: roomId,
session_id: "session_id",
sender_key: bobDevice.deviceCurve25519Key,
code: "m.no_olm",
reason: "Unable to establish a secure channel.",
},
}));
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
room_id: roomId,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "blablabla",
device_id: "bobdevice",
sender_key: bobDevice.deviceCurve25519Key,
session_id: "session_id",
},
origin_server_ts: now,
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
});
it("throws an error to indicate a wedged olm session", async function() {
const aliceClient = (new TestClient(
"@alice:example.com", "alicedevice",
)).client;
const bobClient = (new TestClient(
"@bob:example.com", "bobdevice",
)).client;
await Promise.all([
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
const bobDevice = bobClient._crypto._olmDevice;
aliceClient._crypto.downloadKeys = async () => {};
const roomId = "!someroom";
const now = Date.now();
// pretend we got an event that we can't decrypt
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
content: {
msgtype: "m.bad.encrypted",
algorithm: "m.megolm.v1.aes-sha2",
session_id: "session_id",
sender_key: bobDevice.deviceCurve25519Key,
},
}));
await new Promise((resolve) => {
setTimeout(resolve, 100);
});
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
type: "m.room.encrypted",
sender: "@bob:example.com",
event_id: "$event",
room_id: roomId,
content: {
algorithm: "m.megolm.v1.aes-sha2",
ciphertext: "blablabla",
device_id: "bobdevice",
sender_key: bobDevice.deviceCurve25519Key,
session_id: "session_id",
},
origin_server_ts: now,
}))).rejects.toThrow("The secure channel with the sender was corrupted.");
});
});
+62 -9
View File
@@ -1,5 +1,6 @@
/*
Copyright 2018,2019 New Vector Ltd
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.
@@ -15,14 +16,12 @@ limitations under the License.
*/
import '../../../olm-loader';
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../../MockStorageApi';
import logger from '../../../../lib/logger';
import OlmDevice from '../../../../lib/crypto/OlmDevice';
import olmlib from '../../../../lib/crypto/olmlib';
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
import {MemoryCryptoStore} from "../../../../src/crypto/store/memory-crypto-store";
import {MockStorageApi} from "../../../MockStorageApi";
import {logger} from "../../../../src/logger";
import {OlmDevice} from "../../../../src/crypto/OlmDevice";
import * as olmlib from "../../../../src/crypto/olmlib";
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
function makeOlmDevice() {
const mockStorage = new MockStorageApi();
@@ -42,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;
@@ -82,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
+38 -14
View File
@@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
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.
@@ -15,23 +16,20 @@ limitations under the License.
*/
import '../../olm-loader';
import sdk from '../../..';
import algorithms from '../../../lib/crypto/algorithms';
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
import MockStorageApi from '../../MockStorageApi';
import testUtils from '../../test-utils';
import OlmDevice from '../../../lib/crypto/OlmDevice';
import Crypto from '../../../lib/crypto';
import logger from '../../../lib/logger';
import olmlib from '../../../lib/crypto/olmlib';
import {logger} from "../../../src/logger";
import * as olmlib from "../../../src/crypto/olmlib";
import {MatrixClient} from "../../../src/client";
import {MatrixEvent} from "../../../src/models/event";
import * as algorithms from "../../../src/crypto/algorithms";
import {WebStorageSessionStore} from "../../../src/store/session/webstorage";
import {MemoryCryptoStore} from "../../../src/crypto/store/memory-crypto-store";
import {MockStorageApi} from "../../MockStorageApi";
import * as testUtils from "../../test-utils";
import {OlmDevice} from "../../../src/crypto/OlmDevice";
import {Crypto} from "../../../src/crypto";
const Olm = global.Olm;
const MatrixClient = sdk.MatrixClient;
const MatrixEvent = sdk.MatrixEvent;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const ROOM_ID = '!ROOM:ID';
@@ -543,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();
});
});
});
+6 -10
View File
@@ -16,13 +16,9 @@ limitations under the License.
*/
import '../../olm-loader';
import anotherjson from 'another-json';
import olmlib from '../../../lib/crypto/olmlib';
import TestClient from '../../TestClient';
import * as olmlib from "../../../src/crypto/olmlib";
import {TestClient} from '../../TestClient';
import {HttpResponse, setHttpResponses} from '../../test-utils';
async function makeTestClient(userInfo, options, keys) {
@@ -178,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,
@@ -241,7 +237,7 @@ describe("Cross Signing", function() {
},
{
method: "POST",
path: "/keys/upload/Osborne2",
path: "/keys/upload",
data: {
one_time_key_counts: {
curve25519: 100,
@@ -433,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,
@@ -491,7 +487,7 @@ describe("Cross Signing", function() {
},
{
method: "POST",
path: "/keys/upload/Osborne2",
path: "/keys/upload",
data: {
one_time_key_counts: {
curve25519: 100,
+53 -21
View File
@@ -15,22 +15,37 @@ limitations under the License.
*/
import '../../olm-loader';
import * as olmlib from "../../../src/crypto/olmlib";
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 { MatrixEvent } from '../../../lib/models/event';
import { SECRET_STORAGE_ALGORITHM_V1 } from '../../../lib/crypto/SecretStorage';
import * as utils from "../../../src/utils";
import olmlib from '../../../lib/crypto/olmlib';
import TestClient from '../../TestClient';
import { makeTestClients } from './verification/util';
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,
)).client;
// Make it seem as if we've synced and thus the store can be trusted to
// contain valid account data.
client.isInitialSyncComplete = function() {
return true;
};
await client.initCrypto();
// No need to download keys for these tests
client._crypto.downloadKeys = async function() {};
return client;
}
@@ -45,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();
@@ -63,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(
@@ -94,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');
@@ -106,11 +119,11 @@ describe("Secrets", function() {
}),
]);
expect(secretStorage.isStored("foo")).toBe(false);
expect(await secretStorage.isStored("foo")).toBeFalsy();
await secretStorage.store("foo", "bar", ["abc"]);
expect(secretStorage.isStored("foo")).toBe(true);
expect(await secretStorage.isStored("foo")).toBeTruthy();
expect(await secretStorage.get("foo")).toBe("bar");
expect(getKey).toHaveBeenCalled();
@@ -143,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"},
@@ -150,6 +170,7 @@ describe("Secrets", function() {
cryptoCallbacks: {
getCrossSigningKey: t => keys[t],
saveCrossSigningKeys: k => keys = k,
getSecretStorageKey: getKey,
},
},
);
@@ -164,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
@@ -246,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 () => {};
@@ -271,8 +303,8 @@ describe("Secrets", function() {
const secretStorage = bob._crypto._secretStorage;
expect(crossSigning.getId()).toBeTruthy();
expect(crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
expect(secretStorage.hasKey()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
});
it("bootstraps when cross-signing keys in secret storage", async function() {
@@ -287,8 +319,8 @@ describe("Secrets", function() {
},
{
cryptoCallbacks: {
getSecretStorageKey: request => {
const defaultKeyId = bob.getDefaultSecretStorageKeyId();
getSecretStorageKey: async request => {
const defaultKeyId = await bob.getDefaultSecretStorageKeyId();
expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
return [defaultKeyId, storagePrivateKey];
},
@@ -327,7 +359,7 @@ describe("Secrets", function() {
await bob.bootstrapSecretStorage();
expect(crossSigning.getId()).toBeTruthy();
expect(crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
expect(secretStorage.hasKey()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
});
});
@@ -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);
});
});
+10 -129
View File
@@ -1,5 +1,6 @@
/*
Copyright 2018-2019 New Vector Ltd
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.
@@ -13,17 +14,8 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../../../../lib/logger';
try {
global.Olm = require('olm');
} catch (e) {
logger.warn("unable to run device verification tests: libolm not available");
}
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
import {ShowQRCode, ScanQRCode} from '../../../../lib/crypto/verification/QRCode';
import "../../../olm-loader";
import {logger} from "../../../../src/logger";
const Olm = global.Olm;
@@ -37,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.
});
});
});
+16 -14
View File
@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
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.
@@ -13,32 +14,31 @@ 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 logger from '../../../../lib/logger';
try {
global.Olm = require('olm');
} catch (e) {
logger.warn("unable to run device verification tests: libolm not available");
}
import {verificationMethods} from '../../../../lib/crypto';
import SAS from '../../../../lib/crypto/verification/SAS';
import "../../../olm-loader";
import {verificationMethods} from "../../../../src/crypto";
import {logger} from "../../../../src/logger";
import {SAS} from "../../../../src/crypto/verification/SAS";
import {makeTestClients, setupWebcrypto, teardownWebcrypto} from './util';
const Olm = global.Olm;
import {makeTestClients} from './util';
jest.useFakeTimers();
describe("verification request", function() {
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(
[
@@ -71,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)
+24 -28
View File
@@ -1,5 +1,6 @@
/*
Copyright 2018-2019 New Vector Ltd
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.
@@ -13,29 +14,17 @@ 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 logger from '../../../../lib/logger';
try {
global.Olm = require('olm');
} catch (e) {
logger.warn("unable to run device verification tests: libolm not available");
}
import olmlib from '../../../../lib/crypto/olmlib';
import sdk from '../../../..';
import {verificationMethods} from '../../../../lib/crypto';
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
import SAS from '../../../../lib/crypto/verification/SAS';
import "../../../olm-loader";
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";
import {verificationMethods} from "../../../../src/crypto";
import * as olmlib from "../../../../src/crypto/olmlib";
import {logger} from "../../../../src/logger";
const Olm = global.Olm;
const MatrixEvent = sdk.MatrixEvent;
import {makeTestClients} from './util';
let ALICE_DEVICES;
let BOB_DEVICES;
@@ -46,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({
@@ -128,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) {
@@ -145,7 +139,7 @@ describe("SAS verification", function() {
}
}
});
resolve(verifier);
resolve(request.verifier);
});
});
@@ -345,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);
});
});
@@ -453,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);
});
});
+44 -22
View File
@@ -1,5 +1,6 @@
/*
Copyright 2019 New Vector Ltd
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.
@@ -14,10 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import TestClient from '../../../TestClient';
import sdk from '../../../..';
const MatrixEvent = sdk.MatrixEvent;
import {TestClient} from '../../../TestClient';
import {MatrixEvent} from "../../../../src/models/event";
import nodeCrypto from "crypto";
export async function makeTestClients(userInfos, options) {
const clients = [];
@@ -34,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),
);
}
}
}
@@ -51,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) {
@@ -93,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 -7
View File
@@ -1,12 +1,10 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const EventTimeline = sdk.EventTimeline;
const utils = require("../test-utils");
import * as utils from "../test-utils";
import {EventTimeline} from "../../src/models/event-timeline";
import {RoomState} from "../../src/models/room-state";
function mockRoomStates(timeline) {
timeline._startState = utils.mock(sdk.RoomState, "startState");
timeline._endState = utils.mock(sdk.RoomState, "endState");
timeline._startState = utils.mock(RoomState, "startState");
timeline._endState = utils.mock(RoomState, "endState");
}
describe("EventTimeline", function() {
+3 -4
View File
@@ -1,5 +1,6 @@
/*
Copyright 2017 New Vector Ltd
Copyright 2019 The Matrix.org Foundaction 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.
@@ -14,10 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import sdk from '../..';
const MatrixEvent = sdk.MatrixEvent;
import logger from '../../lib/logger';
import {logger} from "../../src/logger";
import {MatrixEvent} from "../../src/models/event";
describe("MatrixEvent", () => {
describe(".attemptDecryption", () => {
+1 -4
View File
@@ -1,7 +1,4 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const Filter = sdk.Filter;
import {Filter} from "../../src/filter";
describe("Filter", function() {
const filterId = "f1lt3ring15g00d4ursoul";
+4 -8
View File
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
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.
@@ -13,15 +14,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const InteractiveAuth = sdk.InteractiveAuth;
const MatrixError = sdk.MatrixError;
import logger from '../../lib/logger';
import {logger} from "../../src/logger";
import {InteractiveAuth} from "../../src/interactive-auth";
import {MatrixError} from "../../src/http-api";
// Trivial client object to test interactive auth
// (we do not need TestClient here)
+1 -1
View File
@@ -1,4 +1,4 @@
import TestClient from '../TestClient';
import {TestClient} from '../TestClient';
describe('Login request', function() {
let client;
+5 -8
View File
@@ -1,9 +1,6 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const MatrixClient = sdk.MatrixClient;
import logger from '../../lib/logger';
import {logger} from "../../src/logger";
import {MatrixClient} from "../../src/client";
import {Filter} from "../../src/filter";
jest.useFakeTimers();
@@ -180,7 +177,7 @@ describe("MatrixClient", function() {
httpLookups.push(SYNC_RESPONSE);
const filterId = "ehfewf";
store.getFilterIdByName.mockReturnValue(filterId);
const filter = new sdk.Filter(0, filterId);
const filter = new Filter(0, filterId);
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
store.getFilter.mockReturnValue(filter);
const syncPromise = new Promise((resolve, reject) => {
@@ -247,7 +244,7 @@ describe("MatrixClient", function() {
const filterName = getFilterName(client.credentials.userId);
client.store.setFilterIdByName(filterName, invalidFilterId);
const filter = new sdk.Filter(client.credentials.userId);
const filter = new Filter(client.credentials.userId);
client.getOrCreateFilter(filterName, filter).then(function(filterId) {
expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id);
+2 -4
View File
@@ -1,7 +1,5 @@
"use strict";
import 'source-map-support/register';
const PushProcessor = require("../../lib/pushprocessor");
const utils = require("../test-utils");
import * as utils from "../test-utils";
import {PushProcessor} from "../../src/pushprocessor";
describe('NotificationService', function() {
const testUserId = "@ali:matrix.org";
+1 -4
View File
@@ -1,7 +1,4 @@
"use strict";
import 'source-map-support/register';
const callbacks = require("../../src/realtime-callbacks");
import * as callbacks from "../../src/realtime-callbacks";
let wallTime = 1234567890;
jest.useFakeTimers();
+2 -5
View File
@@ -1,8 +1,5 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const RoomMember = sdk.RoomMember;
const utils = require("../test-utils");
import * as utils from "../test-utils";
import {RoomMember} from "../../src/models/room-member";
describe("RoomMember", function() {
const roomId = "!foo:bar";
+3 -6
View File
@@ -1,9 +1,6 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const RoomState = sdk.RoomState;
const RoomMember = sdk.RoomMember;
const utils = require("../test-utils");
import * as utils from "../test-utils";
import {RoomState} from "../../src/models/room-state";
import {RoomMember} from "../../src/models/room-member";
describe("RoomState", function() {
const roomId = "!foo:bar";
+12 -21
View File
@@ -1,12 +1,8 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const Room = sdk.Room;
const RoomState = sdk.RoomState;
const MatrixEvent = sdk.MatrixEvent;
const EventStatus = sdk.EventStatus;
const EventTimeline = sdk.EventTimeline;
const utils = require("../test-utils");
import * as utils from "../test-utils";
import {EventStatus, MatrixEvent} from "../../src/models/event";
import {EventTimeline} from "../../src/models/event-timeline";
import {RoomState} from "../../src/models/room-state";
import {Room} from "../../src/models/room";
describe("Room", function() {
const roomId = "!foo:bar";
@@ -20,9 +16,9 @@ describe("Room", function() {
room = new Room(roomId);
// mock RoomStates
room.oldState = room.getLiveTimeline()._startState =
utils.mock(sdk.RoomState, "oldState");
utils.mock(RoomState, "oldState");
room.currentState = room.getLiveTimeline()._endState =
utils.mock(sdk.RoomState, "currentState");
utils.mock(RoomState, "currentState");
});
describe("getAvatarUrl", function() {
@@ -621,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,
})]);
};
@@ -866,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);
@@ -876,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);
+3 -5
View File
@@ -1,12 +1,10 @@
// This file had a function whose name is all caps, which displeases eslint
/* eslint new-cap: "off" */
import 'source-map-support/register';
import {defer} from '../../src/utils';
const sdk = require("../..");
const MatrixScheduler = sdk.MatrixScheduler;
const MatrixError = sdk.MatrixError;
const utils = require("../test-utils");
import {MatrixError} from "../../src/http-api";
import {MatrixScheduler} from "../../src/scheduler";
import * as utils from "../test-utils";
jest.useFakeTimers();
+2 -5
View File
@@ -1,5 +1,6 @@
/*
Copyright 2017 Vector Creations Ltd
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.
@@ -14,11 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
import 'source-map-support/register';
import sdk from "../..";
const SyncAccumulator = sdk.SyncAccumulator;
import {SyncAccumulator} from "../../src/sync-accumulator";
describe("SyncAccumulator", function() {
let sa;
+3 -8
View File
@@ -1,11 +1,6 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const EventTimeline = sdk.EventTimeline;
const TimelineWindow = sdk.TimelineWindow;
const TimelineIndex = require("../../lib/timeline-window").TimelineIndex;
const utils = require("../test-utils");
import {EventTimeline} from "../../src/models/event-timeline";
import {TimelineIndex, TimelineWindow} from "../../src/timeline-window";
import * as utils from "../test-utils";
const ROOM_ID = "roomId";
const USER_ID = "userId";
+2 -5
View File
@@ -1,8 +1,5 @@
"use strict";
import 'source-map-support/register';
const sdk = require("../..");
const User = sdk.User;
const utils = require("../test-utils");
import {User} from "../../src/models/user";
import * as utils from "../test-utils";
describe("User", function() {
const userId = "@alice:bar";
+1 -3
View File
@@ -1,6 +1,4 @@
"use strict";
import 'source-map-support/register';
const utils = require("../../lib/utils");
import * as utils from "../../src/utils";
describe("utils", function() {
describe("encodeParams", function() {
+1 -1
View File
@@ -20,7 +20,7 @@ limitations under the License.
* @module
*/
export default class Reemitter {
export class ReEmitter {
constructor(target) {
this.target = target;
+2 -2
View File
@@ -17,8 +17,8 @@ limitations under the License.
/** @module auto-discovery */
import logger from './logger';
import { URL as NodeURL } from "url";
import {logger} from './logger';
import {URL as NodeURL} from "url";
// Dev note: Auto discovery is part of the spec.
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
+196 -280
View File
File diff suppressed because it is too large Load Diff
+54
View File
@@ -0,0 +1,54 @@
/*
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.
*/
import * as matrixcs from "./matrix";
import request from "browser-request";
import queryString from "qs";
matrixcs.request(function(opts, fn) {
// We manually fix the query string for browser-request because
// it doesn't correctly handle cases like ?via=one&via=two. Instead
// we mimic `request`'s query string interface to make it all work
// as expected.
// browser-request will happily take the constructed string as the
// query string without trying to modify it further.
opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions);
return request(opts, fn);
});
// just *accessing* indexedDB throws an exception in firefox with
// indexeddb disabled.
let indexedDB;
try {
indexedDB = global.indexedDB;
} catch(e) {}
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
if (indexedDB) {
matrixcs.setCryptoStoreFactory(
function() {
return new matrixcs.IndexedDBCryptoStore(
indexedDB, "matrix-js-sdk:crypto",
);
},
);
}
// We export 3 things to make browserify happy as well as downstream projects.
// It's awkward, but required.
export * from "./matrix";
export default matrixcs; // keep export for browserify package deps
global.matrixcs = matrixcs;
+397 -185
View File
File diff suppressed because it is too large Load Diff
+77 -78
View File
@@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
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.
@@ -13,88 +14,86 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/** @module ContentHelpers */
module.exports = {
/**
* Generates the content for a HTML Message event
* @param {string} body the plaintext body of the message
* @param {string} htmlBody the HTML representation of the message
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
makeHtmlMessage: function(body, htmlBody) {
return {
msgtype: "m.text",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
},
/**
* Generates the content for a HTML Notice event
* @param {string} body the plaintext body of the notice
* @param {string} htmlBody the HTML representation of the notice
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
makeHtmlNotice: function(body, htmlBody) {
return {
msgtype: "m.notice",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
},
/**
* Generates the content for a HTML Message event
* @param {string} body the plaintext body of the message
* @param {string} htmlBody the HTML representation of the message
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
export function makeHtmlMessage(body, htmlBody) {
return {
msgtype: "m.text",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
}
/**
* Generates the content for a HTML Emote event
* @param {string} body the plaintext body of the emote
* @param {string} htmlBody the HTML representation of the emote
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
makeHtmlEmote: function(body, htmlBody) {
return {
msgtype: "m.emote",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
},
/**
* Generates the content for a HTML Notice event
* @param {string} body the plaintext body of the notice
* @param {string} htmlBody the HTML representation of the notice
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
export function makeHtmlNotice(body, htmlBody) {
return {
msgtype: "m.notice",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
}
/**
* Generates the content for a Plaintext Message event
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
makeTextMessage: function(body) {
return {
msgtype: "m.text",
body: body,
};
},
/**
* Generates the content for a HTML Emote event
* @param {string} body the plaintext body of the emote
* @param {string} htmlBody the HTML representation of the emote
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
*/
export function makeHtmlEmote(body, htmlBody) {
return {
msgtype: "m.emote",
format: "org.matrix.custom.html",
body: body,
formatted_body: htmlBody,
};
}
/**
* Generates the content for a Plaintext Notice event
* @param {string} body the plaintext body of the notice
* @returns {{msgtype: string, body: string}}
*/
makeNotice: function(body) {
return {
msgtype: "m.notice",
body: body,
};
},
/**
* Generates the content for a Plaintext Message event
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
export function makeTextMessage(body) {
return {
msgtype: "m.text",
body: body,
};
}
/**
* Generates the content for a Plaintext Emote event
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
makeEmoteMessage: function(body) {
return {
msgtype: "m.emote",
body: body,
};
},
};
/**
* Generates the content for a Plaintext Notice event
* @param {string} body the plaintext body of the notice
* @returns {{msgtype: string, body: string}}
*/
export function makeNotice(body) {
return {
msgtype: "m.notice",
body: body,
};
}
/**
* Generates the content for a Plaintext Emote event
* @param {string} body the plaintext body of the emote
* @returns {{msgtype: string, body: string}}
*/
export function makeEmoteMessage(body) {
return {
msgtype: "m.emote",
body: body,
};
}
+84 -85
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
@@ -16,95 +17,93 @@ limitations under the License.
/**
* @module content-repo
*/
const utils = require("./utils");
/** Content Repo utility functions */
module.exports = {
/**
* Get the HTTP URL for an MXC URI.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} mxc The mxc:// URI.
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @param {Boolean} allowDirectLinks If true, return any non-mxc URLs
* directly. Fetching such URLs will leak information about the user to
* anyone they share a room with. If false, will return the emptry string
* for such URLs.
* @return {string} The complete URL to the content.
*/
getHttpUriForMxc: function(baseUrl, mxc, width, height,
resizeMethod, allowDirectLinks) {
if (typeof mxc !== "string" || !mxc) {
import * as utils from "./utils";
/**
* Get the HTTP URL for an MXC URI.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} mxc The mxc:// URI.
* @param {Number} width The desired width of the thumbnail.
* @param {Number} height The desired height of the thumbnail.
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @param {Boolean} allowDirectLinks If true, return any non-mxc URLs
* directly. Fetching such URLs will leak information about the user to
* anyone they share a room with. If false, will return the emptry string
* for such URLs.
* @return {string} The complete URL to the content.
*/
export function getHttpUriForMxc(baseUrl, mxc, width, height,
resizeMethod, allowDirectLinks) {
if (typeof mxc !== "string" || !mxc) {
return '';
}
if (mxc.indexOf("mxc://") !== 0) {
if (allowDirectLinks) {
return mxc;
} else {
return '';
}
if (mxc.indexOf("mxc://") !== 0) {
if (allowDirectLinks) {
return mxc;
} else {
return '';
}
}
let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix = "/_matrix/media/r0/download/";
const params = {};
}
let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix = "/_matrix/media/r0/download/";
const params = {};
if (width) {
params.width = Math.round(width);
}
if (height) {
params.height = Math.round(height);
}
if (resizeMethod) {
params.method = resizeMethod;
}
if (utils.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
prefix = "/_matrix/media/r0/thumbnail/";
}
if (width) {
params.width = Math.round(width);
}
if (height) {
params.height = Math.round(height);
}
if (resizeMethod) {
params.method = resizeMethod;
}
if (utils.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
prefix = "/_matrix/media/r0/thumbnail/";
}
const fragmentOffset = serverAndMediaId.indexOf("#");
let fragment = "";
if (fragmentOffset >= 0) {
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
}
return baseUrl + prefix + serverAndMediaId +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params))) + fragment;
},
const fragmentOffset = serverAndMediaId.indexOf("#");
let fragment = "";
if (fragmentOffset >= 0) {
fragment = serverAndMediaId.substr(fragmentOffset);
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
}
return baseUrl + prefix + serverAndMediaId +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params))) + fragment;
}
/**
* Get an identicon URL from an arbitrary string.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} identiconString The string to create an identicon for.
* @param {Number} width The desired width of the image in pixels. Default: 96.
* @param {Number} height The desired height of the image in pixels. Default: 96.
* @return {string} The complete URL to the identicon.
* @deprecated This is no longer in the specification.
*/
getIdenticonUri: function(baseUrl, identiconString, width, height) {
if (!identiconString) {
return null;
}
if (!width) {
width = 96;
}
if (!height) {
height = 96;
}
const params = {
width: width,
height: height,
};
/**
* Get an identicon URL from an arbitrary string.
* @param {string} baseUrl The base homeserver url which has a content repo.
* @param {string} identiconString The string to create an identicon for.
* @param {Number} width The desired width of the image in pixels. Default: 96.
* @param {Number} height The desired height of the image in pixels. Default: 96.
* @return {string} The complete URL to the identicon.
* @deprecated This is no longer in the specification.
*/
export function getIdenticonUri(baseUrl, identiconString, width, height) {
if (!identiconString) {
return null;
}
if (!width) {
width = 96;
}
if (!height) {
height = 96;
}
const params = {
width: width,
height: height,
};
const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
$ident: identiconString,
});
return baseUrl + path +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params)));
},
};
const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
$ident: identiconString,
});
return baseUrl + path +
(utils.keys(params).length === 0 ? "" :
("?" + utils.encodeParams(params)));
}
+116 -27
View File
@@ -20,9 +20,10 @@ limitations under the License.
* @module crypto/CrossSigning
*/
import {pkSign, pkVerify, encodeBase64, decodeBase64} from './olmlib';
import {decodeBase64, encodeBase64, pkSign, pkVerify} from './olmlib';
import {EventEmitter} from 'events';
import logger from '../logger';
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
*/
isStoredInSecretStorage(secretStorage) {
let stored = true;
for (const type of ["master", "self_signing", "user_signing"]) {
stored &= secretStorage.isStored(`m.cross_signing.${type}`, false);
async isStoredInSecretStorage(secretStorage) {
// 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 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);
},
);
},
};
}
+57 -29
View File
@@ -15,7 +15,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module crypto/DeviceList
@@ -24,12 +23,11 @@ limitations under the License.
*/
import {EventEmitter} from 'events';
import logger from '../logger';
import DeviceInfo from './deviceinfo';
import {logger} from '../logger';
import {DeviceInfo} from './deviceinfo';
import {CrossSigningInfo} from './CrossSigning';
import olmlib from './olmlib';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
import * as olmlib from './olmlib';
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
import {defer, sleep} from '../utils';
@@ -63,7 +61,7 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
/**
* @alias module:crypto/DeviceList
*/
export default class DeviceList extends EventEmitter {
export class DeviceList extends EventEmitter {
constructor(baseApis, cryptoStore, olmDevice) {
super();
@@ -199,16 +197,16 @@ export default 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({
@@ -219,7 +217,13 @@ export default 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);
}
@@ -313,6 +317,15 @@ export default 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
*
@@ -375,6 +388,26 @@ export default 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
*
@@ -384,19 +417,11 @@ export default 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;
@@ -596,7 +621,7 @@ export default 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.
*/
@@ -701,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.
*/
@@ -751,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);
+258 -30
View File
@@ -1,6 +1,7 @@
/*
Copyright 2016 OpenMarket Ltd
Copyright 2017, 2019 New Vector Ltd
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.
@@ -15,8 +16,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../logger';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
import {logger} from '../logger';
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
import * as algorithms from './algorithms';
// The maximum size of an event is 65K, and we base64 the content, so this is a
// reasonable approximation to the biggest plaintext we can encrypt.
@@ -69,7 +71,7 @@ function checkPayloadLength(payloadString) {
* @property {string} deviceCurve25519Key Curve25519 key for the account
* @property {string} deviceEd25519Key Ed25519 key for the account
*/
function OlmDevice(cryptoStore) {
export function OlmDevice(cryptoStore) {
this._cryptoStore = cryptoStore;
this._pickleKey = "DEFAULT_KEY";
@@ -109,16 +111,52 @@ 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();
@@ -130,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);
}
});
},
);
}
/**
@@ -189,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
@@ -671,6 +790,18 @@ OlmDevice.prototype.matchesSession = async function(
return matches;
};
OlmDevice.prototype.recordSessionProblem = async function(deviceKey, type, fixed) {
await this._cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed);
};
OlmDevice.prototype.sessionMayHaveProblems = async function(deviceKey, timestamp) {
return await this._cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp);
};
OlmDevice.prototype.filterOutNotifiedErrorDevices = async function(devices) {
return await this._cryptoStore.filterOutNotifiedErrorDevices(devices);
};
// Outbound group session
// ======================
@@ -818,9 +949,9 @@ OlmDevice.prototype._getInboundGroupSession = function(
roomId, senderKey, sessionId, txn, func,
) {
this._cryptoStore.getEndToEndInboundGroupSession(
senderKey, sessionId, txn, (sessionData) => {
senderKey, sessionId, txn, (sessionData, withheld) => {
if (sessionData === null) {
func(null);
func(null, null, withheld);
return;
}
@@ -834,7 +965,7 @@ OlmDevice.prototype._getInboundGroupSession = function(
}
this._unpickleInboundGroupSession(sessionData, (session) => {
func(session, sessionData);
func(session, sessionData, withheld);
});
},
);
@@ -859,7 +990,10 @@ OlmDevice.prototype.addInboundGroupSession = async function(
exportFormat,
) {
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
'readwrite', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
], (txn) => {
/* if we already have this session, consider updating it */
this._getInboundGroupSession(
roomId, senderKey, sessionId, txn,
@@ -914,6 +1048,60 @@ OlmDevice.prototype.addInboundGroupSession = async function(
);
};
/**
* Record in the data store why an inbound group session was withheld.
*
* @param {string} roomId room that the session belongs to
* @param {string} senderKey base64-encoded curve25519 key of the sender
* @param {string} sessionId session identifier
* @param {string} code reason code
* @param {string} reason human-readable version of `code`
*/
OlmDevice.prototype.addInboundGroupSessionWithheld = async function(
roomId, senderKey, sessionId, code, reason,
) {
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD],
(txn) => {
this._cryptoStore.storeEndToEndInboundGroupSessionWithheld(
senderKey, sessionId,
{
room_id: roomId,
code: code,
reason: reason,
},
txn,
);
},
);
};
export const WITHHELD_MESSAGES = {
"m.unverified": "The sender has disabled encrypting to unverified devices.",
"m.blacklisted": "The sender has blocked you.",
"m.unauthorised": "You are not authorised to read the message.",
"m.no_olm": "Unable to establish a secure channel.",
};
/**
* Calculate the message to use for the exception when a session key is withheld.
*
* @param {object} withheld An object that describes why the key was withheld.
*
* @return {string} the message
*
* @private
*/
function _calculateWithheldMessage(withheld) {
if (withheld.code && withheld.code in WITHHELD_MESSAGES) {
return WITHHELD_MESSAGES[withheld.code];
} else if (withheld.reason) {
return withheld.reason;
} else {
return "decryption key withheld";
}
}
/**
* Decrypt a received message with an inbound group session
*
@@ -934,16 +1122,49 @@ OlmDevice.prototype.decryptGroupMessage = async function(
roomId, senderKey, sessionId, body, eventId, timestamp,
) {
let result;
// when the localstorage crypto store is used as an indexeddb backend,
// exceptions thrown from within the inner function are not passed through
// to the top level, so we store exceptions in a variable and raise them at
// the end
let error;
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
'readwrite', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
], (txn) => {
this._getInboundGroupSession(
roomId, senderKey, sessionId, txn, (session, sessionData) => {
roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
if (session === null) {
if (withheld) {
error = new algorithms.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
_calculateWithheldMessage(withheld),
{
session: senderKey + '|' + sessionId,
},
);
}
result = null;
return;
}
const res = session.decrypt(body);
let res;
try {
res = session.decrypt(body);
} catch (e) {
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) {
error = new algorithms.DecryptionError(
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
_calculateWithheldMessage(withheld),
{
session: senderKey + '|' + sessionId,
},
);
} else {
error = e;
}
return;
}
let plaintext = res.plaintext;
if (plaintext === undefined) {
@@ -965,10 +1186,11 @@ OlmDevice.prototype.decryptGroupMessage = async function(
msgInfo.id !== eventId ||
msgInfo.timestamp !== timestamp
) {
throw new Error(
error = new Error(
"Duplicate message index, possible replay attack: " +
messageIndexKey,
);
return;
}
}
this._inboundGroupSessionMessageIndexes[messageIndexKey] = {
@@ -994,6 +1216,9 @@ OlmDevice.prototype.decryptGroupMessage = async function(
},
);
if (error) {
throw error;
}
return result;
};
@@ -1002,14 +1227,17 @@ 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
*/
OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, sessionId) {
let result;
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
'readonly', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
], (txn) => {
this._cryptoStore.getEndToEndInboundGroupSession(
senderKey, sessionId, txn, (sessionData) => {
if (sessionData === null) {
@@ -1060,7 +1288,10 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
) {
let result;
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
'readonly', [
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
], (txn) => {
this._getInboundGroupSession(
roomId, senderKey, sessionId, txn, (session, sessionData) => {
if (session === null) {
@@ -1139,6 +1370,3 @@ OlmDevice.prototype.verifySignature = function(
util.ed25519_verify(key, message, signature);
});
};
/** */
module.exports = OlmDevice;
+3 -3
View File
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../logger';
import utils from '../utils';
import {logger} from '../logger';
import * as utils from '../utils';
/**
* Internal module. Management of outgoing room key requests.
@@ -75,7 +75,7 @@ const ROOM_KEY_REQUEST_STATES = {
CANCELLATION_PENDING_AND_WILL_RESEND: 3,
};
export default class OutgoingRoomKeyRequestManager {
export class OutgoingRoomKeyRequestManager {
constructor(baseApis, deviceId, cryptoStore) {
this._baseApis = baseApis;
this._deviceId = deviceId;
+2 -2
View File
@@ -20,12 +20,12 @@ limitations under the License.
* Manages the list of encrypted rooms
*/
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
/**
* @alias module:crypto/RoomList
*/
export default class RoomList {
export class RoomList {
constructor(cryptoStore) {
this._cryptoStore = cryptoStore;
+220 -139
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.
@@ -15,18 +15,23 @@ limitations under the License.
*/
import {EventEmitter} from 'events';
import logger from '../logger';
import olmlib from './olmlib';
import { randomString } from '../randomstring';
import { pkVerify } from './olmlib';
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)
* @module crypto/SecretStorage
*/
export default class SecretStorage extends EventEmitter {
export class SecretStorage extends EventEmitter {
constructor(baseApis, cryptoCallbacks, crossSigningInfo) {
super();
this._baseApis = baseApis;
@@ -36,12 +41,12 @@ export default class SecretStorage extends EventEmitter {
this._incomingRequests = {};
}
getDefaultKeyId() {
const defaultKeyEvent = this._baseApis.getAccountData(
async getDefaultKeyId() {
const defaultKey = await this._baseApis.getAccountDataFromServer(
'm.secret_storage.default_key',
);
if (!defaultKeyEvent) return null;
return defaultKeyEvent.getContent().key;
if (!defaultKey) return null;
return defaultKey.key;
}
setDefaultKeyId(keyId) {
@@ -85,20 +90,12 @@ export default 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();
@@ -112,7 +109,11 @@ export default class SecretStorage extends EventEmitter {
if (!keyId) {
do {
keyId = randomString(32);
} while (this._baseApis.getAccountData(`m.secret_storage.key.${keyId}`));
} while (
await this._baseApis.getAccountDataFromServer(
`m.secret_storage.key.${keyId}`,
)
);
}
await this._crossSigningInfo.signObject(keyData, 'master');
@@ -130,18 +131,20 @@ export default class SecretStorage extends EventEmitter {
* @param {string} [keyId = default key's ID] The ID of the key to sign.
* Defaults to the default key ID if not provided.
*/
async signKey(keyId = this.getDefaultKeyId()) {
async signKey(keyId) {
if (!keyId) {
keyId = await this.getDefaultKeyId();
}
if (!keyId) {
throw new Error("signKey requires a key ID");
}
const keyInfoEvent = this._baseApis.getAccountData(
const keyInfo = await this._baseApis.getAccountDataFromServer(
`m.secret_storage.key.${keyId}`,
);
if (!keyInfoEvent) {
if (!keyInfo) {
throw new Error(`Key ${keyId} does not exist in account data`);
}
const keyInfo = keyInfoEvent.getContent();
await this._crossSigningInfo.signObject(keyInfo, 'master');
await this._baseApis.setAccountData(
@@ -149,6 +152,28 @@ export default 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.
*
@@ -156,15 +181,17 @@ export default class SecretStorage extends EventEmitter {
* for. Defaults to the default key ID if not provided.
* @return {boolean} Whether we have the key.
*/
hasKey(keyId = this.getDefaultKeyId()) {
if (!keyId) {
async hasKey(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;
}
const keyInfo = this._baseApis.getAccountData(
"m.secret_storage.key." + keyId,
);
return keyInfo && keyInfo.getContent();
}
/**
@@ -179,7 +206,7 @@ export default class SecretStorage extends EventEmitter {
const encrypted = {};
if (!keys) {
const defaultKeyId = this.getDefaultKeyId();
const defaultKeyId = await this.getDefaultKeyId();
if (!defaultKeyId) {
throw new Error("No keys specified and no default key present");
}
@@ -192,37 +219,25 @@ export default class SecretStorage extends EventEmitter {
for (const keyId of keys) {
// get key information from key storage
const keyInfo = this._baseApis.getAccountData(
const keyInfo = await this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
if (!keyInfo) {
throw new Error("Unknown key: " + keyId);
}
const keyInfoContent = keyInfo.getContent();
// check signature of key info
pkVerify(
keyInfoContent,
this._crossSigningInfo.getId('master'),
this._crossSigningInfo.userId,
);
// encrypt secret, based on the algorithm
switch (keyInfoContent.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1_AES:
{
const encryption = new global.Olm.PkEncryption();
try {
encryption.set_recipient_key(keyInfoContent.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:
logger.warn("unknown algorithm for secret storage key " + keyId
+ ": " + keyInfoContent.algorithm);
+ ": " + keyInfo.algorithm);
// do nothing if we don't understand the encryption algorithm
}
}
@@ -232,24 +247,30 @@ export default 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;
}
/**
@@ -260,29 +281,39 @@ export default class SecretStorage extends EventEmitter {
* @return {string} the contents of the secret
*/
async get(name) {
const secretInfo = this._baseApis.getAccountData(name);
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
if (!secretInfo) {
return;
}
const secretContent = secretInfo.getContent();
if (!secretContent.encrypted) {
throw new Error("Content is not encrypted!");
if (!secretInfo.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
const keys = {};
for (const keyId of Object.keys(secretContent.encrypted)) {
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyInfo = this._baseApis.getAccountData(
const keyInfo = await this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
).getContent();
const encInfo = secretContent.encrypted[keyId];
);
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;
@@ -295,23 +326,17 @@ export default 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 = secretContent.encrypted[keyId];
const encInfo = secretInfo.encrypted[keyId];
// We don't actually need the decryption object if it's a passthrough
// since we just want to return the key itself.
if (encInfo.passthrough) return 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();
}
}
@@ -321,53 +346,82 @@ export default 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
*/
isStored(name, checkKey) {
async isStored(name, checkKey) {
// check if secret exists
const secretInfo = this._baseApis.getAccountData(name);
if (!secretInfo) {
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 secretContent = secretInfo.getContent();
if (!secretContent.encrypted) {
return false;
}
const ret = {};
// check if secret is encrypted by a known/trusted secret and
// encryption looks sane
for (const keyId of Object.keys(secretContent.encrypted)) {
for (const keyId of Object.keys(secretInfo.encrypted)) {
// get key information from key storage
const keyEvent = this._baseApis.getAccountData(
const keyInfo = await this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
if (!keyEvent) return false;
const keyInfo = keyEvent.getContent();
if (!keyInfo) return false;
const encInfo = secretContent.encrypted[keyId];
if (checkKey) {
pkVerify(
keyInfo,
this._crossSigningInfo.getId('master'),
this._crossSigningInfo.userId,
);
if (!keyInfo) continue;
const encInfo = secretInfo.encrypted[keyId];
// 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;
}
/**
@@ -419,6 +473,7 @@ export default 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,
});
@@ -472,6 +527,7 @@ export default 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: {
@@ -508,7 +564,10 @@ export default 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}`);
}
}
}
@@ -520,7 +579,7 @@ export default 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
@@ -544,12 +603,12 @@ export default 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");
@@ -564,26 +623,48 @@ export default 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);
}
+23 -7
View File
@@ -50,7 +50,7 @@ export const DECRYPTION_CLASSES = {};
* @param {string} params.roomId The ID of the room we will be sending to
* @param {object} params.config The body of the m.room.encryption event
*/
class EncryptionAlgorithm {
export class EncryptionAlgorithm {
constructor(params) {
this._userId = params.userId;
this._deviceId = params.deviceId;
@@ -60,6 +60,15 @@ 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 @@ 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
*/
/**
@@ -84,7 +93,6 @@ class EncryptionAlgorithm {
onRoomMembership(event, member, oldMembership) {
}
}
export {EncryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
/**
* base type for decryption implementations
@@ -98,7 +106,7 @@ export {EncryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
* @param {string=} params.roomId The ID of the room we will be receiving
* from. Null for to-device events.
*/
class DecryptionAlgorithm {
export class DecryptionAlgorithm {
constructor(params) {
this._userId = params.userId;
this._crypto = params.crypto;
@@ -159,8 +167,17 @@ class DecryptionAlgorithm {
shareKeysWithDevice(keyRequest) {
throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm");
}
/**
* Retry decrypting all the events from a sender that haven't been
* decrypted yet.
*
* @param {string} senderKey the sender's key
*/
async retryDecryptionFromSender(senderKey) {
// ignore by default
}
}
export {DecryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
/**
* Exception thrown when decryption fails
@@ -173,7 +190,7 @@ export {DecryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
*
* @extends Error
*/
class DecryptionError extends Error {
export class DecryptionError extends Error {
constructor(code, msg, details) {
super(msg);
this.code = code;
@@ -181,7 +198,6 @@ class DecryptionError extends Error {
this.detailedString = _detailedStringForDecryptionError(this, details);
}
}
export {DecryptionError}; // https://github.com/jsdoc3/jsdoc/issues/1272
function _detailedStringForDecryptionError(err, details) {
let result = err.name + '[msg: ' + err.message;
+4 -19
View File
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
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.
@@ -13,28 +14,12 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module crypto/algorithms
*/
const base = require("./base");
import "./olm";
import "./megolm";
require("./olm");
require("./megolm");
/**
* @see module:crypto/algorithms/base.ENCRYPTION_CLASSES
*/
module.exports.ENCRYPTION_CLASSES = base.ENCRYPTION_CLASSES;
/**
* @see module:crypto/algorithms/base.DECRYPTION_CLASSES
*/
module.exports.DECRYPTION_CLASSES = base.DECRYPTION_CLASSES;
/**
* @see module:crypto/algorithms/base.DecryptionError
*/
module.exports.DecryptionError = base.DecryptionError;
export * from "./base";
File diff suppressed because it is too large Load Diff
+31 -25
View File
@@ -13,42 +13,48 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* Defines m.olm encryption/decryption
*
* @module crypto/algorithms/olm
*/
import logger from '../../logger';
const utils = require("../../utils");
const olmlib = require("../olmlib");
const DeviceInfo = require("../deviceinfo");
const DeviceVerification = DeviceInfo.DeviceVerification;
const base = require("./base");
import {logger} from '../../logger';
import * as utils from "../../utils";
import {polyfillSuper} from "../../utils";
import * as olmlib from "../olmlib";
import {DeviceInfo} from "../deviceinfo";
import {
DecryptionAlgorithm,
DecryptionError,
EncryptionAlgorithm,
registerAlgorithm,
} from "./base";
const DeviceVerification = DeviceInfo.DeviceVerification;
/**
* Olm encryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
* @extends {module:crypto/algorithms/EncryptionAlgorithm}
*
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
* {@link module:crypto/algorithms/EncryptionAlgorithm}
*/
function OlmEncryption(params) {
base.EncryptionAlgorithm.call(this, params);
polyfillSuper(this, EncryptionAlgorithm, params);
this._sessionPrepared = false;
this._prepPromise = null;
}
utils.inherits(OlmEncryption, base.EncryptionAlgorithm);
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) {
@@ -79,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.
@@ -143,14 +149,14 @@ OlmEncryption.prototype.encryptMessage = async function(room, eventType, content
* Olm decryption implementation
*
* @constructor
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
* @extends {module:crypto/algorithms/DecryptionAlgorithm}
* @param {object} params parameters, as per
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
* {@link module:crypto/algorithms/DecryptionAlgorithm}
*/
function OlmDecryption(params) {
base.DecryptionAlgorithm.call(this, params);
polyfillSuper(this, DecryptionAlgorithm, params);
}
utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
utils.inherits(OlmDecryption, DecryptionAlgorithm);
/**
* @inheritdoc
@@ -168,14 +174,14 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
const ciphertext = content.ciphertext;
if (!ciphertext) {
throw new base.DecryptionError(
throw new DecryptionError(
"OLM_MISSING_CIPHERTEXT",
"Missing ciphertext",
);
}
if (!(this._olmDevice.deviceCurve25519Key in ciphertext)) {
throw new base.DecryptionError(
throw new DecryptionError(
"OLM_NOT_INCLUDED_IN_RECIPIENTS",
"Not included in recipients",
);
@@ -186,7 +192,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
try {
payloadString = await this._decryptMessage(deviceKey, message);
} catch (e) {
throw new base.DecryptionError(
throw new DecryptionError(
"OLM_BAD_ENCRYPTED_MESSAGE",
"Bad Encrypted Message", {
sender: deviceKey,
@@ -200,14 +206,14 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
// check that we were the intended recipient, to avoid unknown-key attack
// https://github.com/vector-im/vector-web/issues/2483
if (payload.recipient != this._userId) {
throw new base.DecryptionError(
throw new DecryptionError(
"OLM_BAD_RECIPIENT",
"Message was intented for " + payload.recipient,
);
}
if (payload.recipient_keys.ed25519 != this._olmDevice.deviceEd25519Key) {
throw new base.DecryptionError(
throw new DecryptionError(
"OLM_BAD_RECIPIENT_KEY",
"Message not intended for this device", {
intended: payload.recipient_keys.ed25519,
@@ -221,7 +227,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
// (this check is also provided via the sender's embedded ed25519 key,
// which is checked elsewhere).
if (payload.sender != event.getSender()) {
throw new base.DecryptionError(
throw new DecryptionError(
"OLM_FORWARDED_MESSAGE",
"Message forwarded from " + payload.sender, {
reported_sender: event.getSender(),
@@ -231,7 +237,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
// Olm events intended for a room have a room_id.
if (payload.room_id !== event.getRoomId()) {
throw new base.DecryptionError(
throw new DecryptionError(
"OLM_BAD_ROOM",
"Message intended for room " + payload.room_id, {
reported_room: event.room_id,
@@ -334,4 +340,4 @@ OlmDecryption.prototype._decryptMessage = async function(
};
base.registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
+2 -5
View File
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
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.
@@ -13,8 +14,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module crypto/deviceinfo
@@ -44,7 +43,7 @@ limitations under the License.
*
* @param {string} deviceId id of the device
*/
function DeviceInfo(deviceId) {
export function DeviceInfo(deviceId) {
// you can't change the deviceId
Object.defineProperty(this, 'deviceId', {
enumerable: true,
@@ -167,5 +166,3 @@ DeviceInfo.DeviceVerification = {
const DeviceVerification = DeviceInfo.DeviceVerification;
/** */
module.exports = DeviceInfo;
+836 -269
View File
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -15,10 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { randomString } from '../randomstring';
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);
+105 -39
View File
@@ -22,25 +22,24 @@ limitations under the License.
* Utilities common to olm encryption algorithms
*/
const anotherjson = require('another-json');
import logger from '../logger';
const utils = require("../utils");
import {logger} from '../logger';
import * as utils from "../utils";
import anotherjson from "another-json";
/**
* matrix algorithm tag for olm
*/
module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
export const OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
/**
* matrix algorithm tag for megolm
*/
module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
export const MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
/**
* matrix algorithm tag for megolm backups
*/
module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2";
export const MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2";
/**
@@ -59,7 +58,7 @@ module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2
* Returns a promise which resolves (to undefined) when the payload
* has been encrypted into `resultsObject`
*/
module.exports.encryptMessageForDevice = async function(
export async function encryptMessageForDevice(
resultsObject,
ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice,
payloadFields,
@@ -112,7 +111,58 @@ module.exports.encryptMessageForDevice = async function(
resultsObject[deviceKey] = await olmDevice.encryptMessage(
deviceKey, sessionId, JSON.stringify(payload),
);
};
}
/**
* 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.
@@ -124,30 +174,37 @@ module.exports.encryptMessageForDevice = async function(
* @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}
*/
module.exports.ensureOlmSessionsForDevices = async function(
olmDevice, baseApis, devicesByUser, force,
export async function ensureOlmSessionsForDevices(
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]) {
@@ -198,7 +255,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
let res;
try {
res = await baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm,
devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout,
);
} catch (e) {
for (const resolver of Object.values(resolveSession)) {
@@ -208,14 +265,14 @@ module.exports.ensureOlmSessionsForDevices = async function(
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;
@@ -263,12 +320,12 @@ module.exports.ensureOlmSessionsForDevices = async function(
await Promise.all(promises);
return result;
};
}
async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
const deviceId = deviceInfo.deviceId;
try {
await _verifySignature(
await verifySignature(
olmDevice, oneTimeKey, userId, deviceId,
deviceInfo.getFingerprint(),
);
@@ -314,7 +371,7 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
* Returns a promise which resolves (to undefined) if the the signature is good,
* or rejects with an Error if it is bad.
*/
const _verifySignature = module.exports.verifySignature = async function(
export async function verifySignature(
olmDevice, obj, signingUserId, signingDeviceId, signingKey,
) {
const signKeyId = "ed25519:" + signingDeviceId;
@@ -335,7 +392,7 @@ const _verifySignature = module.exports.verifySignature = async function(
olmDevice.verifySignature(
signingKey, json, signature,
);
};
}
/**
* Sign a JSON object using public key cryptography
@@ -347,7 +404,7 @@ const _verifySignature = module.exports.verifySignature = async function(
* @param {string} pubkey The public key (ignored if key is a seed)
* @returns {string} the signature for the object
*/
module.exports.pkSign = function(obj, key, userId, pubkey) {
export function pkSign(obj, key, userId, pubkey) {
let createdKey = false;
if (key instanceof Uint8Array) {
const keyObj = new global.Olm.PkSigning();
@@ -371,7 +428,7 @@ module.exports.pkSign = function(obj, key, userId, pubkey) {
key.free();
}
}
};
}
/**
* Verify a signed JSON object
@@ -379,7 +436,7 @@ module.exports.pkSign = function(obj, key, userId, pubkey) {
* @param {string} pubkey The public key to use to verify
* @param {string} userId The user ID who signed the object
*/
module.exports.pkVerify = function(obj, pubkey, userId) {
export function pkVerify(obj, pubkey, userId) {
const keyId = "ed25519:" + pubkey;
if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) {
throw new Error("No signature");
@@ -397,22 +454,31 @@ module.exports.pkVerify = function(obj, pubkey, userId) {
if (unsigned) obj.unsigned = unsigned;
util.free();
}
};
}
/**
* Encode a typed array of uint8 as base64.
* @param {Uint8Array} uint8Array The data to encode.
* @return {string} The base64.
*/
module.exports.encodeBase64 = function(uint8Array) {
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.
* @return {Uint8Array} The decoded data.
*/
module.exports.decodeBase64 = function(base64) {
export function decodeBase64(base64) {
return Buffer.from(base64, "base64");
};
}
@@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
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.
@@ -15,10 +16,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../../logger';
import utils from '../../utils';
import {logger} from '../../logger';
import * as utils from "../../utils";
export const VERSION = 7;
export const VERSION = 9;
/**
* Implementation of a CryptoStore which is backed by an existing
@@ -79,7 +80,7 @@ export class Backend {
`enqueueing key request for ${requestBody.room_id} / ` +
requestBody.session_id,
);
txn.oncomplete = () => { resolve(request); };
txn.oncomplete = () => {resolve(request);};
const store = txn.objectStore("outgoingRoomKeyRequests");
store.add(request);
});
@@ -340,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);
}
};
}
@@ -401,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);
}
};
}
@@ -425,17 +447,107 @@ export class Backend {
});
}
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
const txn = this._db.transaction("session_problems", "readwrite");
const objectStore = txn.objectStore("session_problems");
objectStore.put({
deviceKey,
type,
fixed,
time: Date.now(),
});
return promiseifyTxn(txn);
}
async getEndToEndSessionProblem(deviceKey, timestamp) {
let result;
const txn = this._db.transaction("session_problems", "readwrite");
const objectStore = txn.objectStore("session_problems");
const index = objectStore.index("deviceKey");
const req = index.getAll(deviceKey);
req.onsuccess = (event) => {
const problems = req.result;
if (!problems.length) {
result = null;
return;
}
problems.sort((a, b) => {
return a.time - b.time;
});
const lastProblem = problems[problems.length - 1];
for (const problem of problems) {
if (problem.time > timestamp) {
result = Object.assign({}, problem, {fixed: lastProblem.fixed});
return;
}
}
if (lastProblem.fixed) {
result = null;
} else {
result = lastProblem;
}
};
await promiseifyTxn(txn);
return result;
}
// FIXME: we should probably prune this when devices get deleted
async filterOutNotifiedErrorDevices(devices) {
const txn = this._db.transaction("notified_error_devices", "readwrite");
const objectStore = txn.objectStore("notified_error_devices");
const ret = [];
await Promise.all(devices.map((device) => {
return new Promise((resolve) => {
const {userId, deviceInfo} = device;
const getReq = objectStore.get([userId, deviceInfo.deviceId]);
getReq.onsuccess = function() {
if (!getReq.result) {
objectStore.put({userId, deviceId: deviceInfo.deviceId});
ret.push(device);
}
resolve();
};
});
}));
return ret;
}
// Inbound group sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
let session = false;
let withheld = false;
const objectStore = txn.objectStore("inbound_group_sessions");
const getReq = objectStore.get([senderCurve25519Key, sessionId]);
getReq.onsuccess = function() {
try {
if (getReq.result) {
func(getReq.result.session);
session = getReq.result.session;
} else {
func(null);
session = null;
}
if (withheld !== false) {
func(session, withheld);
}
} catch (e) {
abortWithException(txn, e);
}
};
const withheldObjectStore = txn.objectStore("inbound_group_sessions_withheld");
const withheldGetReq = withheldObjectStore.get([senderCurve25519Key, sessionId]);
withheldGetReq.onsuccess = function() {
try {
if (withheldGetReq.result) {
withheld = withheldGetReq.result.session;
} else {
withheld = null;
}
if (session !== false) {
func(session, withheld);
}
} catch (e) {
abortWithException(txn, e);
@@ -499,6 +611,15 @@ export class Backend {
});
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
const objectStore = txn.objectStore("inbound_group_sessions_withheld");
objectStore.put({
senderCurve25519Key, sessionId, session: sessionData,
});
}
getEndToEndDeviceData(txn, func) {
const objectStore = txn.objectStore("device_data");
const getReq = objectStore.get("-");
@@ -662,6 +783,21 @@ export function upgradeDatabase(db, oldVersion) {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
if (oldVersion < 8) {
db.createObjectStore("inbound_group_sessions_withheld", {
keyPath: ["senderCurve25519Key", "sessionId"],
});
}
if (oldVersion < 9) {
const problemsStore = db.createObjectStore("session_problems", {
keyPath: ["deviceKey", "time"],
});
problemsStore.createIndex("deviceKey", "deviceKey");
db.createObjectStore("notified_error_devices", {
keyPath: ["userId", "deviceId"],
});
}
// Expand as needed.
}
+97 -102
View File
@@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
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.
@@ -15,9 +16,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../../logger';
import LocalStorageCryptoStore from './localStorage-crypto-store';
import MemoryCryptoStore from './memory-crypto-store';
import {logger} from '../../logger';
import {LocalStorageCryptoStore} from './localStorage-crypto-store';
import {MemoryCryptoStore} from './memory-crypto-store';
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
import {InvalidCryptoStoreError} from '../../errors';
import * as IndexedDBHelpers from "../../indexeddb-helpers";
@@ -34,7 +35,7 @@ import * as IndexedDBHelpers from "../../indexeddb-helpers";
*
* @implements {module:crypto/store/base~CryptoStore}
*/
export default class IndexedDBCryptoStore {
export class IndexedDBCryptoStore {
/**
* Create a new IndexedDBCryptoStore
*
@@ -45,6 +46,7 @@ export default class IndexedDBCryptoStore {
this._indexedDB = indexedDB;
this._dbName = dbName;
this._backendPromise = null;
this._backend = null;
}
static exists(indexedDB, dbName) {
@@ -55,10 +57,12 @@ export default 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;
}
@@ -104,7 +108,10 @@ export default class IndexedDBCryptoStore {
// we can fall back to a different backend.
return backend.doTxn(
'readonly',
[IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS],
[
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
],
(txn) => {
backend.getEndToEndInboundGroupSession('', '', txn, () => {});
}).then(() => {
@@ -131,6 +138,8 @@ export default class IndexedDBCryptoStore {
);
return new MemoryCryptoStore();
}
}).then(backend => {
this._backend = backend;
});
return this._backendPromise;
@@ -185,9 +194,7 @@ export default 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);
}
/**
@@ -201,9 +208,7 @@ export default class IndexedDBCryptoStore {
* not found
*/
getOutgoingRoomKeyRequest(requestBody) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequest(requestBody);
});
return this._backend.getOutgoingRoomKeyRequest(requestBody);
}
/**
@@ -217,9 +222,7 @@ export default 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);
}
/**
@@ -233,11 +236,9 @@ export default 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,
);
}
/**
@@ -253,11 +254,9 @@ export default 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,
);
}
/**
@@ -270,9 +269,7 @@ export default 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
@@ -285,9 +282,7 @@ export default 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);
}
/**
@@ -298,9 +293,7 @@ export default 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);
}
/**
@@ -312,9 +305,16 @@ export default 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);
}
/**
@@ -324,9 +324,18 @@ export default 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
@@ -337,9 +346,7 @@ export default 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);
}
/**
@@ -355,9 +362,7 @@ export default 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);
}
/**
@@ -372,9 +377,7 @@ export default class IndexedDBCryptoStore {
* a message.
*/
getEndToEndSessions(deviceKey, txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndSessions(deviceKey, txn, func);
});
this._backend.getEndToEndSessions(deviceKey, txn, func);
}
/**
@@ -385,9 +388,7 @@ export default class IndexedDBCryptoStore {
* and session keys.
*/
getAllEndToEndSessions(txn, func) {
this._backendPromise.then(backend => {
backend.getAllEndToEndSessions(txn, func);
});
this._backend.getAllEndToEndSessions(txn, func);
}
/**
@@ -398,11 +399,21 @@ export default 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._backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
}
getEndToEndSessionProblem(deviceKey, timestamp) {
return this._backend.getEndToEndSessionProblem(deviceKey, timestamp);
}
filterOutNotifiedErrorDevices(devices) {
return this._backend.filterOutNotifiedErrorDevices(devices);
}
// Inbound group sessions
@@ -417,11 +428,9 @@ export default 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,
);
}
/**
@@ -432,9 +441,7 @@ export default 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);
}
/**
@@ -447,11 +454,9 @@ export default 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,
);
}
/**
@@ -464,11 +469,17 @@ export default 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._backend.storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
// End-to-end device tracking
@@ -484,9 +495,7 @@ export default 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);
}
/**
@@ -497,9 +506,7 @@ export default class IndexedDBCryptoStore {
* device data
*/
getEndToEndDeviceData(txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndDeviceData(txn, func);
});
this._backend.getEndToEndDeviceData(txn, func);
}
// End to End Rooms
@@ -511,9 +518,7 @@ export default 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);
}
/**
@@ -522,9 +527,7 @@ export default 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
@@ -536,9 +539,7 @@ export default 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);
}
/**
@@ -547,9 +548,7 @@ export default 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);
}
/**
@@ -559,9 +558,7 @@ export default 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);
}
/**
@@ -571,9 +568,7 @@ export default 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);
}
/**
@@ -598,15 +593,15 @@ export default 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);
}
}
IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
= 'inbound_group_sessions_withheld';
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
+97 -7
View File
@@ -1,5 +1,6 @@
/*
Copyright 2017, 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.
@@ -14,8 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../../logger';
import MemoryCryptoStore from './memory-crypto-store';
import {logger} from '../../logger';
import {MemoryCryptoStore} from './memory-crypto-store';
/**
* Internal module. Partial localStorage backed storage for e2e.
@@ -30,8 +31,10 @@ import MemoryCryptoStore from './memory-crypto-store';
const E2E_PREFIX = "crypto.";
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys";
const KEY_NOTIFIED_ERROR_DEVICES = E2E_PREFIX + "notified_error_devices";
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.withheld/";
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
@@ -39,10 +42,18 @@ function keyEndToEndSessions(deviceKey) {
return E2E_PREFIX + "sessions/" + deviceKey;
}
function keyEndToEndSessionProblems(deviceKey) {
return E2E_PREFIX + "session.problems/" + deviceKey;
}
function keyEndToEndInboundGroupSession(senderKey, sessionId) {
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
}
function keyEndToEndInboundGroupSessionWithheld(senderKey, sessionId) {
return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId;
}
function keyEndToEndRoomsPrefix(roomId) {
return KEY_ROOMS_PREFIX + roomId;
}
@@ -50,7 +61,7 @@ function keyEndToEndRoomsPrefix(roomId) {
/**
* @implements {module:crypto/store/base~CryptoStore}
*/
export default class LocalStorageCryptoStore extends MemoryCryptoStore {
export class LocalStorageCryptoStore extends MemoryCryptoStore {
constructor(webStore) {
super();
this.store = webStore;
@@ -122,13 +133,71 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
);
}
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
const key = keyEndToEndSessionProblems(deviceKey);
const problems = getJsonItem(this.store, key) || [];
problems.push({type, fixed, time: Date.now()});
problems.sort((a, b) => {
return a.time - b.time;
});
setJsonItem(this.store, key, problems);
}
async getEndToEndSessionProblem(deviceKey, timestamp) {
const key = keyEndToEndSessionProblems(deviceKey);
const problems = getJsonItem(this.store, key) || [];
if (!problems.length) {
return null;
}
const lastProblem = problems[problems.length - 1];
for (const problem of problems) {
if (problem.time > timestamp) {
return Object.assign({}, problem, {fixed: lastProblem.fixed});
}
}
if (lastProblem.fixed) {
return null;
} else {
return lastProblem;
}
}
async filterOutNotifiedErrorDevices(devices) {
const notifiedErrorDevices =
getJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
const ret = [];
for (const device of devices) {
const {userId, deviceInfo} = device;
if (userId in notifiedErrorDevices) {
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
ret.push(device);
notifiedErrorDevices[userId][deviceInfo.deviceId] = true;
}
} else {
ret.push(device);
notifiedErrorDevices[userId] = {[deviceInfo.deviceId]: true };
}
}
setJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES, notifiedErrorDevices);
return ret;
}
// Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
func(getJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
));
func(
getJsonItem(
this.store,
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
),
getJsonItem(
this.store,
keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId),
),
);
}
getAllEndToEndInboundGroupSessions(txn, func) {
@@ -170,6 +239,16 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
);
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
setJsonItem(
this.store,
keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId),
sessionData,
);
}
getEndToEndDeviceData(txn, func) {
func(getJsonItem(
this.store, KEY_DEVICE_DATA,
@@ -288,12 +367,23 @@ export default 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));
}
+100 -9
View File
@@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
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.
@@ -15,8 +16,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../../logger';
import utils from '../../utils';
import {logger} from '../../logger';
import * as utils from "../../utils";
/**
* Internal module. in-memory storage for e2e.
@@ -27,16 +28,23 @@ import utils from '../../utils';
/**
* @implements {module:crypto/store/base~CryptoStore}
*/
export default class MemoryCryptoStore {
export class MemoryCryptoStore {
constructor() {
this._outgoingRoomKeyRequests = [];
this._account = null;
this._crossSigningKeys = null;
this._privateKeys = {};
this._backupKeys = {};
// Map of {devicekey -> {sessionId -> session pickle}}
this._sessions = {};
// Map of {devicekey -> array of problems}
this._sessionProblems = {};
// Map of {userId -> deviceId -> true}
this._notifiedErrorDevices = {};
// Map of {senderCurve25519Key+'/'+sessionId -> session data object}
this._inboundGroupSessions = {};
this._inboundGroupSessionsWithheld = {};
// Opaque device data object
this._deviceData = null;
// roomId -> Opaque roomInfo object
@@ -45,6 +53,18 @@ export default 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.
*
@@ -237,10 +257,19 @@ export default 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) {
@@ -257,11 +286,15 @@ export default 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) {
@@ -273,10 +306,61 @@ export default class MemoryCryptoStore {
deviceSessions[sessionId] = sessionInfo;
}
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
const problems = this._sessionProblems[deviceKey]
= this._sessionProblems[deviceKey] || [];
problems.push({type, fixed, time: Date.now()});
problems.sort((a, b) => {
return a.time - b.time;
});
}
async getEndToEndSessionProblem(deviceKey, timestamp) {
const problems = this._sessionProblems[deviceKey] || [];
if (!problems.length) {
return null;
}
const lastProblem = problems[problems.length - 1];
for (const problem of problems) {
if (problem.time > timestamp) {
return Object.assign({}, problem, {fixed: lastProblem.fixed});
}
}
if (lastProblem.fixed) {
return null;
} else {
return lastProblem;
}
}
async filterOutNotifiedErrorDevices(devices) {
const notifiedErrorDevices = this._notifiedErrorDevices;
const ret = [];
for (const device of devices) {
const {userId, deviceInfo} = device;
if (userId in notifiedErrorDevices) {
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
ret.push(device);
notifiedErrorDevices[userId][deviceInfo.deviceId] = true;
}
} else {
ret.push(device);
notifiedErrorDevices[userId] = {[deviceInfo.deviceId]: true };
}
}
return ret;
}
// Inbound Group Sessions
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
func(this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] || null);
const k = senderCurve25519Key+'/'+sessionId;
func(
this._inboundGroupSessions[k] || null,
this._inboundGroupSessionsWithheld[k] || null,
);
}
getAllEndToEndInboundGroupSessions(txn, func) {
@@ -306,6 +390,13 @@ export default class MemoryCryptoStore {
this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData;
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
const k = senderCurve25519Key+'/'+sessionId;
this._inboundGroupSessionsWithheld[k] = sessionData;
}
// Device Data
getEndToEndDeviceData(txn, func) {
+121 -11
View File
@@ -21,13 +21,22 @@ limitations under the License.
import {MatrixEvent} from '../../models/event';
import {EventEmitter} from 'events';
import logger from '../../logger';
import DeviceInfo from '../deviceinfo';
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 default class VerificationBase extends EventEmitter {
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 default 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 default 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 default 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 default 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 default 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");
}
}
+62 -72
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.
@@ -19,105 +20,94 @@ limitations under the License.
* @module crypto/verification/QRCode
*/
import Base from "./Base";
import {VerificationBase as Base} from "./Base";
import {
errorFactory,
newUserCancelledError,
newKeyMismatchError,
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";
+89 -30
View File
@@ -19,15 +19,18 @@ limitations under the License.
* @module crypto/verification/SAS
*/
import Base from "./Base";
import {VerificationBase as Base, SwitchStartEventError} from "./Base";
import anotherjson from 'another-json';
import {
errorFactory,
newUserCancelledError,
newUnknownMethodError,
newKeyMismatchError,
newInvalidMessageError,
newKeyMismatchError,
newUnknownMethodError,
newUserCancelledError,
} from './Error';
import {logger} from '../../logger';
const START_TYPE = "m.key.verification.start";
const EVENTS = [
"m.key.verification.accept",
@@ -108,7 +111,7 @@ const emojiMapping = [
["✏️", "pencil"], // 43
["📎", "paperclip"], // 44
["✂️", "scissors"], // 45
["🔒", "padlock"], // 46
["🔒", "lock"], // 46
["🔑", "key"], // 47
["🔨", "hammer"], // 48
["☎️", "telephone"], // 49
@@ -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).
@@ -185,7 +197,11 @@ function intersection(anArray, aSet) {
* @alias module:crypto/verification/SAS
* @extends {module:crypto/verification/Base}
*/
export default class SAS extends Base {
export class SAS extends Base {
static get NAME() {
return "m.sas.v1";
}
get events() {
return EVENTS;
}
@@ -197,16 +213,37 @@ export default 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,
@@ -215,11 +252,33 @@ export default 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);
@@ -244,7 +303,7 @@ export default 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();
@@ -257,7 +316,7 @@ export default 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);
@@ -265,7 +324,8 @@ export default class SAS extends Base {
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
});
};
this.emit("show_sas", this.sasEvent);
});
@@ -341,7 +401,7 @@ export default 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);
@@ -349,7 +409,8 @@ export default class SAS extends Base {
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
});
};
this.emit("show_sas", this.sasEvent);
});
@@ -380,7 +441,7 @@ export default 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,
);
@@ -389,14 +450,14 @@ export default 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",
);
@@ -409,7 +470,7 @@ export default 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",
)) {
@@ -417,7 +478,7 @@ export default 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,
)) {
@@ -426,5 +487,3 @@ export default class SAS extends Base {
});
}
}
SAS.NAME = "m.sas.v1";
+150 -19
View File
@@ -15,7 +15,14 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import VerificationRequest, {REQUEST_TYPE, START_TYPE} 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";
const M_RELATES_TO = "m.relates_to";
@@ -24,16 +31,16 @@ const M_RELATES_TO = "m.relates_to";
* A key verification channel that sends verification events in the timeline of a room.
* Uses the event id of the initial m.key.verification.request event as a transaction id.
*/
export default class InRoomChannel {
export class InRoomChannel {
/**
* @param {MatrixClient} client the matrix client, to send messages with and get current user & device from.
* @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;
}
@@ -42,16 +49,41 @@ export default 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();
}
@@ -96,19 +128,26 @@ export default 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);
}
/**
@@ -129,28 +168,54 @@ export default 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);
}
/**
@@ -179,7 +244,7 @@ export default 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) {
@@ -190,7 +255,7 @@ export default 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,
};
@@ -231,3 +296,69 @@ export default 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 default 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;
}
}
@@ -15,33 +15,56 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { randomString } from '../../../randomstring';
import logger from '../../../logger';
import VerificationRequest, {
PHASE_STARTED,
REQUEST_TYPE,
START_TYPE,
CANCEL_TYPE,
} from "./VerificationRequest";
import {randomString} from '../../../randomstring';
import {logger} from '../../../logger';
import {
newUnexpectedMessageError,
errorFromEvent,
} from "../Error";
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.
* Generates its own transaction ids.
*/
export default class ToDeviceChannel {
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();
}
@@ -82,10 +105,12 @@ export default 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;
}
@@ -93,6 +118,7 @@ export default 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() &&
@@ -100,19 +126,19 @@ export default 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;
}
@@ -121,12 +147,13 @@ export default 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;
}
@@ -145,14 +172,17 @@ export default 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({
@@ -188,7 +218,7 @@ export default 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) {
@@ -218,12 +248,28 @@ export default 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) {
@@ -233,7 +279,7 @@ export default 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();
}
@@ -247,3 +293,71 @@ export default 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;
}
}
}
}
}
@@ -15,15 +15,15 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import logger from '../../../logger';
import RequestCallbackChannel from "./RequestCallbackChannel";
import {logger} from '../../../logger';
import {EventEmitter} from 'events';
import {
newUnknownMethodError,
newUnexpectedMessageError,
errorFromEvent,
errorFactory,
errorFromEvent,
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;
@@ -57,18 +57,23 @@ export const PHASE_DONE = 6;
* send and receive verification events are put in `InRoomChannel` or `ToDeviceChannel`.
* @event "change" whenever the state of the request object has changed.
*/
export default class VerificationRequest extends EventEmitter {
constructor(channel, verificationMethods, userId, client) {
export class VerificationRequest extends EventEmitter {
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 default 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 default 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 default 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 default 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 default 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 default 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 default 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 default 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 default 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;
}
}
+3 -5
View File
@@ -1,5 +1,6 @@
/*
Copyright 2016 OpenMarket Ltd
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.
@@ -13,7 +14,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
"use strict";
/**
* @module filter-component
*/
@@ -45,7 +46,7 @@ function _matches_wildcard(actual_value, filter_value) {
* @constructor
* @param {Object} filter_json the definition of this filter JSON, e.g. { 'contains_url': true }
*/
function FilterComponent(filter_json) {
export function FilterComponent(filter_json) {
this.filter_json = filter_json;
this.types = filter_json.types || null;
@@ -141,6 +142,3 @@ FilterComponent.prototype.filter = function(events) {
FilterComponent.prototype.limit = function() {
return this.filter_json.limit !== undefined ? this.filter_json.limit : 10;
};
/** The FilterComponent class */
module.exports = FilterComponent;

Some files were not shown because too many files have changed in this diff Show More