Compare commits

...

319 Commits

Author SHA1 Message Date
RiotRobot c3903f2796 v5.3.1-rc.3 2020-04-17 17:35:49 +01:00
RiotRobot 18564553ad Prepare changelog for v5.3.1-rc.3 2020-04-17 17:35:48 +01:00
J. Ryan Stinnett fe3e87a9d6 Merge pull request #1323 from matrix-org/dbkr/fix_ssss_reset_rel
Fix cross-signing/SSSS reset
2020-04-17 13:42:47 +01:00
Bruno Windels 78c734d2e9 Merge pull request #1325 from matrix-org/bwindels/fixkeybackupcrash-rc
Fix: crash when backup key needs fixing from corruption issue
2020-04-17 12:23:08 +00:00
Bruno Windels 5afa16d454 method is on crypto 2020-04-17 14:19:55 +02:00
David Baker 149f59e9be lint 2020-04-16 20:42:00 +01:00
David Baker 761892d6b1 Fix cross-signing/SSSS reset
We re-used the old SSSS key even when resetting, meaning we prompted
the user to create a new passphrase but then ignored it and kept using
the old one.

Fixes https://github.com/vector-im/riot-web/issues/13212
2020-04-16 20:41:55 +01:00
RiotRobot faf18b1996 v5.3.1-rc.2 2020-04-16 18:39:06 +01:00
RiotRobot 218deddd00 Prepare changelog for v5.3.1-rc.2 2020-04-16 18:39:06 +01:00
Bruno Windels d6fe650c4c Merge pull request #1321 from matrix-org/bwindels/untrustedqrcodereciprocate-rc
Implement QR code reciprocate for self-verification with untrusted MSK
2020-04-16 12:38:04 +00:00
Bruno Windels 951191d99a update comments 2020-04-16 14:31:44 +02:00
Bruno Windels 2654f4bf0f implement qr code reciprocate for self-verif with untrusted MSK 2020-04-16 13:26:56 +02:00
RiotRobot 195f3a7550 v5.3.1-rc.1 2020-04-15 19:05:12 +01:00
RiotRobot 1c9343ed8f Prepare changelog for v5.3.1-rc.1 2020-04-15 19:05:11 +01:00
RiotRobot 5c7d9981f8 Merge branch 'release-v5.3.0' into release-v5.3.1 2020-04-15 19:02:40 +01:00
J. Ryan Stinnett 63bc17a6b4 Merge pull request #1319 from matrix-org/jryans/release-script
Adapt release script for riot-desktop
2020-04-15 18:26:04 +01:00
J. Ryan Stinnett 629490c4ae Adapt release script for riot-desktop
This allows the bulk of the release script to be used for riot-desktop, while
skipping the NPM and develop branch parts that do not apply.

Part of https://github.com/vector-im/riot-web/issues/13176
2020-04-15 18:10:46 +01:00
Bruno Windels d59d62f96a Merge pull request #1318 from matrix-org/bwindels/fixindexernotifs
Fix: prevent spurious notifications from indexer
2020-04-15 16:50:18 +00:00
Bruno Windels 8a460c2368 fix jsdoc 2020-04-15 18:33:55 +02:00
Bruno Windels 6f60183316 allow creating event mappers that map events whose events are not reemitted 2020-04-15 17:08:25 +02:00
J. Ryan Stinnett a21c62519b Merge pull request #1317 from matrix-org/jryans/gen-user-obj
Always create our own user object
2020-04-14 16:19:45 +01:00
J. Ryan Stinnett 1b8f6d1739 Always create our own user object
This ensures our own user object is always available for tasks like
verification, even if you aren't in any rooms.

Fixes https://github.com/vector-im/riot-web/issues/13165
2020-04-14 16:02:00 +01:00
Hubert Chathi e087bce61a Merge pull request #1311 from matrix-org/uhoreg/fix_backup_key_format
Fix incorrect backup key format in SSSS
2020-04-13 17:38:45 -04:00
Hubert Chathi d2f24c3e87 cut long line to appease lint 2020-04-13 17:03:17 -04:00
Hubert Chathi 5d606bba66 add test for passthrough on backups 2020-04-13 16:54:02 -04:00
Hubert Chathi 864fe459b7 improve readability of tests 2020-04-13 16:27:46 -04:00
Bruno Windels 8992438aa6 Merge pull request #1315 from matrix-org/bwindels/fixe2eecrash
Fix e2ee crash after refreshing after having received a cross-singing key reset
2020-04-10 10:20:20 +00:00
Bruno Windels 197d5ebb44 fix e2ee crash after refreshing after cross-singing key reset 2020-04-10 10:37:12 +02:00
Hubert Chathi 4039498eee use cached backup key if available 2020-04-09 17:05:01 -04:00
Bruno Windels a686231a5b Merge pull request #1314 from matrix-org/bwindels/catchverifysenderrors
Fix: catch send errors in SAS verifier
2020-04-09 16:32:09 +00:00
Bruno Windels 97cf4bff1f fix tests 2020-04-09 18:21:20 +02:00
Bruno Windels 11727833a2 mark request as cancelled immediately after verifier is cancelled
in case send errors prevent us from receiving remote echo
2020-04-09 17:54:45 +02:00
Hubert Chathi df38fde336 apply changes from code review 2020-04-09 11:48:58 -04:00
Bruno Windels 00233d610b pass on send errors so request gets cancelled 2020-04-09 17:23:44 +02:00
Bruno Windels d1c9030fac Merge pull request #1312 from matrix-org/bwindels/fixresetkeys
Clear cross-signing keys when detecting the keys have changed
2020-04-09 13:34:26 +00:00
Bruno Windels 70071eef41 also emit user trust has changed 2020-04-09 14:09:51 +02:00
Bruno Windels 65dd56f53a remove obsolete comment 2020-04-09 13:27:13 +02:00
Bruno Windels c8fb4af369 fix comment style 2020-04-09 13:13:17 +02:00
Bruno Windels 9e1ba992e3 Clear cross-signing keys when detecting the keys have changed 2020-04-09 12:24:29 +02:00
J. Ryan Stinnett bad09fed44 Merge pull request #1310 from matrix-org/jryans/upgrade-2020-04-08
Upgrade deps
2020-04-09 10:19:09 +01:00
Hubert Chathi 5bd146bb85 lint 2020-04-09 00:21:31 -04:00
Hubert Chathi 75703f273f fix unit tests 2020-04-09 00:18:21 -04:00
Hubert Chathi ca7b49d209 fix incorrect backup key format in SSSS 2020-04-09 00:08:17 -04:00
J. Ryan Stinnett 30bce8a682 yarn upgrade 2020-04-08 14:41:36 +01:00
RiotRobot f413f0ee5f v5.3.0-rc.1 2020-04-08 13:46:11 +01:00
RiotRobot be6778a931 Prepare changelog for v5.3.0-rc.1 2020-04-08 13:46:10 +01:00
J. Ryan Stinnett 84637c6ebd Merge pull request #1308 from matrix-org/jryans/backup-key-cache-format
Store key backup key in cache as Uint8Array
2020-04-08 11:32:18 +01:00
J. Ryan Stinnett 2ed6e9ba2f Convert recovery key to Uint8Array explicitly 2020-04-08 00:43:52 +01:00
J. Ryan Stinnett 2e3cb54d38 Restore redundant conversion for tests 2020-04-07 19:21:25 +01:00
J. Ryan Stinnett 0efac7eae0 Store key backup key in cache as Uint8Array
Bootstrap was incorrectly storing the key backup key as base64 encoded, when
instead it should be a uint8Array.

Fixes https://github.com/vector-im/riot-web/issues/13057
2020-04-07 18:28:48 +01:00
J. Ryan Stinnett dc56f05d03 Throw error when trying to cache keys in the wrong format 2020-04-07 18:06:57 +01:00
Hubert Chathi 4d2625278c Merge pull request #1307 from MTRNord/fix-keys_query_endpoint
Use the correct request body for the /keys/query endpoint.
2020-04-07 12:01:06 -04:00
Marcel bff8a947a1 Fix expectBobQueryKeys test 2020-04-07 16:53:28 +02:00
Marcel 9cb7406ebd Fix downloadKeys test 2020-04-07 16:44:30 +02:00
Marcel 38681ca6ca Fix expectAliQueryKeys test 2020-04-07 16:31:15 +02:00
Marcel 916696c906 Fix expectKeyQuery test 2020-04-07 16:19:46 +02:00
Marcel 1b3c4c935e Use the correct request body for the /keys/query endpoint.
https://matrix.org/docs/spec/client_server/r0.6.0#post-matrix-client-r0-keys-query
2020-04-07 14:08:49 +02:00
David Baker 66417e6742 Merge pull request #1305 from matrix-org/dbkr/uia_ignore_poll_after_success
Avoid creating two devices on registration
2020-04-07 10:32:08 +01:00
David Baker 6ad4d6dd62 also fix typo 2020-04-07 10:21:57 +01:00
David Baker 59607f8dbf Clear resolve / reject function after rejecting too 2020-04-07 10:20:58 +01:00
David Baker 62380a48d5 Merge pull request #1306 from matrix-org/dbkr/max_warnings_81
Lower max-warnings to 81
2020-04-07 10:19:45 +01:00
David Baker 4245372495 Lower max-warnings to 81
We apparently fixed 11 warnings at some point. Let's keep it that way.
2020-04-07 10:06:23 +01:00
David Baker 964a448470 Avoid creating two devices on registration
Ignore poll requests after a UIA session has finished. Sometimes
the app calls poll() even after the registration has succeeded
which causes the API call to happen again which, in the case of
registration, creates a phantom device.

Fixes https://github.com/vector-im/riot-web/issues/13040
2020-04-06 19:45:18 +01:00
J. Ryan Stinnett 2151f28d4c Merge pull request #1303 from matrix-org/jryans/backup-key-not-cached
Move key backup key creation before caching
2020-04-06 18:41:58 +01:00
J. Ryan Stinnett 060549656e Move key backup key creation before caching
For some reason, we were trying to cache the key backup key before it was
created, and as a result we wouldn't cache the key until perhaps a second time
through bootstrapping.

Fixes https://github.com/vector-im/riot-web/issues/13023
2020-04-06 16:03:24 +01:00
Zoe 7dabf507a2 Merge pull request #1298 from matrix-org/foldleft/12843-resend-requests
Expose function to force-reset outgoing room key requests
2020-04-06 13:43:06 +01:00
Zoe d69afa47a1 review feedback 2020-04-06 13:30:36 +01:00
Bruno Windels 275a8175aa Merge pull request #1302 from matrix-org/bwindels/selfverifux
Add isSelfVerification property to VerificationRequest
2020-04-03 16:02:05 +00:00
Bruno Windels 40bebf4cbd Merge pull request #1297 from matrix-org/bwindels/qr-reciprocate
QR code reciprocation
2020-04-03 12:28:34 +00:00
Zoe b8bc323c03 add a crypto-level function for resending key requests 2020-04-03 11:47:13 +01:00
Zoe c6d32ea2b0 Expose function to force-reset outgoing room key requests 2020-04-02 16:25:50 +01:00
Bruno Windels 2ace35ad6b add isSelfVerification helper 2020-04-02 17:16:06 +02:00
Bruno Windels b5ea8d3a78 Merge branch 'bwindels/qr-reciprocate' into bwindels/selfverifux 2020-04-02 17:15:32 +02:00
Bruno Windels f3a05f6ed0 fix jsdoc 2020-04-02 13:37:15 +02:00
Bruno Windels 43eae4929b remove ts annotations as I don't want to convert the whole file now 2020-04-02 13:31:17 +02:00
Bruno Windels 6144962c24 cancel with m.user code when user doesn't reciprocate 2020-04-02 12:53:52 +02:00
Bruno Windels 544cc36006 explain a bit why we're doing things here 2020-04-02 11:31:44 +02:00
Hubert Chathi 1834e6688f Merge pull request #1294 from matrix-org/uhoreg/check_sssss_passphrase
Add ability to check symmetric SSSS key before we try to use it
2020-04-01 16:01:34 -04:00
David Baker e1ee56aa43 Merge pull request #1296 from matrix-org/dbkr/debug_stuck_events
Add some debug logging for events stuck to bottom of timeline
2020-04-01 18:54:52 +01:00
David Baker a1779719be Add some debug logging for events stuck to bottom of timeline 2020-04-01 18:48:59 +01:00
Bruno Windels 0e464af627 Merge pull request #1295 from matrix-org/bwindels/fixdoublerequestexplosion
Fix: spontanous verification request cancellation under some circumstances
2020-04-01 17:17:24 +00:00
Bruno Windels fd7f0c3b5a Ignore phase while checking if other party supports scanning
(as the phase hasn't been set yet)

Also, set the qrCodeData in handleEvent as we need it to be async
2020-04-01 19:01:53 +02:00
Bruno Windels ed210a4fb1 Use static constructor that can be async (as looking up device is async)
Also fix static methods not to use this
2020-04-01 19:00:57 +02:00
Bruno Windels c2a0504980 update comments 2020-04-01 18:23:34 +02:00
Bruno Windels b6708871d3 fix olmlib import 2020-04-01 18:23:34 +02:00
Bruno Windels 275ea6aacb spec recommends 11 random bytes for shared secret 2020-04-01 18:23:34 +02:00
Bruno Windels f97d413603 expose chosen method after request has started
so we can show a different ux now that it's not just SAS anymore,
but also reciprocate
2020-04-01 18:23:34 +02:00
Bruno Windels 2c7ea0606b move shared secret to QRCodeData 2020-04-01 18:23:34 +02:00
Bruno Windels d7a7100912 reuse keys from qr code data to sign
so we don't sign the wrong keys if a malicious HS admin
forged a cross-sign identity reset
2020-04-01 18:23:34 +02:00
Bruno Windels 2c54b8d77e ask the user if the other user scanned the QR code 2020-04-01 18:23:34 +02:00
Bruno Windels b30f278e03 checking the userid isn't a thing anymore AFAIK 2020-04-01 18:23:34 +02:00
Bruno Windels b642030a34 keep a copy of the master/device key and mode, for use in reciprocate 2020-04-01 18:23:34 +02:00
Bruno Windels 725976d472 Add QR code buffer generation to VerificationRequest (from react-sdk)
Doing this so later we can keep the other user master signing key on it
so we can make sure we cross-sign that one once we decide to sign.
2020-04-01 18:23:34 +02:00
Bruno Windels 80d87e1bf1 prevent the same event being handled twice in the verification request, result in cancellation 2020-04-01 17:13:24 +02:00
Hubert Chathi d01f527e71 only try to set keyInfo if it exists 2020-03-31 16:54:06 -04:00
Hubert Chathi 3e68c82171 apply changes from code review 2020-03-31 16:34:41 -04:00
Hubert Chathi 51bde23207 Merge branch 'develop' into uhoreg/check_sssss_passphrase 2020-03-31 10:41:39 -04:00
J. Ryan Stinnett 934ed37fdc Merge pull request #1293 from matrix-org/jryans/4s-prompt-overload
Receive private key for caching from the app layer
2020-03-31 11:04:51 +01:00
J. Ryan Stinnett 4a965f5408 Convert secret storage key creation to object 2020-03-31 10:45:02 +01:00
J. Ryan Stinnett 6343da33c3 Merge pull request #1292 from matrix-org/jryans/id-change-red
Track whether we have verified a user before
2020-03-31 10:07:20 +01:00
Hubert Chathi e6edee863f add ability to check symmetric SSSS key before we try to use it 2020-03-30 17:25:03 -04:00
J. Ryan Stinnett 9a1d62438d Receive private key for caching from the app layer
This passes through the new secret storage key as well as the key info so that
bootstrap can use it immediately without triggering an immediate prompt.

Part of https://github.com/vector-im/riot-web/issues/12867
2020-03-30 21:46:22 +01:00
J. Ryan Stinnett d2ba3039c7 Add missing awaits in bootstrap path 2020-03-30 21:46:22 +01:00
J. Ryan Stinnett b6ad4c10bc Track whether we have verified a user before
This tracks whether we have ever cross-signing verified a user before (at least
as far as the current device has ever observed). This info helps to present an
alert in case the user subsequently becomes unverified.

Part of https://github.com/vector-im/riot-web/issues/12808
2020-03-30 14:51:49 +01:00
J. Ryan Stinnett 93954d314e Remove unused variable in device list 2020-03-30 14:51:49 +01:00
J. Ryan Stinnett 282f85f1dd Add missing await when changing key IDs 2020-03-30 14:51:49 +01:00
RiotRobot 223d37ffce Merge branch 'master' into develop 2020-03-30 13:23:17 +01:00
RiotRobot 3d20388ca0 v5.2.0 2020-03-30 13:20:20 +01:00
RiotRobot 198c9d934e Prepare changelog for v5.2.0 2020-03-30 13:20:20 +01:00
J. Ryan Stinnett d43005d91e Merge pull request #1290 from matrix-org/dbkr/send_is_verified_rel
Fix isVerified returning false
2020-03-30 10:28:51 +01:00
Bruno Windels 02264b4572 Merge pull request #1222 from matrix-org/bwindels/fixtestfailure
Fix: error during tests
2020-03-30 08:36:15 +00:00
Bruno Windels add652f18e Merge pull request #1288 from matrix-org/bwindels/alwayssenddone
Send .done event for to_device verification
2020-03-27 16:30:28 +00:00
David Baker 1b9146b9e7 Merge pull request #1291 from matrix-org/dbkr/request_key_backup_key
Request the key backup key & restore backup
2020-03-27 15:43:16 +00:00
J. Ryan Stinnett 5178819b51 Merge pull request #1276 from mjattiot/hotfix/screen_sharing
Make screen sharing works on Chrome using getDisplayMedia()
2020-03-27 15:40:53 +00:00
David Baker f57c25ec27 Make test pass again 2020-03-27 15:13:46 +00:00
David Baker 794429b68b Request the key backup key & restore backup
After a successful verification with ourselves, request the key
backup key too and restore a key backup if we get it.

Also cache the key backup key when we cache the SSK & USK so we have
it available to share.

Fixes https://github.com/vector-im/riot-web/issues/12704
2020-03-27 14:40:15 +00:00
David Baker 983a04bb00 Merge pull request #1289 from matrix-org/dbkr/send_is_verified
Fix isVerified returning false
2020-03-27 14:28:47 +00:00
David Baker adbef16b9d Also pass the parameter in 2020-03-27 14:26:58 +00:00
David Baker 157ea49328 Fix isVerified returning false
which would cause key backups uploads to be missing is_verified
because it was set to `undefined` which would cause the backup to
fail

Fixes https://github.com/vector-im/riot-web/issues/12901
2020-03-27 14:26:53 +00:00
David Baker 17386e7aae Also pass the parameter in 2020-03-27 14:14:45 +00:00
David Baker cb19cd673f Fix isVerified returning false
which would cause key backups uploads to be missing is_verified
because it was set to `undefined` which would cause the backup to
fail

Fixes https://github.com/vector-im/riot-web/issues/12901
2020-03-27 14:11:32 +00:00
Bruno Windels 4f0a297cf3 remove obsolete comment 2020-03-27 12:08:43 +01:00
Bruno Windels 6553e331cd also send .done event for to_device verification 2020-03-27 12:04:48 +01:00
Bruno Windels 21908aea6c Merge pull request #1286 from matrix-org/bwindels/verifignoredupes
Fix: verification gets cancelled when event gets duplicated
2020-03-26 17:07:09 +00:00
David Baker 7c40798ee0 Merge pull request #1287 from matrix-org/dbkr/secret_request_front_door
Use requestSecret on the client to request secrets
2020-03-26 16:16:02 +00:00
David Baker 8cdc635cad lint and aldo indent in a more traditional way 2020-03-26 16:12:11 +00:00
David Baker 7f5ac072e6 Use requestSecret on the client to request secrets
Rather than accessing private method to get the secret storage
object (this was a bit confusing when I grepped for 'requestSecret'
and didn't find anything).
2020-03-26 16:01:35 +00:00
Travis Ralston d69af72c7a Merge pull request #1277 from Awesome-Technologies/develop
Allow guests to fetch TURN servers
2020-03-26 09:33:41 -06:00
Bruno Windels ece1e202de indenting 2020-03-26 15:04:28 +01:00
Bruno Windels 91f38a362d ignore duplicate verification events 2020-03-26 15:04:13 +01:00
RiotRobot 5a3cc314be v5.2.0-rc.1 2020-03-26 12:55:17 +00:00
RiotRobot 3dfaafd177 Prepare changelog for v5.2.0-rc.1 2020-03-26 12:55:16 +00:00
David Baker bdba61975b Merge pull request #1285 from matrix-org/dbkr/trust_cross_signing_flag
Add a flag for whether cross signing signatures are trusted
2020-03-26 12:19:08 +00:00
David Baker 3b9023ec2b add comment 2020-03-26 12:04:16 +00:00
David Baker 4dfc7958b6 lint 2020-03-26 10:07:17 +00:00
David Baker 2fad318726 Make the flag only affect trust of other people's devices 2020-03-26 09:58:05 +00:00
David Baker 480b0e64a6 lint 2020-03-25 18:44:55 +00:00
David Baker 6ec7b5d404 Add a flag for whether cross signing signatures are trusted 2020-03-25 18:36:08 +00:00
J. Ryan Stinnett 0781d78da8 Merge pull request #1282 from matrix-org/jryans/robust-secret-share
Cache user and self signing keys during bootstrap
2020-03-25 17:50:58 +00:00
Zoe 513a256ec1 Merge pull request #1283 from matrix-org/foldleft/remove-extra-promise
remove unnecessary promise
2020-03-25 12:52:42 +00:00
Zoe 9372790666 remove unnecessary promise 2020-03-25 11:47:59 +00:00
J. Ryan Stinnett a6532b7881 Fix logging lints 2020-03-24 18:34:05 +00:00
J. Ryan Stinnett cea3582ed1 Always attempt caching via bootstrap 2020-03-24 18:28:31 +00:00
J. Ryan Stinnett 6bd22a3e9c Add logging to secret request side 2020-03-24 17:44:44 +00:00
J. Ryan Stinnett 7b93b99054 Cache USK and SSK private key during bootstrap 2020-03-24 17:35:59 +00:00
Michael Albert c6eb1525b5 Allow guests to fetch TURN servers
Signed-off-by: Michael Albert <michael.albert@awesome-technologies.de>
2020-03-24 18:04:26 +01: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
mjattiot e25158975b formatting
Signed-off-by: mjattiot <mjattiot@opensense.fr>
2020-03-20 15:22:56 +01:00
mjattiot 7e028a82fc improved compatibility
Signed-off-by: mjattiot <mjattiot@opensense.fr>
2020-03-20 15:22:56 +01:00
mjattiot 17fe3e4dc1 init
Signed-off-by: mjattiot <mjattiot@opensense.fr>
2020-03-20 15:22:56 +01: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
Bruno Windels c4e449fc45 add null check for when there is no response 2020-02-20 11:42:33 +01: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
Bruno Windels 76c4875088 fix targetDevice renaming 2020-02-18 11:23:04 +01:00
48 changed files with 5565 additions and 2287 deletions
-58
View File
@@ -1,58 +0,0 @@
steps:
- label: ":eslint: JS Lint"
command:
- "yarn install"
- "yarn lint:js"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: ":tslint: TS Lint"
command:
- "yarn install"
- "yarn lint:ts"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: ":typescript: Types Lint"
command:
- "yarn install"
- "yarn lint:types"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: "🛠 Build"
command:
- "yarn install"
- "yarn build"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: ":jest: Tests"
command:
- "yarn install"
- "yarn test"
plugins:
- docker#v3.0.1:
image: "node:12"
- label: "📃 Docs"
command:
- "yarn install"
- "yarn gendoc"
plugins:
- docker#v3.0.1:
image: "node:12"
- wait
- label: "🐴 Trigger matrix-react-sdk"
trigger: "matrix-react-sdk"
branches: "develop"
build:
branch: "develop"
message: "[js-sdk] ${BUILDKITE_MESSAGE}"
async: true
+226
View File
@@ -1,3 +1,229 @@
Changes in [5.3.1-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.1-rc.3) (2020-04-17)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.1-rc.2...v5.3.1-rc.3)
* Fix cross-signing/SSSS reset
[\#1323](https://github.com/matrix-org/matrix-js-sdk/pull/1323)
* Fix: crash when backup key needs fixing from corruption issue
[\#1325](https://github.com/matrix-org/matrix-js-sdk/pull/1325)
Changes in [5.3.1-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.1-rc.2) (2020-04-16)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.1-rc.1...v5.3.1-rc.2)
* Implement QR code reciprocate for self-verification with untrusted MSK
[\#1321](https://github.com/matrix-org/matrix-js-sdk/pull/1321)
Changes in [5.3.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.1-rc.1) (2020-04-15)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.0-rc.1...v5.3.1-rc.1)
* Adapt release script for riot-desktop
[\#1319](https://github.com/matrix-org/matrix-js-sdk/pull/1319)
* Fix: prevent spurious notifications from indexer
[\#1318](https://github.com/matrix-org/matrix-js-sdk/pull/1318)
* Always create our own user object
[\#1317](https://github.com/matrix-org/matrix-js-sdk/pull/1317)
* Fix incorrect backup key format in SSSS
[\#1311](https://github.com/matrix-org/matrix-js-sdk/pull/1311)
* Fix e2ee crash after refreshing after having received a cross-singing key
reset
[\#1315](https://github.com/matrix-org/matrix-js-sdk/pull/1315)
* Fix: catch send errors in SAS verifier
[\#1314](https://github.com/matrix-org/matrix-js-sdk/pull/1314)
* Clear cross-signing keys when detecting the keys have changed
[\#1312](https://github.com/matrix-org/matrix-js-sdk/pull/1312)
* Upgrade deps
[\#1310](https://github.com/matrix-org/matrix-js-sdk/pull/1310)
Changes in [5.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.0-rc.1) (2020-04-08)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.2.0...v5.3.0-rc.1)
* Store key backup key in cache as Uint8Array
[\#1308](https://github.com/matrix-org/matrix-js-sdk/pull/1308)
* Use the correct request body for the /keys/query endpoint.
[\#1307](https://github.com/matrix-org/matrix-js-sdk/pull/1307)
* Avoid creating two devices on registration
[\#1305](https://github.com/matrix-org/matrix-js-sdk/pull/1305)
* Lower max-warnings to 81
[\#1306](https://github.com/matrix-org/matrix-js-sdk/pull/1306)
* Move key backup key creation before caching
[\#1303](https://github.com/matrix-org/matrix-js-sdk/pull/1303)
* Expose function to force-reset outgoing room key requests
[\#1298](https://github.com/matrix-org/matrix-js-sdk/pull/1298)
* Add isSelfVerification property to VerificationRequest
[\#1302](https://github.com/matrix-org/matrix-js-sdk/pull/1302)
* QR code reciprocation
[\#1297](https://github.com/matrix-org/matrix-js-sdk/pull/1297)
* Add ability to check symmetric SSSS key before we try to use it
[\#1294](https://github.com/matrix-org/matrix-js-sdk/pull/1294)
* Add some debug logging for events stuck to bottom of timeline
[\#1296](https://github.com/matrix-org/matrix-js-sdk/pull/1296)
* Fix: spontanous verification request cancellation under some circumstances
[\#1295](https://github.com/matrix-org/matrix-js-sdk/pull/1295)
* Receive private key for caching from the app layer
[\#1293](https://github.com/matrix-org/matrix-js-sdk/pull/1293)
* Track whether we have verified a user before
[\#1292](https://github.com/matrix-org/matrix-js-sdk/pull/1292)
* Fix: error during tests
[\#1222](https://github.com/matrix-org/matrix-js-sdk/pull/1222)
* Send .done event for to_device verification
[\#1288](https://github.com/matrix-org/matrix-js-sdk/pull/1288)
* Request the key backup key & restore backup
[\#1291](https://github.com/matrix-org/matrix-js-sdk/pull/1291)
* Make screen sharing works on Chrome using getDisplayMedia()
[\#1276](https://github.com/matrix-org/matrix-js-sdk/pull/1276)
* Fix isVerified returning false
[\#1289](https://github.com/matrix-org/matrix-js-sdk/pull/1289)
* Fix: verification gets cancelled when event gets duplicated
[\#1286](https://github.com/matrix-org/matrix-js-sdk/pull/1286)
* Use requestSecret on the client to request secrets
[\#1287](https://github.com/matrix-org/matrix-js-sdk/pull/1287)
* Allow guests to fetch TURN servers
[\#1277](https://github.com/matrix-org/matrix-js-sdk/pull/1277)
Changes in [5.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.2.0) (2020-03-30)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.2.0-rc.1...v5.2.0)
* Fix isVerified returning false
[\#1290](https://github.com/matrix-org/matrix-js-sdk/pull/1290)
Changes in [5.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.2.0-rc.1) (2020-03-26)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.1...v5.2.0-rc.1)
* Add a flag for whether cross signing signatures are trusted
[\#1285](https://github.com/matrix-org/matrix-js-sdk/pull/1285)
* Cache user and self signing keys during bootstrap
[\#1282](https://github.com/matrix-org/matrix-js-sdk/pull/1282)
* remove unnecessary promise
[\#1283](https://github.com/matrix-org/matrix-js-sdk/pull/1283)
* Functions to cache session backups key automatically
[\#1281](https://github.com/matrix-org/matrix-js-sdk/pull/1281)
* Add function for checking cross-signing is ready
[\#1279](https://github.com/matrix-org/matrix-js-sdk/pull/1279)
* Use symmetric encryption for SSSS
[\#1228](https://github.com/matrix-org/matrix-js-sdk/pull/1228)
* Migrate SSSS to use symmetric algorithm
[\#1238](https://github.com/matrix-org/matrix-js-sdk/pull/1238)
* Migration to symmetric SSSS
[\#1272](https://github.com/matrix-org/matrix-js-sdk/pull/1272)
* Reduce number of one-time-key requests
[\#1280](https://github.com/matrix-org/matrix-js-sdk/pull/1280)
* Fix: assume the requested method is supported by other party with to_device
[\#1275](https://github.com/matrix-org/matrix-js-sdk/pull/1275)
* Use checkDeviceTrust when computing untrusted devices
[\#1278](https://github.com/matrix-org/matrix-js-sdk/pull/1278)
* Add a store for backup keys
[\#1271](https://github.com/matrix-org/matrix-js-sdk/pull/1271)
* Upload only new device signature of master key
[\#1268](https://github.com/matrix-org/matrix-js-sdk/pull/1268)
* Expose prepareToEncrypt in the client API
[\#1270](https://github.com/matrix-org/matrix-js-sdk/pull/1270)
* Don't kill the whole device download if one device gives an error
[\#1269](https://github.com/matrix-org/matrix-js-sdk/pull/1269)
* Handle racing .start event during self verification
[\#1267](https://github.com/matrix-org/matrix-js-sdk/pull/1267)
* A crypto.keySignatureUploadFailure event reported the wrong source
[\#1266](https://github.com/matrix-org/matrix-js-sdk/pull/1266)
* Fix editing of unsent messages by waiting for actual event id
[\#1263](https://github.com/matrix-org/matrix-js-sdk/pull/1263)
* Fix: ensureOlmSessionsForDevices parameter format
[\#1264](https://github.com/matrix-org/matrix-js-sdk/pull/1264)
* Remove stuff that yarn install doesn't think we need
[\#1261](https://github.com/matrix-org/matrix-js-sdk/pull/1261)
* Fix: prevent error being thrown during sync in some cases
[\#1258](https://github.com/matrix-org/matrix-js-sdk/pull/1258)
* Force `is_verified` for key backups to bool and fix computation
[\#1259](https://github.com/matrix-org/matrix-js-sdk/pull/1259)
* Add a method for legacy single device verification, returning a verification
request
[\#1257](https://github.com/matrix-org/matrix-js-sdk/pull/1257)
* yarn upgrade
[\#1256](https://github.com/matrix-org/matrix-js-sdk/pull/1256)
Changes in [5.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.1) (2020-03-17)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.1-rc.1...v5.1.1)
* Fix: ensureOlmSessionsForDevices parameter format
[\#1265](https://github.com/matrix-org/matrix-js-sdk/pull/1265)
* Fix: prevent error being thrown during sync in some cases
[\#1262](https://github.com/matrix-org/matrix-js-sdk/pull/1262)
* Force `is_verified` for key backups to bool and fix computation
[\#1260](https://github.com/matrix-org/matrix-js-sdk/pull/1260)
Changes in [5.1.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.1-rc.1) (2020-03-11)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.0...v5.1.1-rc.1)
* refactor megolm encryption to improve perceived speed
[\#1252](https://github.com/matrix-org/matrix-js-sdk/pull/1252)
* Remove v1 identity server fallbacks
[\#1253](https://github.com/matrix-org/matrix-js-sdk/pull/1253)
* Use alt_aliases instead of local ones for room names
[\#1251](https://github.com/matrix-org/matrix-js-sdk/pull/1251)
* Upload cross-signing key signatures in the background
[\#1250](https://github.com/matrix-org/matrix-js-sdk/pull/1250)
* Fix secret sharing names to match spec
[\#1249](https://github.com/matrix-org/matrix-js-sdk/pull/1249)
* Cleanup: remove crypto.verification.start event
[\#1248](https://github.com/matrix-org/matrix-js-sdk/pull/1248)
* Fix regression in key backup request params
[\#1246](https://github.com/matrix-org/matrix-js-sdk/pull/1246)
* Use cross-signing trust to mark backups verified
[\#1244](https://github.com/matrix-org/matrix-js-sdk/pull/1244)
* Check both cross-signing and local trust for key sharing
[\#1243](https://github.com/matrix-org/matrix-js-sdk/pull/1243)
* Fixed up tests to match new way that crypto stores are created
[\#1242](https://github.com/matrix-org/matrix-js-sdk/pull/1242)
* Store USK and SSK locally
[\#1235](https://github.com/matrix-org/matrix-js-sdk/pull/1235)
* Use unpadded base64 for QR code secrets
[\#1236](https://github.com/matrix-org/matrix-js-sdk/pull/1236)
* Don't require .done event for finishing self-verification
[\#1239](https://github.com/matrix-org/matrix-js-sdk/pull/1239)
* Don't cancel as 3rd party in verification request
[\#1237](https://github.com/matrix-org/matrix-js-sdk/pull/1237)
* Verification: log when switching start event
[\#1234](https://github.com/matrix-org/matrix-js-sdk/pull/1234)
* Perform crypto store operations directly after transaction
[\#1233](https://github.com/matrix-org/matrix-js-sdk/pull/1233)
* More verification request logging
[\#1232](https://github.com/matrix-org/matrix-js-sdk/pull/1232)
* Upgrade deps
[\#1231](https://github.com/matrix-org/matrix-js-sdk/pull/1231)
Changes in [5.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.0) (2020-03-02)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.0-rc.1...v5.1.0)
* No changes since rc.1
Changes in [5.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.0-rc.1) (2020-02-26)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.0.1...v5.1.0-rc.1)
* Add latest dist-tag for releases
[\#1230](https://github.com/matrix-org/matrix-js-sdk/pull/1230)
* Add room method for alt_aliases
[\#1225](https://github.com/matrix-org/matrix-js-sdk/pull/1225)
* Remove buildkite pipeline
[\#1227](https://github.com/matrix-org/matrix-js-sdk/pull/1227)
* don't assume verify has been called when receiving a cancellation in
verifier
[\#1226](https://github.com/matrix-org/matrix-js-sdk/pull/1226)
* Reduce secret size for new binary packing
[\#1221](https://github.com/matrix-org/matrix-js-sdk/pull/1221)
* misc rageshake fixes
[\#1223](https://github.com/matrix-org/matrix-js-sdk/pull/1223)
* Fix cancelled historical requests not appearing as cancelled
[\#1220](https://github.com/matrix-org/matrix-js-sdk/pull/1220)
* Fix renaming error that broke QR code verification
[\#1217](https://github.com/matrix-org/matrix-js-sdk/pull/1217)
Changes in [5.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.0.1) (2020-02-19)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.0.0...v5.0.1)
+4 -2
View File
@@ -1,6 +1,6 @@
{
"name": "matrix-js-sdk",
"version": "5.0.1",
"version": "5.3.1-rc.3",
"description": "Matrix Client-Server SDK for Javascript",
"scripts": {
"prepare": "yarn build",
@@ -14,7 +14,7 @@
"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:js": "eslint --max-warnings 81 src spec",
"lint:types": "tsc --noEmit",
"lint:ts": "tslint --project ./tsconfig.json -t stylish",
"test": "jest spec/ --coverage --testEnvironment node",
@@ -78,7 +78,9 @@
"eslint-plugin-babel": "^5.3.0",
"eslint-plugin-jest": "^23.0.4",
"exorcist": "^1.0.1",
"fake-indexeddb": "^3.0.0",
"jest": "^24.9.0",
"jest-localstorage-mock": "^2.4.0",
"jsdoc": "^3.5.5",
"matrix-mock-request": "^1.2.3",
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
+29 -16
View File
@@ -38,6 +38,7 @@ $USAGE
-c changelog_file: specify name of file containing changelog
-x: skip updating the changelog
-z: skip generating the jsdoc
-n: skip publish to NPM
EOF
}
@@ -60,9 +61,10 @@ fi
skip_changelog=
skip_jsdoc=
skip_npm=
changelog_file="CHANGELOG.md"
expected_npm_user="matrixdotorg"
while getopts hc:u:xz f; do
while getopts hc:u:xzn f; do
case $f in
h)
help
@@ -77,6 +79,9 @@ while getopts hc:u:xz f; do
z)
skip_jsdoc=1
;;
n)
skip_npm=1
;;
u)
expected_npm_user="$OPTARG"
;;
@@ -96,10 +101,12 @@ fi
# 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.
actual_npm_user=`npm whoami`;
if [ $expected_npm_user != $actual_npm_user ]; then
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
exit 1
if [ -z "$skip_npm" ]; then
actual_npm_user=`npm whoami`;
if [ $expected_npm_user != $actual_npm_user ]; then
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
exit 1
fi
fi
# ignore leading v on release
@@ -294,14 +301,18 @@ fi
rm "${release_text}"
rm "${latest_changes}"
npm_publish_flags=''
if [ $prerelease -eq 1 ]; then
# Tag prereleases as `next` so the last stable release remains the default
npm_publish_flags='--tag next'
fi
# 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 $npm_publish_flags
# Tag both releases and prereleases as `next` so the last stable release remains
# the default.
if [ -z "$skip_npm" ]; then
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
fi
if [ -z "$skip_jsdoc" ]; then
echo "generating jsdocs"
@@ -336,8 +347,10 @@ if [ -z "$skip_jsdoc" ]; then
git push origin gh-pages
fi
# finally, merge master back onto develop
git checkout develop
git pull
git merge master
git push origin develop
# finally, merge master back onto develop (if it exists)
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
git checkout develop
git pull
git merge master
git push origin develop
fi
+1 -1
View File
@@ -185,7 +185,7 @@ TestClient.prototype.expectKeyQuery = function(response) {
200, (path, content) => {
Object.keys(response.device_keys).forEach((userId) => {
expect(content.device_keys[userId]).toEqual(
{},
[],
"Expected key query for " + userId + ", got " +
Object.keys(content.device_keys),
);
+2 -2
View File
@@ -70,7 +70,7 @@ function expectAliQueryKeys() {
aliTestClient.httpBackend.when("POST", "/keys/query")
.respond(200, function(path, content) {
expect(content.device_keys[bobUserId]).toEqual(
{},
[],
"Expected Alice to key query for " + bobUserId + ", got " +
Object.keys(content.device_keys),
);
@@ -98,7 +98,7 @@ function expectBobQueryKeys() {
"POST", "/keys/query",
).respond(200, function(path, content) {
expect(content.device_keys[aliUserId]).toEqual(
{},
[],
"Expected Bob to key query for " + aliUserId + ", got " +
Object.keys(content.device_keys),
);
+2 -2
View File
@@ -347,8 +347,8 @@ describe("MatrixClient", function() {
httpBackend.when("POST", "/keys/query").check(function(req) {
expect(req.data).toEqual({device_keys: {
'boris': {},
'chaz': {},
'boris': [],
'chaz': [],
}});
}).respond(200, {
device_keys: {
+248
View File
@@ -0,0 +1,248 @@
/*
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import '../../olm-loader';
import {
CrossSigningInfo,
createCryptoStoreCacheCallbacks,
} from '../../../src/crypto/CrossSigning';
import {
IndexedDBCryptoStore,
} from '../../../src/crypto/store/indexeddb-crypto-store';
import {MemoryCryptoStore} from '../../../src/crypto/store/memory-crypto-store';
import 'fake-indexeddb/auto';
import 'jest-localstorage-mock';
const userId = "@alice:example.com";
// Private key for tests only
const testKey = new Uint8Array([
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
]);
const types = [
{ type: "master", shouldCache: false },
{ type: "self_signing", shouldCache: true },
{ type: "user_signing", shouldCache: true },
{ type: "invalid", shouldCache: false },
];
const badKey = Uint8Array.from(testKey);
badKey[0] ^= 1;
const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
describe("CrossSigningInfo.getCrossSigningKey", function() {
if (!global.Olm) {
console.warn('Not running megolm backup unit tests: libolm not present');
return;
}
beforeAll(function() {
return global.Olm.init();
});
it("should throw if no callback is provided", async () => {
const info = new CrossSigningInfo(userId);
await expect(info.getCrossSigningKey("master")).rejects.toThrow();
});
it.each(types)("should throw if the callback returns falsey",
async ({type, shouldCache}) => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: () => false,
});
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
});
it("should throw if the expected key doesn't come back", async () => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: () => masterKeyPub,
});
await expect(info.getCrossSigningKey("master", "")).rejects.toThrow();
});
it("should return a key from its callback", async () => {
const info = new CrossSigningInfo(userId, {
getCrossSigningKey: () => testKey,
});
const [pubKey, ab] = await info.getCrossSigningKey("master", masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(ab).toEqual({a: 106712, b: 106712});
});
it.each(types)("should request a key from the cache callback (if set)" +
" and does not call app if one is found" +
" %o",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
return Promise.reject(new Error("Regular callback called"));
} else {
return Promise.resolve(testKey);
}
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
if (shouldCache) {
expect(getCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
}
});
it.each(types)("should store a key with the cache callback (if set)",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
if (shouldCache) {
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
}
});
it.each(types)("does not store a bad key to the cache",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ storeCrossSigningKeyCache },
);
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
});
it.each(types)("does not store a value to the cache if it came from the cache",
async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockImplementation(() => {
if (shouldCache) {
return Promise.reject(new Error("Regular callback called"));
} else {
return Promise.resolve(testKey);
}
});
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(
new Error("Tried to store a value from cache"),
);
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
});
it.each(types)("requests a key from the cache callback (if set) and then calls app" +
" if one is not found", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
});
it.each(types)("requests a key from the cache callback (if set) and then" +
" calls app if that key doesn't match", async ({ type, shouldCache }) => {
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(badKey);
const storeCrossSigningKeyCache = jest.fn();
const info = new CrossSigningInfo(
userId,
{ getCrossSigningKey },
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
);
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
expect(pubKey).toEqual(masterKeyPub);
expect(getCrossSigningKey.mock.calls.length).toBe(1);
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
/* Also expect that the cache gets updated */
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
});
});
/*
* Note that MemoryStore is weird. It's only used for testing - as far as I can tell,
* it's not possible to get one in normal execution unless you hack as we do here.
*/
describe.each([
["IndexedDBCryptoStore",
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore",
() => new IndexedDBCryptoStore(undefined, "tests")],
["MemoryCryptoStore", () => {
const store = new IndexedDBCryptoStore(undefined, "tests");
store._backend = new MemoryCryptoStore();
store._backendPromise = Promise.resolve(store._backend);
return store;
}],
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function(name, dbFactory) {
let store;
beforeAll(() => {
store = dbFactory();
});
beforeEach(async () => {
await store.deleteAllData();
});
it("should cache data to the store and retrieve it", async () => {
await store.startup();
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } =
createCryptoStoreCacheCallbacks(store);
await storeCrossSigningKeyCache("self_signing", testKey);
// If we've not saved anything, don't expect anything
// Definitely don't accidentally return the wrong key for the type
const nokey = await getCrossSigningKeyCache("self", "");
expect(nokey).toBeNull();
const key = await getCrossSigningKeyCache("self_signing", "");
expect(key).toEqual(testKey);
});
});
+24 -18
View File
@@ -297,6 +297,10 @@ describe("MegolmDecryption", function() {
},
}));
mockCrypto.checkDeviceTrust.mockReturnValue({
isVerified: () => false,
});
const megolmEncryption = new MegolmEncryption({
userId: '@user:id',
crypto: mockCrypto,
@@ -320,14 +324,14 @@ describe("MegolmDecryption", function() {
// this should have claimed a key for alice as it's starting a new session
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
['@alice:home.server'], false,
);
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
);
mockBaseApis.claimOneTimeKeys.mockReset();
@@ -516,21 +520,22 @@ describe("MegolmDecryption", function() {
};
};
let run = false;
aliceClient.sendToDevice = async (msgtype, contentMap) => {
run = true;
expect(msgtype).toBe("org.matrix.room_key.withheld");
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice: {
algorithm: "m.megolm.v1.aes-sha2",
code: 'm.no_olm',
reason: 'Unable to establish a secure channel.',
sender_key: aliceDevice.deviceCurve25519Key,
const sendPromise = new Promise((resolve, reject) => {
aliceClient.sendToDevice = async (msgtype, contentMap) => {
expect(msgtype).toBe("org.matrix.room_key.withheld");
expect(contentMap).toStrictEqual({
'@bob:example.com': {
bobdevice: {
algorithm: "m.megolm.v1.aes-sha2",
code: 'm.no_olm',
reason: 'Unable to establish a secure channel.',
sender_key: aliceDevice.deviceCurve25519Key,
},
},
},
});
};
});
resolve();
};
});
const event = new MatrixEvent({
type: "m.room.message",
@@ -540,8 +545,7 @@ describe("MegolmDecryption", function() {
content: {},
});
await aliceClient._crypto.encryptEvent(event, aliceRoom);
expect(run).toBe(true);
await sendPromise;
});
it("throws an error describing why it doesn't have a key", async function() {
@@ -598,6 +602,7 @@ describe("MegolmDecryption", function() {
aliceClient.initCrypto(),
bobClient.initCrypto(),
]);
aliceClient._crypto.downloadKeys = async () => {};
const bobDevice = bobClient._crypto._olmDevice;
const roomId = "!someroom";
@@ -649,6 +654,7 @@ describe("MegolmDecryption", function() {
bobClient.initCrypto(),
]);
const bobDevice = bobClient._crypto._olmDevice;
aliceClient._crypto.downloadKeys = async () => {};
const roomId = "!someroom";
+26
View File
@@ -541,5 +541,31 @@ describe("MegolmBackup", function() {
expect(res.clearEvent.content).toEqual('testytest');
});
});
it('has working cache functions', async function() {
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
await client._crypto.storeSessionBackupPrivateKey(key);
const result = await client._crypto.getSessionBackupPrivateKey();
expect(result).toEqual(key);
});
it('caches session backup keys as it encounters them', async function() {
const cachedNull = await client._crypto.getSessionBackupPrivateKey();
expect(cachedNull).toBeNull();
client._http.authedRequest = function() {
return Promise.resolve(KEY_BACKUP_DATA);
};
await new Promise((resolve) => {
client.restoreKeyBackupWithRecoveryKey(
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
ROOM_ID,
SESSION_ID,
BACKUP_INFO,
{ cacheCompleteCallback: resolve },
);
});
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
expect(cachedKey).not.toBeNull();
});
});
});
@@ -0,0 +1,87 @@
/*
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 {
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';
import {
ROOM_KEY_REQUEST_STATES,
} from '../../../src/crypto/OutgoingRoomKeyRequestManager';
const requests = [
{
requestId: "A",
requestBody: { session_id: "A", room_id: "A" },
state: ROOM_KEY_REQUEST_STATES.SENT,
},
{
requestId: "B",
requestBody: { session_id: "B", room_id: "B" },
state: ROOM_KEY_REQUEST_STATES.SENT,
},
{
requestId: "C",
requestBody: { session_id: "C", room_id: "C" },
state: ROOM_KEY_REQUEST_STATES.UNSENT,
},
];
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;
}],
])("Outgoing room key requests [%s]", function(name, dbFactory) {
let store;
beforeAll(async () => {
store = dbFactory();
await store.startup();
await Promise.all(requests.map((request) =>
store.getOrAddOutgoingRoomKeyRequest(request),
));
});
it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state",
async () => {
const r = await
store.getAllOutgoingRoomKeyRequestsByState(ROOM_KEY_REQUEST_STATES.SENT);
expect(r).toHaveLength(2);
requests.filter((e) => e.state == ROOM_KEY_REQUEST_STATES.SENT).forEach((e) => {
expect(r).toContainEqual(e);
});
});
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
async () => {
const r =
await store.getOutgoingRoomKeyRequestByState([ROOM_KEY_REQUEST_STATES.SENT]);
expect(r).not.toBeNull();
expect(r).not.toBeUndefined();
expect(r.state).toEqual(ROOM_KEY_REQUEST_STATES.SENT);
expect(requests).toContainEqual(r);
});
});
+725 -83
View File
@@ -16,10 +16,20 @@ limitations under the License.
import '../../olm-loader';
import * as olmlib from "../../../src/crypto/olmlib";
import {SECRET_STORAGE_ALGORITHM_V1} from "../../../src/crypto/SecretStorage";
import {SECRET_STORAGE_ALGORITHM_V1_AES} from "../../../src/crypto/SecretStorage";
import {MatrixEvent} from "../../../src/models/event";
import {TestClient} from '../../TestClient';
import {makeTestClients} from './verification/util';
import {encryptAES} from "../../../src/crypto/aes";
import * as utils from "../../../src/utils";
try {
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
console.log('nodejs was compiled without crypto support');
}
async function makeTestClient(userInfo, options) {
const client = (new TestClient(
@@ -40,6 +50,13 @@ async function makeTestClient(userInfo, options) {
return client;
}
// Wrapper around pkSign to return a signed object. pkSign returns the
// signature, rather than the signed object.
function sign(obj, key, userId) {
olmlib.pkSign(obj, key, userId);
return obj;
}
describe("Secrets", function() {
if (!global.Olm) {
console.warn('Not running megolm backup unit tests: libolm not present');
@@ -51,9 +68,8 @@ describe("Secrets", function() {
});
it("should store and retrieve a secret", async function() {
const decryption = new global.Olm.PkDecryption();
const pubkey = decryption.generate_key();
const privkey = decryption.get_private_key();
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const signing = new global.Olm.PkSigning();
const signingKey = signing.generate_seed();
@@ -69,7 +85,7 @@ describe("Secrets", function() {
const getKey = jest.fn(e => {
expect(Object.keys(e.keys)).toEqual(["abc"]);
return ['abc', privkey];
return ['abc', key];
});
const alice = await makeTestClient(
@@ -100,8 +116,7 @@ describe("Secrets", function() {
};
const keyAccountData = {
algorithm: SECRET_STORAGE_ALGORITHM_V1,
pubkey: pubkey,
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
};
await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master');
@@ -112,11 +127,11 @@ describe("Secrets", function() {
}),
]);
expect(await secretStorage.isStored("foo")).toBe(false);
expect(await secretStorage.isStored("foo")).toBeFalsy();
await secretStorage.store("foo", "bar", ["abc"]);
expect(await secretStorage.isStored("foo")).toBe(true);
expect(await secretStorage.isStored("foo")).toBeTruthy();
expect(await secretStorage.get("foo")).toBe("bar");
expect(getKey).toHaveBeenCalled();
@@ -149,6 +164,13 @@ describe("Secrets", function() {
});
it("should encrypt with default key if keys is null", async function() {
const key = new Uint8Array(16);
for (let i = 0; i < 16; i++) key[i] = i;
const getKey = jest.fn(e => {
expect(Object.keys(e.keys)).toEqual([newKeyId]);
return [newKeyId, key];
});
let keys = {};
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
@@ -156,6 +178,7 @@ describe("Secrets", function() {
cryptoCallbacks: {
getCrossSigningKey: t => keys[t],
saveCrossSigningKeys: k => keys = k,
getSecretStorageKey: getKey,
},
},
);
@@ -170,7 +193,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
@@ -251,89 +274,708 @@ describe("Secrets", function() {
expect(secret).toBe("bar");
});
it("bootstraps when no storage or cross-signing keys locally", async function() {
const bob = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
describe("bootstrap", function() {
// keys used in some of the tests
const XSK = new Uint8Array(
olmlib.decodeBase64("3lo2YdJugHjfE+Or7KJ47NuKbhE7AAGLgQ/dc19913Q="),
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
const event = new MatrixEvent({
type: eventType,
content: contents,
const XSPubKey = "DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0";
const USK = new Uint8Array(
olmlib.decodeBase64("lKWi3hJGUie5xxHgySoz8PHFnZv6wvNaud/p2shN9VU="),
);
const USPubKey = "CUpoiTtHiyXpUmd+3ohb7JVxAlUaOG1NYs9Jlx8soQU";
const SSK = new Uint8Array(
olmlib.decodeBase64("1R6JVlXX99UcfUZzKuCDGQgJTw8ur1/ofgPD8pp+96M="),
);
const SSPubKey = "0DfNsRDzEvkCLA0gD3m7VAGJ5VClhjEsewI35xq873Q";
const backupKey = new Uint8Array(
olmlib.decodeBase64(
"OTmOKrPsWP1qxmt0s2qat4vYxULG+U9cmFAWFco3A5U=",
),
);
const SSSSKey = new Uint8Array(
olmlib.decodeBase64(
"XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0=",
),
);
const SSSSPubKey = "v3A8HTypbccUm6jaRCRw5l+7lSdzQUACeY9xpQ5BVmE";
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];
});
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
};
await bob.bootstrapSecretStorage();
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
});
it("bootstraps when cross-signing keys in secret storage", async function() {
const decryption = new global.Olm.PkDecryption();
const storagePublicKey = decryption.generate_key();
const storagePrivateKey = decryption.get_private_key();
const bob = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
{
cryptoCallbacks: {
getSecretStorageKey: async request => {
const defaultKeyId = await bob.getDefaultSecretStorageKeyId();
expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
return [defaultKeyId, storagePrivateKey];
const bob = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
{
cryptoCallbacks: {
getSecretStorageKey: getKey,
},
},
},
);
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
const event = new MatrixEvent({
type: eventType,
content: contents,
});
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
};
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
const event = new MatrixEvent({
type: eventType,
content: contents,
});
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
};
bob._crypto.checkKeyBackup = async () => {};
await bob.bootstrapSecretStorage();
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
// Set up cross-signing keys from scratch with specific storage key
await bob.bootstrapSecretStorage({
createSecretStorageKey: async () => ({ pubkey: storagePublicKey }),
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
.toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
});
// Clear local cross-signing keys and read from secret storage
bob._crypto._deviceList.storeCrossSigningForUser(
"@bob:example.com",
crossSigning.toStorage(),
);
crossSigning.keys = {};
await bob.bootstrapSecretStorage();
it("bootstraps when cross-signing keys in secret storage", async function() {
const decryption = new global.Olm.PkDecryption();
const storagePublicKey = decryption.generate_key();
const storagePrivateKey = decryption.get_private_key();
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
const bob = await makeTestClient(
{
userId: "@bob:example.com",
deviceId: "bob1",
},
{
cryptoCallbacks: {
getSecretStorageKey: async request => {
const defaultKeyId = await bob.getDefaultSecretStorageKeyId();
expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
return [defaultKeyId, storagePrivateKey];
},
},
},
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
bob.setAccountData = async function(eventType, contents, callback) {
const event = new MatrixEvent({
type: eventType,
content: contents,
});
this.store.storeAccountDataEvents([
event,
]);
this.emit("accountData", event);
};
bob._crypto.checkKeyBackup = async () => {};
const crossSigning = bob._crypto._crossSigningInfo;
const secretStorage = bob._crypto._secretStorage;
// Set up cross-signing keys from scratch with specific storage key
await bob.bootstrapSecretStorage({
createSecretStorageKey: async () => ({
// `pubkey` not used anymore with symmetric 4S
keyInfo: { pubkey: storagePublicKey },
privateKey: storagePrivateKey,
}),
});
// Clear local cross-signing keys and read from secret storage
bob._crypto._deviceList.storeCrossSigningForUser(
"@bob:example.com",
crossSigning.toStorage(),
);
crossSigning.keys = {};
await bob.bootstrapSecretStorage();
expect(crossSigning.getId()).toBeTruthy();
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
.toBeTruthy();
expect(await secretStorage.hasKey()).toBeTruthy();
});
it("converts asymmetric SSSS to symmetric SSSS", async function() {
let crossSigningKeys = {};
const secretStorageKeys = {
"old_key_id": SSSSKey,
"new_key_id": SSSSKey,
};
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
{
cryptoCallbacks: {
getCrossSigningKey: t => crossSigningKeys[t],
saveCrossSigningKeys: k => crossSigningKeys = k,
getSecretStorageKey: ({keys}, name) => {
for (const keyId of Object.keys(keys)) {
if (secretStorageKeys[keyId]) {
return [keyId, secretStorageKeys[keyId]];
}
}
},
},
},
);
const encryption = new global.Olm.PkEncryption();
encryption.set_recipient_key(SSSSPubKey);
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: "m.secret_storage.default_key",
content: {
key: "old_key_id",
},
}),
new MatrixEvent({
type: "m.secret_storage.key.old_key_id",
content: sign({
algorithm: "m.secret_storage.v1.curve25519-aes-sha2",
passphrase: {
algorithm: "m.pbkdf2",
iterations: 500000,
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
},
pubkey: SSSSPubKey,
}, XSK, "@alice:example.com"),
}),
new MatrixEvent({
type: "m.cross_signing.master",
content: {
encrypted: {
old_key_id: encryption.encrypt(olmlib.encodeBase64(XSK)),
},
},
}),
new MatrixEvent({
type: "m.cross_signing.self_signing",
content: {
encrypted: {
old_key_id: encryption.encrypt(olmlib.encodeBase64(SSK)),
},
},
}),
new MatrixEvent({
type: "m.cross_signing.user_signing",
content: {
encrypted: {
old_key_id: encryption.encrypt(olmlib.encodeBase64(USK)),
},
},
}),
new MatrixEvent({
type: "m.megolm_backup.v1",
content: {
encrypted: {
old_key_id: encryption.encrypt(
olmlib.encodeBase64(backupKey),
),
},
},
}),
]);
encryption.free();
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign({
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
[`ed25519:${SSPubKey}`]: SSPubKey,
},
}, XSK, "@alice:example.com"),
user_signing: sign({
user_id: "@alice:example.com",
usage: ["user_signing"],
keys: {
[`ed25519:${USPubKey}`]: USPubKey,
},
}, XSK, "@alice:example.com"),
},
});
alice.getKeyBackupVersion = async () => {
return {
version: "1",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
auth_data: sign({
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
}, XSK, "@alice:example.com"),
};
};
const origAddSecretStorageKey = alice._crypto.addSecretStorageKey;
alice._crypto.addSecretStorageKey = async function(algorithm, opts, keyId) {
return await origAddSecretStorageKey.call(
alice._crypto, algorithm, opts, keyId || "new_key_id",
);
};
alice.setAccountData = async function(name, data) {
const event = new MatrixEvent({
type: name,
content: data,
});
alice.store.storeAccountDataEvents([event]);
this.emit("accountData", event);
};
await alice.bootstrapSecretStorage();
// it should create a new key with the same parameters
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
.toEqual({key: "new_key_id"});
const newKeyInfo = alice.getAccountData("m.secret_storage.key.new_key_id")
.getContent();
expect(newKeyInfo.algorithm)
.toEqual("m.secret_storage.v1.aes-hmac-sha2");
expect(newKeyInfo.passphrase).toEqual({
algorithm: "m.pbkdf2",
iterations: 500000,
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
});
expect(newKeyInfo).toHaveProperty("iv");
expect(newKeyInfo).toHaveProperty("mac");
expect(alice.checkSecretStorageKey(secretStorageKeys.new_key_id, newKeyInfo))
.toBeTruthy();
const crossSigningMaster = alice.getAccountData("m.cross_signing.master")
.getContent();
expect(crossSigningMaster.encrypted).toHaveProperty("new_key_id");
expect(crossSigningMaster.encrypted).not.toHaveProperty("old_key_id");
expect(await alice.getSecret("m.cross_signing.master"))
.toEqual(olmlib.encodeBase64(XSK));
const USKInfo = alice.getAccountData("m.cross_signing.self_signing")
.getContent();
expect(USKInfo.encrypted).toHaveProperty("new_key_id");
expect(USKInfo.encrypted).not.toHaveProperty("old_key_id");
expect(await alice.getSecret("m.cross_signing.user_signing"))
.toEqual(olmlib.encodeBase64(USK));
const SSKInfo = alice.getAccountData("m.cross_signing.self_signing")
.getContent();
expect(SSKInfo.encrypted).toHaveProperty("new_key_id");
expect(SSKInfo.encrypted).not.toHaveProperty("old_key_id");
expect(await alice.getSecret("m.cross_signing.self_signing"))
.toEqual(olmlib.encodeBase64(SSK));
const backupKeyInfo = alice.getAccountData("m.megolm_backup.v1")
.getContent();
expect(backupKeyInfo.encrypted).toHaveProperty("new_key_id");
expect(backupKeyInfo.encrypted).not.toHaveProperty("old_key_id");
expect(await alice.getSecret("m.megolm_backup.v1"))
.toEqual(olmlib.encodeBase64(backupKey));
});
it("converts asymmetric SSSS with passthrough", async function() {
let crossSigningKeys = {};
const secretStorageKeys = {
"old_key_id": SSSSKey,
"new_key_id": SSSSKey,
};
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
{
cryptoCallbacks: {
getCrossSigningKey: t => crossSigningKeys[t],
saveCrossSigningKeys: k => crossSigningKeys = k,
getSecretStorageKey: ({keys}, name) => {
for (const keyId of Object.keys(keys)) {
if (secretStorageKeys[keyId]) {
return [keyId, secretStorageKeys[keyId]];
}
}
},
},
},
);
const encryption = new global.Olm.PkEncryption();
encryption.set_recipient_key(SSSSPubKey);
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: "m.secret_storage.default_key",
content: {
key: "old_key_id",
},
}),
new MatrixEvent({
type: "m.secret_storage.key.old_key_id",
content: sign({
algorithm: "m.secret_storage.v1.curve25519-aes-sha2",
passphrase: {
algorithm: "m.pbkdf2",
iterations: 500000,
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
},
pubkey: "v3A8HTypbccUm6jaRCRw5l+7lSdzQUACeY9xpQ5BVmE",
}, XSK, "@alice:example.com"),
}),
new MatrixEvent({
type: "m.cross_signing.master",
content: {
encrypted: {
old_key_id: encryption.encrypt(olmlib.encodeBase64(XSK)),
},
},
}),
new MatrixEvent({
type: "m.cross_signing.self_signing",
content: {
encrypted: {
old_key_id: encryption.encrypt(olmlib.encodeBase64(SSK)),
},
},
}),
new MatrixEvent({
type: "m.cross_signing.user_signing",
content: {
encrypted: {
old_key_id: encryption.encrypt(olmlib.encodeBase64(USK)),
},
},
}),
new MatrixEvent({
type: "m.megolm_backup.v1",
content: {
encrypted: {
old_key_id: {
passthrough: true,
},
},
},
}),
]);
encryption.free();
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign({
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
[`ed25519:${SSPubKey}`]: SSPubKey,
},
}, XSK, "@alice:example.com"),
user_signing: sign({
user_id: "@alice:example.com",
usage: ["user_signing"],
keys: {
[`ed25519:${USPubKey}`]: USPubKey,
},
}, XSK, "@alice:example.com"),
},
});
alice.getKeyBackupVersion = async () => {
return {
version: "1",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
auth_data: sign({
public_key: "v3A8HTypbccUm6jaRCRw5l+7lSdzQUACeY9xpQ5BVmE",
}, XSK, "@alice:example.com"),
};
};
const origAddSecretStorageKey = alice._crypto.addSecretStorageKey;
alice._crypto.addSecretStorageKey = async function(algorithm, opts, keyId) {
return await origAddSecretStorageKey.call(
alice._crypto, algorithm, opts, keyId || "new_key_id",
);
};
alice.setAccountData = async function(name, data) {
const event = new MatrixEvent({
type: name,
content: data,
});
alice.store.storeAccountDataEvents([event]);
this.emit("accountData", event);
};
await alice.bootstrapSecretStorage();
// the new backup key should be the encoded SSSS key
const backupKeyInfo = alice.getAccountData("m.megolm_backup.v1")
.getContent();
expect(backupKeyInfo.encrypted).toHaveProperty("new_key_id");
expect(backupKeyInfo.encrypted).not.toHaveProperty("old_key_id");
expect(await alice.getSecret("m.megolm_backup.v1"))
.toEqual(olmlib.encodeBase64(SSSSKey));
});
it("adds passphrase checking if it's lacking", async function() {
let crossSigningKeys = {
master: XSK,
user_signing: USK,
self_signing: SSK,
};
const secretStorageKeys = {
key_id: SSSSKey,
};
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
{
cryptoCallbacks: {
getCrossSigningKey: t => crossSigningKeys[t],
saveCrossSigningKeys: k => crossSigningKeys = k,
getSecretStorageKey: ({keys}, name) => {
for (const keyId of Object.keys(keys)) {
if (secretStorageKeys[keyId]) {
return [keyId, secretStorageKeys[keyId]];
}
}
},
},
},
);
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: "m.secret_storage.default_key",
content: {
key: "key_id",
},
}),
new MatrixEvent({
type: "m.secret_storage.key.key_id",
content: {
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
passphrase: {
algorithm: "m.pbkdf2",
iterations: 500000,
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
},
},
}),
// we never use these values, other than checking that they
// exist, so just use dummy values
new MatrixEvent({
type: "m.cross_signing.master",
content: {
encrypted: {
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
},
},
}),
new MatrixEvent({
type: "m.cross_signing.self_signing",
content: {
encrypted: {
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
},
},
}),
new MatrixEvent({
type: "m.cross_signing.user_signing",
content: {
encrypted: {
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
},
},
}),
]);
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign({
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
[`ed25519:${SSPubKey}`]: SSPubKey,
},
}, XSK, "@alice:example.com"),
user_signing: sign({
user_id: "@alice:example.com",
usage: ["user_signing"],
keys: {
[`ed25519:${USPubKey}`]: USPubKey,
},
}, XSK, "@alice:example.com"),
},
});
alice.getKeyBackupVersion = async () => {
return {
version: "1",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
auth_data: sign({
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
}, XSK, "@alice:example.com"),
};
};
alice.setAccountData = async function(name, data) {
const event = new MatrixEvent({
type: name,
content: data,
});
alice.store.storeAccountDataEvents([event]);
this.emit("accountData", event);
};
await alice.bootstrapSecretStorage();
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
.toEqual({key: "key_id"});
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")
.getContent();
expect(keyInfo.algorithm)
.toEqual("m.secret_storage.v1.aes-hmac-sha2");
expect(keyInfo.passphrase).toEqual({
algorithm: "m.pbkdf2",
iterations: 500000,
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
});
expect(keyInfo).toHaveProperty("iv");
expect(keyInfo).toHaveProperty("mac");
expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo))
.toBeTruthy();
});
it("fixes backup keys in the wrong format", async function() {
let crossSigningKeys = {
master: XSK,
user_signing: USK,
self_signing: SSK,
};
const secretStorageKeys = {
key_id: SSSSKey,
};
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
{
cryptoCallbacks: {
getCrossSigningKey: t => crossSigningKeys[t],
saveCrossSigningKeys: k => crossSigningKeys = k,
getSecretStorageKey: ({keys}, name) => {
for (const keyId of Object.keys(keys)) {
if (secretStorageKeys[keyId]) {
return [keyId, secretStorageKeys[keyId]];
}
}
},
},
},
);
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: "m.secret_storage.default_key",
content: {
key: "key_id",
},
}),
new MatrixEvent({
type: "m.secret_storage.key.key_id",
content: {
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
passphrase: {
algorithm: "m.pbkdf2",
iterations: 500000,
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
},
},
}),
new MatrixEvent({
type: "m.cross_signing.master",
content: {
encrypted: {
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
},
},
}),
new MatrixEvent({
type: "m.cross_signing.self_signing",
content: {
encrypted: {
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
},
},
}),
new MatrixEvent({
type: "m.cross_signing.user_signing",
content: {
encrypted: {
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
},
},
}),
new MatrixEvent({
type: "m.megolm_backup.v1",
content: {
encrypted: {
key_id: await encryptAES(
"123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90",
secretStorageKeys.key_id, "m.megolm_backup.v1",
),
},
},
}),
]);
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
keys: {
master: {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
[`ed25519:${XSPubKey}`]: XSPubKey,
},
},
self_signing: sign({
user_id: "@alice:example.com",
usage: ["self_signing"],
keys: {
[`ed25519:${SSPubKey}`]: SSPubKey,
},
}, XSK, "@alice:example.com"),
user_signing: sign({
user_id: "@alice:example.com",
usage: ["user_signing"],
keys: {
[`ed25519:${USPubKey}`]: USPubKey,
},
}, XSK, "@alice:example.com"),
},
});
alice.getKeyBackupVersion = async () => {
return {
version: "1",
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
auth_data: sign({
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
}, XSK, "@alice:example.com"),
};
};
alice.setAccountData = async function(name, data) {
const event = new MatrixEvent({
type: name,
content: data,
});
alice.store.storeAccountDataEvents([event]);
this.emit("accountData", event);
};
await alice.bootstrapSecretStorage();
const backupKey = alice.getAccountData("m.megolm_backup.v1")
.getContent();
expect(backupKey.encrypted).toHaveProperty("key_id");
expect(await alice.getSecret("m.megolm_backup.v1"))
.toEqual("ey0GB1kB6jhOWgwiBUMIWg==");
});
});
});
+16 -7
View File
@@ -44,7 +44,16 @@ describe("SAS verification", function() {
});
it("should error on an unexpected event", async function() {
const sas = new SAS({}, "@alice:example.com", "ABCDEFG");
//channel, baseApis, userId, deviceId, startEvent, request
const request = {
onVerifierCancelled: function() {},
};
const channel = {
send: function() {
return Promise.resolve();
},
};
const sas = new SAS(channel, {}, "@alice:example.com", "ABCDEFG", null, request);
sas.handleEvent(new MatrixEvent({
sender: "@alice:example.com",
type: "es.inquisition",
@@ -122,8 +131,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) {
@@ -139,7 +148,7 @@ describe("SAS verification", function() {
}
}
});
resolve(verifier);
resolve(request.verifier);
});
});
@@ -339,11 +348,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);
});
});
@@ -0,0 +1,117 @@
/*
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 storeSessionBackupPrivateKey = jest.fn();
const restoreKeyBackupWithCache = jest.fn(() => Promise.resolve());
const client = {
_crypto: {
_crossSigningInfo,
_secretStorage,
storeSessionBackupPrivateKey,
getSessionBackupPrivateKey: () => null,
},
requestSecret: _secretStorage.request.bind(_secretStorage),
getUserId: () => userId,
getKeyBackupVersion: () => Promise.resolve({}),
restoreKeyBackupWithCache,
};
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 cross signing key and the key backup key */
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(2);
expect(_secretStorage.request.mock.calls.length).toBe(3);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1])
.toEqual(testKey);
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[1][1])
.toEqual(testKey);
expect(storeSessionBackupPrivateKey.mock.calls[0][0])
.toEqual(testKey);
expect(restoreKeyBackupWithCache).toHaveBeenCalled();
expect(result).toBeInstanceOf(Array);
expect(result[0][0]).toBe(testKeyPub);
expect(result[1][0]).toBe(testKeyPub);
});
});
@@ -120,7 +120,6 @@ async function distributeEvent(ownRequest, theirRequest, event) {
}
describe("verification request unit tests", function() {
beforeAll(function() {
setupWebcrypto();
});
+5 -10
View File
@@ -617,15 +617,10 @@ describe("Room", function() {
}, event: true,
})]);
};
const setAliases = function(aliases, stateKey) {
if (!stateKey) {
stateKey = aliases.length
? aliases[0].split(':').splice(1).join(':') // domain+port
: 'fibble';
}
const setAltAliases = function(aliases) {
room.addLiveEvents([utils.mkEvent({
type: "m.room.aliases", room: roomId, skey: stateKey, content: {
aliases: aliases,
type: "m.room.canonical_alias", room: roomId, skey: "", content: {
alt_aliases: aliases,
}, event: true,
})]);
};
@@ -862,7 +857,7 @@ describe("Room", function() {
"(invite join_rules) rooms if a room name doesn't exist.", function() {
const alias = "#room_alias:here";
setJoinRule("invite");
setAliases([alias, "#another:here"]);
setAltAliases([alias, "#another:here"]);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
@@ -872,7 +867,7 @@ describe("Room", function() {
"(public join_rules) rooms if a room name doesn't exist.", function() {
const alias = "#room_alias:here";
setJoinRule("public");
setAliases([alias, "#another:here"]);
setAltAliases([alias, "#another:here"]);
room.recalculate();
const name = room.name;
expect(name).toEqual(alias);
+4 -2
View File
@@ -502,10 +502,12 @@ export class AutoDiscovery {
request(
{ method: "GET", uri: url, timeout: 5000 },
(err, response, body) => {
if (err || response.statusCode < 200 || response.statusCode >= 300) {
if (err || response &&
(response.statusCode < 200 || response.statusCode >= 300)
) {
let action = "FAIL_PROMPT";
let reason = (err ? err.message : null) || "General failure";
if (response.statusCode === 404) {
if (response && response.statusCode === 404) {
action = "IGNORE";
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
}
+65 -153
View File
@@ -28,13 +28,7 @@ import {SERVICE_TYPES} from './service-types';
import {logger} from './logger';
import {PushProcessor} from "./pushprocessor";
import * as utils from "./utils";
import {
MatrixHttpApi,
PREFIX_IDENTITY_V1,
PREFIX_IDENTITY_V2,
PREFIX_R0,
PREFIX_UNSTABLE,
} from "./http-api";
import {MatrixHttpApi, PREFIX_IDENTITY_V2, PREFIX_R0, PREFIX_UNSTABLE} from "./http-api";
function termsUrlForService(serviceType, baseUrl) {
switch (serviceType) {
@@ -1763,7 +1757,7 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
content.token = opts.token;
}
userIds.forEach((u) => {
content.device_keys[u] = {};
content.device_keys[u] = [];
});
return this._http.authedRequest(undefined, "POST", "/keys/query", undefined, content);
@@ -1776,10 +1770,13 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
*
* @param {string} [key_algorithm = signed_curve25519] desired key type
*
* @param {number} [timeout] the time (in milliseconds) to wait for keys from remote
* servers
*
* @return {Promise} Resolves: result object. Rejects: with
* an error response ({@link module:http-api.MatrixError}).
*/
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm, timeout) {
const queries = {};
if (key_algorithm === undefined) {
@@ -1794,6 +1791,9 @@ MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
query[deviceId] = key_algorithm;
}
const content = {one_time_keys: queries};
if (timeout) {
content.timeout = timeout;
}
const path = "/keys/claim";
return this._http.authedRequest(undefined, "POST", path, undefined, content);
};
@@ -1895,28 +1895,10 @@ MatrixBaseApis.prototype.requestEmailToken = async function(
next_link: nextLink,
};
try {
const response = await this._http.idServerRequest(
undefined, "POST", "/validate/email/requestToken",
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, response);
return response;
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "POST", "/validate/email/requestToken",
params, PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err);
throw err;
}
return await this._http.idServerRequest(
callback, "POST", "/validate/email/requestToken",
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
};
/**
@@ -1963,28 +1945,10 @@ MatrixBaseApis.prototype.requestMsisdnToken = async function(
next_link: nextLink,
};
try {
const response = await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/requestToken",
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, response);
return response;
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "POST", "/validate/msisdn/requestToken",
params, PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err);
throw err;
}
return await this._http.idServerRequest(
callback, "POST", "/validate/msisdn/requestToken",
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
};
/**
@@ -2018,24 +1982,10 @@ MatrixBaseApis.prototype.submitMsisdnToken = async function(
token: msisdnToken,
};
try {
return await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, PREFIX_IDENTITY_V1,
);
}
throw err;
}
return await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, PREFIX_IDENTITY_V2, identityAccessToken,
);
};
/**
@@ -2190,53 +2140,32 @@ MatrixBaseApis.prototype.lookupThreePid = async function(
callback,
identityAccessToken,
) {
try {
// Note: we're using the V2 API by calling this function, but our
// function contract requires a V1 response. We therefore have to
// convert it manually.
const response = await this.identityHashedLookup(
[[address, medium]], identityAccessToken,
);
const result = response.find(p => p.address === address);
if (!result) {
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, {});
return {};
}
const mapping = {
address,
medium,
mxid: result.mxid,
// We can't reasonably fill these parameters:
// not_before
// not_after
// ts
// signatures
};
// TODO: Fold callback into above call once v1 path below is removed
if (callback) callback(null, mapping);
return mapping;
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
const params = {
medium: medium,
address: address,
};
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
callback, "GET", "/lookup",
params, PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err, undefined);
throw err;
// Note: we're using the V2 API by calling this function, but our
// function contract requires a V1 response. We therefore have to
// convert it manually.
const response = await this.identityHashedLookup(
[[address, medium]], identityAccessToken,
);
const result = response.find(p => p.address === address);
if (!result) {
if (callback) callback(null, {});
return {};
}
const mapping = {
address,
medium,
mxid: result.mxid,
// We can't reasonably fill these parameters:
// not_before
// not_after
// ts
// signatures
};
if (callback) callback(null, mapping);
return mapping;
};
/**
@@ -2254,46 +2183,29 @@ MatrixBaseApis.prototype.bulkLookupThreePids = async function(
query,
identityAccessToken,
) {
try {
// Note: we're using the V2 API by calling this function, but our
// function contract requires a V1 response. We therefore have to
// convert it manually.
const response = await this.identityHashedLookup(
// We have to reverse the query order to get [address, medium] pairs
query.map(p => [p[1], p[0]]), identityAccessToken,
);
// Note: we're using the V2 API by calling this function, but our
// function contract requires a V1 response. We therefore have to
// convert it manually.
const response = await this.identityHashedLookup(
// We have to reverse the query order to get [address, medium] pairs
query.map(p => [p[1], p[0]]), identityAccessToken,
);
const v1results = [];
for (const mapping of response) {
const originalQuery = query.find(p => p[1] === mapping.address);
if (!originalQuery) {
throw new Error("Identity sever returned unexpected results");
}
v1results.push([
originalQuery[0], // medium
mapping.address,
mapping.mxid,
]);
const v1results = [];
for (const mapping of response) {
const originalQuery = query.find(p => p[1] === mapping.address);
if (!originalQuery) {
throw new Error("Identity sever returned unexpected results");
}
return {threepids: v1results};
} catch (err) {
if (err.cors === "rejected" || err.httpStatus === 404) {
// Fall back to deprecated v1 API for now
// TODO: Remove this path once v2 is only supported version
// See https://github.com/vector-im/riot-web/issues/10443
const params = {
threepids: query,
};
logger.warn("IS doesn't support v2, falling back to deprecated v1");
return await this._http.idServerRequest(
undefined, "POST", "/bulk_lookup", params,
PREFIX_IDENTITY_V1, identityAccessToken,
);
}
throw err;
v1results.push([
originalQuery[0], // medium
mapping.address,
mapping.mxid,
]);
}
return {threepids: v1results};
};
/**
+173 -41
View File
@@ -42,12 +42,13 @@ import * as olmlib from "./crypto/olmlib";
import {ReEmitter} from './ReEmitter';
import {RoomList} from './crypto/RoomList';
import {logger} from './logger';
import {Crypto, isCryptoAvailable} from './crypto';
import {Crypto, isCryptoAvailable, fixBackupKey} from './crypto';
import {decodeRecoveryKey} from './crypto/recoverykey';
import {keyFromAuthData} from './crypto/key_passphrase';
import {randomString} from './randomstring';
import {PushProcessor} from "./pushprocessor";
import {encodeBase64, decodeBase64} from "./crypto/olmlib";
import { User } from "./models/user";
const SCROLLBACK_DELAY_MS = 3000;
export const CRYPTO_ENABLED = isCryptoAvailable();
@@ -217,8 +218,10 @@ function keyFromRecoverySession(session, decryptionKey) {
* Args:
* {object} keys Information about the keys:
* {
* <key name>: {
* pubkey: {UInt8Array}
* keys: {
* <key name>: {
* pubkey: {UInt8Array}
* }, ...
* }
* }
* {string} name the name of the value we want to read out of SSSS, for UI purposes.
@@ -281,13 +284,19 @@ export function MatrixClient(opts) {
this.scheduler = opts.scheduler;
if (this.scheduler) {
const self = this;
this.scheduler.setProcessFunction(function(eventToSend) {
this.scheduler.setProcessFunction(async function(eventToSend) {
const room = self.getRoom(eventToSend.getRoomId());
if (eventToSend.status !== EventStatus.SENDING) {
_updatePendingEventStatus(room, eventToSend,
EventStatus.SENDING);
}
return _sendEventHttpRequest(self, eventToSend);
const res = await _sendEventHttpRequest(self, eventToSend);
if (room) {
// ensure we update pending event before the next scheduler run so that any listeners to event id
// updates on the synchronous event emitter get a chance to run first.
room.updatePendingEvent(eventToSend, EventStatus.SENT, res.event_id);
}
return res;
});
}
this.clientRunning = false;
@@ -407,9 +416,9 @@ export function MatrixClient(opts) {
break;
}
highlightCount += this.getPushActionsForEvent(
event,
).tweaks.highlight ? 1 : 0;
const pushActions = this.getPushActionsForEvent(event);
highlightCount += pushActions.tweaks &&
pushActions.tweaks.highlight ? 1 : 0;
}
// Note: we don't need to handle 'total' notifications because the counts
@@ -685,6 +694,9 @@ MatrixClient.prototype.initCrypto = async function() {
throw new Error(`Cannot enable encryption: no cryptoStore provided`);
}
logger.log("Crypto: Starting up crypto store...");
await this._cryptoStore.startup();
// initialise the list of encrypted rooms (whether or not crypto is enabled)
logger.log("Crypto: initialising roomlist...");
await this._roomList.init();
@@ -749,7 +761,6 @@ MatrixClient.prototype.isCryptoEnabled = function() {
return this._crypto !== null;
};
/**
* Get the Ed25519 key for this device
*
@@ -763,6 +774,19 @@ MatrixClient.prototype.getDeviceEd25519Key = function() {
return this._crypto.getDeviceEd25519Key();
};
/**
* Get the Curve25519 key for this device
*
* @return {?string} base64-encoded curve25519 key. Null if crypto is
* disabled.
*/
MatrixClient.prototype.getDeviceCurve25519Key = function() {
if (!this._crypto) {
return null;
}
return this._crypto.getDeviceCurve25519Key();
};
/**
* Upload the device keys to the homeserver.
* @return {object} A promise that will resolve when the keys are uploaded.
@@ -1111,6 +1135,12 @@ function wrapCryptoFuncs(MatrixClient, names) {
* @returns {boolean} true if the key matches, otherwise false
*/
/**
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
*
* @param {module:models/room} room the room the event is in
*/
wrapCryptoFuncs(MatrixClient, [
"resetCrossSigningKeys",
"getCrossSigningId",
@@ -1119,6 +1149,11 @@ wrapCryptoFuncs(MatrixClient, [
"checkDeviceTrust",
"checkOwnCrossSigningTrust",
"checkCrossSigningPrivateKey",
"legacyDeviceVerification",
"prepareToEncrypt",
"isCrossSigningReady",
"getCryptoTrustCrossSignedDevices",
"setCryptoTrustCrossSignedDevices",
]);
/**
@@ -1145,7 +1180,9 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
* @param {string} password Passphrase string that can be entered by the user
* when restoring the backup as an alternative to entering the recovery key.
* Optional.
* @returns {Promise<String>} The user-facing recovery key string.
* @returns {Promise<Object>} Object with public key metadata, encoded private
* recovery key which should be disposed of after displaying to the user,
* and raw private key to avoid round tripping if needed.
*/
/**
@@ -1217,7 +1254,9 @@ MatrixClient.prototype.checkEventSenderTrust = async function(event) {
* @param {boolean} checkKey check if the secret is encrypted by a trusted
* key
*
* @return {boolean} whether or not the secret is stored
* @return {object?} map of key name to key info the secret is encrypted
* with, or null if it is not present or not encrypted with a trusted
* key
*/
/**
@@ -1265,12 +1304,14 @@ wrapCryptoFuncs(MatrixClient, [
"bootstrapSecretStorage",
"addSecretStorageKey",
"hasSecretStorageKey",
"secretStorageKeyNeedsUpgrade",
"storeSecret",
"getSecret",
"isSecretStored",
"requestSecret",
"getDefaultSecretStorageKeyId",
"setDefaultSecretStorageKeyId",
"checkSecretStorageKey",
"checkSecretStoragePrivateKey",
]);
@@ -1526,7 +1567,7 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(
throw new Error("End-to-end encryption disabled");
}
const [keyInfo, encodedPrivateKey, privateKey] =
const { keyInfo, encodedPrivateKey, privateKey } =
await this.createRecoveryKeyFromPassphrase(password);
if (secureSecretStorage) {
@@ -1551,7 +1592,9 @@ MatrixClient.prototype.prepareKeyBackupVersion = async function(
/**
* Check whether the key backup private key is stored in secret storage.
* @return {Promise<boolean>} Whether the backup key is stored.
* @return {Promise<object?>} map of key name to key info the secret is
* encrypted with, or null if it is not present or not encrypted with a
* trusted key
*/
MatrixClient.prototype.isKeyBackupKeyStored = async function() {
return this.isSecretStored("m.megolm_backup.v1", false /* checkKey */);
@@ -1704,6 +1747,35 @@ MatrixClient.prototype.isValidRecoveryKey = function(recoveryKey) {
}
};
/**
* Get the raw key for a key backup from the password
* Used when migrating key backups into SSSS
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @param {string} password Passphrase
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @return {Promise<Buffer>} key backup key
*/
MatrixClient.prototype.keyBackupKeyFromPassword = function(
password, backupInfo,
) {
return keyFromAuthData(backupInfo.auth_data, password);
};
/**
* Get the raw key for a key backup from the recovery key
* Used when migrating key backups into SSSS
*
* The cross-signing API is currently UNSTABLE and may change without notice.
*
* @param {string} recoveryKey The recovery key
* @return {Buffer} key backup key
*/
MatrixClient.prototype.keyBackupKeyFromRecoveryKey = function(recoveryKey) {
return decodeRecoveryKey(recoveryKey);
};
MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
/**
@@ -1715,15 +1787,16 @@ MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY = 'RESTORE_BACKUP_ERROR_BAD_KEY';
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @param {object} opts Optional params such as callbacks
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
password, targetRoomId, targetSessionId, backupInfo,
password, targetRoomId, targetSessionId, backupInfo, opts,
) {
const privKey = await keyFromAuthData(backupInfo.auth_data, password);
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, backupInfo,
privKey, targetRoomId, targetSessionId, backupInfo, opts,
);
};
@@ -1736,15 +1809,26 @@ MatrixClient.prototype.restoreKeyBackupWithPassword = async function(
* Restores all rooms if omitted.
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} opts Optional params such as callbacks
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithSecretStorage = async function(
backupInfo, targetRoomId, targetSessionId,
backupInfo, targetRoomId, targetSessionId, opts,
) {
const privKey = decodeBase64(await this.getSecret("m.megolm_backup.v1"));
const storedKey = await this.getSecret("m.megolm_backup.v1");
// ensure that the key is in the right format. If not, fix the key and
// store the fixed version
const fixedKey = fixBackupKey(storedKey);
if (fixedKey) {
const [keyId] = await this._crypto.getSecretStorageKey();
await this.storeSecret("m.megolm_backup.v1", fixedKey, [keyId]);
}
const privKey = decodeBase64(fixedKey || storedKey);
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, backupInfo,
privKey, targetRoomId, targetSessionId, backupInfo, opts,
);
};
@@ -1757,20 +1841,47 @@ MatrixClient.prototype.restoreKeyBackupWithSecretStorage = async function(
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @param {object} opts Optional params such as callbacks
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithRecoveryKey = function(
recoveryKey, targetRoomId, targetSessionId, backupInfo,
recoveryKey, targetRoomId, targetSessionId, backupInfo, opts,
) {
const privKey = decodeRecoveryKey(recoveryKey);
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, backupInfo,
privKey, targetRoomId, targetSessionId, backupInfo, opts,
);
};
/**
* Restore from an existing key backup using a cached key, or fail
*
* @param {string} [targetRoomId] Room ID to target a specific room.
* Restores all rooms if omitted.
* @param {string} [targetSessionId] Session ID to target a specific session.
* Restores all sessions if omitted.
* @param {object} backupInfo Backup metadata from `checkKeyBackup`
* @param {object} opts Optional params such as callbacks
* @return {Promise<object>} Status of restoration with `total` and `imported`
* key counts.
*/
MatrixClient.prototype.restoreKeyBackupWithCache = async function(
targetRoomId, targetSessionId, backupInfo, opts,
) {
const privKey = await this._crypto.getSessionBackupPrivateKey();
if (!privKey) {
throw new Error("Couldn't get key");
}
return this._restoreKeyBackup(
privKey, targetRoomId, targetSessionId, backupInfo, opts,
);
};
MatrixClient.prototype._restoreKeyBackup = function(
privKey, targetRoomId, targetSessionId, backupInfo,
{ cacheCompleteCallback }={}, // For sequencing during tests
) {
if (this._crypto === null) {
throw new Error("End-to-end encryption disabled");
@@ -1798,6 +1909,13 @@ MatrixClient.prototype._restoreKeyBackup = function(
return Promise.reject({errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY});
}
// Cache the key, if possible.
// This is async.
this._crypto.storeSessionBackupPrivateKey(privKey)
.catch((e) => {
console.warn("Error caching session backup key:", e);
}).then(cacheCompleteCallback);
return this._http.authedRequest(
undefined, "GET", path.path, path.queryData, undefined,
{prefix: PREFIX_UNSTABLE},
@@ -2395,7 +2513,7 @@ function _sendEvent(client, room, event, callback) {
let promise;
// this event may be queued
if (client.scheduler) {
// if this returns a promsie then the scheduler has control now and will
// if this returns a promise then the scheduler has control now and will
// resolve/reject when it is done. Internally, the scheduler will invoke
// processFn which is set to this._sendEventHttpRequest so the same code
// path is executed regardless.
@@ -2409,12 +2527,15 @@ function _sendEvent(client, room, event, callback) {
if (!promise) {
promise = _sendEventHttpRequest(client, event);
if (room) {
promise = promise.then(res => {
room.updatePendingEvent(event, EventStatus.SENT, res.event_id);
return res;
});
}
}
return promise;
}).then(function(res) { // the request was sent OK
if (room) {
room.updatePendingEvent(event, EventStatus.SENT, res.event_id);
}
if (callback) {
callback(null, res);
}
@@ -4624,6 +4745,13 @@ MatrixClient.prototype.startClient = async function(opts) {
};
}
// Create our own user object artificially (instead of waiting for sync)
// so it's always available, even if the user is not in any rooms etc.
const userId = this.getUserId();
if (userId) {
this.store.storeUser(new User(userId));
}
if (this._crypto) {
this._crypto.uploadDeviceKeys();
this._crypto.start();
@@ -5097,9 +5225,6 @@ function checkTurnServers(client) {
if (!client._supportsVoip) {
return;
}
if (client.isGuest()) {
return; // guests can't access TURN servers
}
client.turnServer().then(function(res) {
if (res.uris) {
@@ -5140,17 +5265,20 @@ function _resolve(callback, resolve, res) {
resolve(res);
}
function _PojoToMatrixEventMapper(client) {
function _PojoToMatrixEventMapper(client, options) {
const preventReEmit = Boolean(options && options.preventReEmit);
function mapper(plainOldJsObject) {
const event = new MatrixEvent(plainOldJsObject);
if (event.isEncrypted()) {
client.reEmitter.reEmit(event, [
"Event.decrypted",
]);
if (!preventReEmit) {
client.reEmitter.reEmit(event, [
"Event.decrypted",
]);
}
event.attemptDecryption(client._crypto);
}
const room = client.getRoom(event.getRoomId());
if (room) {
if (room && !preventReEmit) {
room.reEmitter.reEmit(event, ["Event.replaced"]);
}
return event;
@@ -5159,10 +5287,21 @@ function _PojoToMatrixEventMapper(client) {
}
/**
* @param {object} [options]
* @param {bool} options.preventReEmit don't reemit events emitted on an event mapped by this mapper on the client
* @return {Function}
*/
MatrixClient.prototype.getEventMapper = function() {
return _PojoToMatrixEventMapper(this);
MatrixClient.prototype.getEventMapper = function(options = undefined) {
return _PojoToMatrixEventMapper(this, options);
};
/**
* The app may wish to see if we have a key cached without
* triggering a user interaction.
* @return {object}
*/
MatrixClient.prototype.getCrossSigningCacheCallbacks = function() {
return this._crypto && this._crypto._crossSigningInfo.getCacheCallbacks();
};
// Identity Server Operations
@@ -5481,13 +5620,6 @@ MatrixClient.prototype.generateClientSecret = function() {
* reject the key verification.
*/
/**
* Fires when a key verification started message is received.
* @event module:client~MatrixClient#"crypto.verification.start"
* @param {module:crypto/verification/Base} verifier a verifier object to
* perform the key verification
*/
/**
* Fires when a secret request has been cancelled. If the client is prompting
* the user to ask whether they want to share a secret, the prompt can be
+155 -28
View File
@@ -23,6 +23,7 @@ limitations under the License.
import {decodeBase64, encodeBase64, pkSign, pkVerify} from './olmlib';
import {EventEmitter} from 'events';
import {logger} from '../logger';
import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store';
function publicKeyFromKeyInfo(keyInfo) {
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
@@ -40,8 +41,9 @@ export class CrossSigningInfo extends EventEmitter {
* @param {string} userId the user that the information is about
* @param {object} callbacks Callbacks used to interact with the app
* Requires getCrossSigningKey and saveCrossSigningKeys
* @param {object} cacheCallbacks Callbacks used to interact with the cache
*/
constructor(userId, callbacks) {
constructor(userId, callbacks, cacheCallbacks) {
super();
// you can't change the userId
@@ -50,8 +52,15 @@ export class CrossSigningInfo extends EventEmitter {
value: userId,
});
this._callbacks = callbacks || {};
this._cacheCallbacks = cacheCallbacks || {};
this.keys = {};
this.firstUse = true;
// This tracks whether we've ever verified this user with any identity.
// When you verify a user, any devices online at the time that receive
// the verifying signature via the homeserver will latch this to true
// and can use it in the future to detect cases where the user has
// become unverifed later for any reason.
this.crossSigningVerifiedBefore = false;
}
/**
@@ -62,6 +71,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 +81,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) {
@@ -102,6 +138,7 @@ export class CrossSigningInfo extends EventEmitter {
return {
keys: this.keys,
firstUse: this.firstUse,
crossSigningVerifiedBefore: this.crossSigningVerifiedBefore,
};
}
@@ -111,14 +148,28 @@ export class CrossSigningInfo extends EventEmitter {
* want to know this anyway...
*
* @param {SecretStorage} secretStorage The secret store using account data
* @returns {boolean} Whether all private keys were found in storage
* @returns {object} map of key name to key info the secret is encrypted
* with, or null if it is not present or not encrypted with a trusted
* key
*/
async isStoredInSecretStorage(secretStorage) {
let stored = true;
for (const type of ["master", "self_signing", "user_signing"]) {
stored &= await secretStorage.isStored(`m.cross_signing.${type}`, false);
// check what SSSS keys have encrypted the master key (if any)
const stored =
await secretStorage.isStored("m.cross_signing.master", false) || {};
// then check which of those SSSS keys have also encrypted the SSK and USK
function intersect(s) {
for (const k of Object.keys(stored)) {
if (!s[k]) {
delete stored[k];
}
}
}
return stored;
for (const type of ["self_signing", "user_signing"]) {
intersect(
await secretStorage.isStored(`m.cross_signing.${type}`, false) || {},
);
}
return Object.keys(stored).length ? stored : null;
}
/**
@@ -259,6 +310,13 @@ export class CrossSigningInfo extends EventEmitter {
}
}
/**
* unsets the keys, used when another session has reset the keys, to disable cross-signing
*/
clearKeys() {
this.keys = {};
}
setKeys(keys) {
const signingKeys = {};
if (keys.master) {
@@ -331,6 +389,14 @@ export class CrossSigningInfo extends EventEmitter {
}
}
updateCrossSigningVerifiedBefore(isCrossSigningVerified) {
// It is critical that this value latches forward from false to true but
// never back to false to avoid a downgrade attack.
if (!this.crossSigningVerifiedBefore && isCrossSigningVerified) {
this.crossSigningVerifiedBefore = true;
}
}
async signObject(data, type) {
if (!this.keys[type]) {
throw new Error(
@@ -389,13 +455,13 @@ export class CrossSigningInfo extends EventEmitter {
&& this.getId("self_signing")
&& this.getId("self_signing") === userCrossSigning.getId("self_signing")
) {
return new UserTrustLevel(true, this.firstUse);
return new UserTrustLevel(true, true, this.firstUse);
}
if (!this.keys.user_signing) {
// If there's no user signing key, they can't possibly be verified.
// They may be TOFU trusted though.
return new UserTrustLevel(false, userCrossSigning.firstUse);
return new UserTrustLevel(false, false, userCrossSigning.firstUse);
}
let userTrusted;
@@ -407,7 +473,11 @@ export class CrossSigningInfo extends EventEmitter {
} catch (e) {
userTrusted = false;
}
return new UserTrustLevel(userTrusted, userCrossSigning.firstUse);
return new UserTrustLevel(
userTrusted,
userCrossSigning.crossSigningVerifiedBefore,
userCrossSigning.firstUse,
);
}
/**
@@ -416,17 +486,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);
@@ -438,11 +511,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) {
@@ -465,8 +549,9 @@ export const CrossSigningLevel = {
* Represents the ways in which we trust a user
*/
export class UserTrustLevel {
constructor(crossSigningVerified, tofu) {
constructor(crossSigningVerified, crossSigningVerifiedBefore, tofu) {
this._crossSigningVerified = crossSigningVerified;
this._crossSigningVerifiedBefore = crossSigningVerifiedBefore;
this._tofu = tofu;
}
@@ -484,6 +569,14 @@ export class UserTrustLevel {
return this._crossSigningVerified;
}
/**
* @returns {bool} true if we ever verified this user before (at least for
* the history of verifications observed by this device).
*/
wasCrossSigningVerified() {
return this._crossSigningVerifiedBefore;
}
/**
* @returns {bool} true if this user's key is trusted on first use
*/
@@ -496,17 +589,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,
);
}
@@ -514,7 +609,9 @@ export class DeviceTrustLevel {
* @returns {bool} true if this device is verified via any means
*/
isVerified() {
return this.isCrossSigningVerified() || this.isLocallyVerified();
return Boolean(this.isLocallyVerified() || (
this._trustCrossSignedDevices && this.isCrossSigningVerified()
));
}
/**
@@ -539,3 +636,33 @@ 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) {
if (!(key instanceof Uint8Array)) {
throw new Error(
`storeCrossSigningKeyCache expects Uint8Array, got ${key}`,
);
}
return store.doTxn(
'readwrite',
[IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
store.storeSecretStorePrivateKey(txn, type, key);
},
);
},
};
}
+51 -21
View File
@@ -197,16 +197,16 @@ export class DeviceList extends EventEmitter {
const resolveSavePromise = this._resolveSavePromise;
this._savePromiseTime = targetTime;
this._saveTimer = setTimeout(() => {
logger.log('Saving device tracking data at token ' + this._syncToken);
logger.log('Saving device tracking data', this._syncToken);
// null out savePromise now (after the delay but before the write),
// otherwise we could return the existing promise when the save has
// actually already happened. Likewise for the dirty flag.
// actually already happened.
this._savePromiseTime = null;
this._saveTimer = null;
this._savePromise = null;
this._resolveSavePromise = null;
this._dirty = false;
this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.storeEndToEndDeviceData({
@@ -217,7 +217,13 @@ export class DeviceList extends EventEmitter {
}, txn);
},
).then(() => {
// The device list is considered dirty until the write
// completes.
this._dirty = false;
resolveSavePromise();
}, err => {
logger.error('Failed to save device tracking data', this._syncToken);
logger.error(err);
});
}, delay);
}
@@ -311,6 +317,15 @@ export class DeviceList extends EventEmitter {
return stored;
}
/**
* Returns a list of all user IDs the DeviceList knows about
*
* @return {array} All known user IDs
*/
getKnownUserIds() {
return Object.keys(this._devices);
}
/**
* Get the stored device keys for a user id
*
@@ -373,6 +388,26 @@ export class DeviceList extends EventEmitter {
return DeviceInfo.fromStorage(devs[deviceId], deviceId);
}
/**
* Get a user ID by one of their device's curve25519 identity key
*
* @param {string} algorithm encryption algorithm
* @param {string} senderKey curve25519 key to match
*
* @return {string} user ID
*/
getUserByIdentityKey(algorithm, senderKey) {
if (
algorithm !== olmlib.OLM_ALGORITHM &&
algorithm !== olmlib.MEGOLM_ALGORITHM
) {
// we only deal in olm keys
return null;
}
return this._userByIdentityKey[senderKey];
}
/**
* Find a device by curve25519 identity key
*
@@ -382,19 +417,11 @@ export class DeviceList extends EventEmitter {
* @return {module:crypto/deviceinfo?}
*/
getDeviceByIdentityKey(algorithm, senderKey) {
const userId = this._userByIdentityKey[senderKey];
const userId = this.getUserByIdentityKey(algorithm, senderKey);
if (!userId) {
return null;
}
if (
algorithm !== olmlib.OLM_ALGORITHM &&
algorithm !== olmlib.MEGOLM_ALGORITHM
) {
// we only deal in olm keys
return null;
}
const devices = this._devices[userId];
if (!devices) {
return null;
@@ -749,31 +776,34 @@ class DeviceListUpdateSerialiser {
this._baseApis.downloadKeysForUsers(
downloadUsers, opts,
).then((res) => {
).then(async (res) => {
const dk = res.device_keys || {};
const masterKeys = res.master_keys || {};
const ssks = res.self_signing_keys || {};
const usks = res.user_signing_keys || {};
// do each user in a separate promise, to avoid wedging the CPU
// yield to other things that want to execute in between users, to
// avoid wedging the CPU
// (https://github.com/vector-im/riot-web/issues/3158)
//
// of course we ought to do this in a web worker or similar, but
// this serves as an easy solution for now.
let prom = Promise.resolve();
for (const userId of downloadUsers) {
prom = prom.then(sleep(5)).then(() => {
return this._processQueryResponseForUser(
await sleep(5);
try {
await this._processQueryResponseForUser(
userId, dk[userId], {
master: masterKeys[userId],
self_signing: ssks[userId],
user_signing: usks[userId],
},
);
});
} catch (e) {
// log the error but continue, so that one bad key
// doesn't kill the whole process
logger.error(`Error processing keys for ${userId}:`, e);
}
}
return prom;
}).then(() => {
logger.log('Completed key download for ' + downloadUsers);
@@ -794,7 +824,7 @@ class DeviceListUpdateSerialiser {
}
async _processQueryResponseForUser(
userId, dkResponse, crossSigningResponse, sskResponse,
userId, dkResponse, crossSigningResponse,
) {
logger.log('got device keys for ' + userId + ':', dkResponse);
logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse);
+16 -1
View File
@@ -58,7 +58,7 @@ const SEND_KEY_REQUESTS_DELAY_MS = 500;
*
* @enum {number}
*/
const ROOM_KEY_REQUEST_STATES = {
export const ROOM_KEY_REQUEST_STATES = {
/** request not yet sent */
UNSENT: 0,
@@ -327,6 +327,21 @@ export class OutgoingRoomKeyRequestManager {
);
}
/**
* Find anything in `sent` state, and kick it around the loop again.
* This is intended for situations where something substantial has changed, and we
* don't really expect the other end to even care about the cancellation.
* For example, after initialization or self-verification.
* @return {Promise} An array of `sendRoomKeyRequest` outputs.
*/
async cancelAndResendAllOutgoingRequests() {
const outgoings = await this._cryptoStore.getAllOutgoingRoomKeyRequestsByState(
ROOM_KEY_REQUEST_STATES.SENT,
);
return Promise.all(outgoings.map(({ requestBody, recipients }) =>
this.sendRoomKeyRequest(requestBody, recipients, true)));
}
// start the background timer to send queued requests, if the timer isn't
// already running
_startTimer() {
+194 -109
View File
@@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -19,8 +19,16 @@ import {logger} from '../logger';
import * as olmlib from './olmlib';
import {pkVerify} from './olmlib';
import {randomString} from '../randomstring';
import {encryptAES, decryptAES} from './aes';
import {encodeBase64} from "./olmlib";
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";
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
/**
* Implements Secure Secret Storage and Sharing (MSC1946)
@@ -85,23 +93,15 @@ export class SecretStorage extends EventEmitter {
}
switch (algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
case SECRET_STORAGE_ALGORITHM_V1_AES:
{
const decryption = new global.Olm.PkDecryption();
try {
const { passphrase, pubkey } = opts;
// Copies in public key details of the form generated by
// the Crypto module's `createRecoveryKeyFromPassphrase`.
if (passphrase && pubkey) {
keyData.passphrase = passphrase;
keyData.pubkey = pubkey;
} else if (pubkey) {
keyData.pubkey = pubkey;
} else {
keyData.pubkey = decryption.generate_key();
}
} finally {
decryption.free();
if (opts.passphrase) {
keyData.passphrase = opts.passphrase;
}
if (opts.key) {
const {iv, mac} = await SecretStorage._calculateKeyCheck(opts.key);
keyData.iv = iv;
keyData.mac = mac;
}
break;
}
@@ -155,6 +155,28 @@ export class SecretStorage extends EventEmitter {
);
}
/**
* Get the key information for a given ID.
*
* @param {string} [keyId = default key's ID] The ID of the key to check
* for. Defaults to the default key ID if not provided.
* @returns {Array?} If the key was found, the return value is an array of
* the form [keyId, keyInfo]. Otherwise, null is returned.
*/
async getKey(keyId) {
if (!keyId) {
keyId = await this.getDefaultKeyId();
}
if (!keyId) {
return null;
}
const keyInfo = await this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
return keyInfo ? [keyId, keyInfo] : null;
}
/**
* Check whether we have a key with a given ID.
*
@@ -163,16 +185,59 @@ export class SecretStorage extends EventEmitter {
* @return {boolean} Whether we have the key.
*/
async hasKey(keyId) {
if (!keyId) {
keyId = await this.getDefaultKeyId();
}
if (!keyId) {
return !!(await this.getKey(keyId));
}
async keyNeedsUpgrade(keyId) {
const keyInfo = await this.getKey(keyId);
if (keyInfo && keyInfo[1].algorithm === SECRET_STORAGE_ALGORITHM_V1_CURVE25519) {
return true;
} else {
return false;
}
}
return !!this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
/**
* Check whether a key matches what we expect based on the key info
*
* @param {Uint8Array} key the key to check
* @param {object} info the key info
*
* @return {boolean} whether or not the key matches
*/
async checkKey(key, info) {
switch (info.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1_AES:
{
if (info.mac) {
const {mac} = await SecretStorage._calculateKeyCheck(key, info.iv);
return info.mac === mac;
} else {
// if we have no information, we have to assume the key is right
return true;
}
}
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
{
let decryption = null;
try {
decryption = new global.Olm.PkDecryption();
const gotPubkey = decryption.init_with_private_key(key);
// make sure it agrees with the given pubkey
return gotPubkey === info.pubkey;
} catch (e) {
return false;
} finally {
if (decryption) decryption.free();
}
}
default:
throw new Error("Unknown algorithm");
}
}
static async _calculateKeyCheck(key, iv) {
return await encryptAES(ZERO_STR, key, "", iv);
}
/**
@@ -207,24 +272,13 @@ export class SecretStorage extends EventEmitter {
throw new Error("Unknown key: " + keyId);
}
// check signature of key info
pkVerify(
keyInfo,
this._crossSigningInfo.getId('master'),
this._crossSigningInfo.userId,
);
// encrypt secret, based on the algorithm
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
case SECRET_STORAGE_ALGORITHM_V1_AES:
{
const encryption = new global.Olm.PkEncryption();
try {
encryption.set_recipient_key(keyInfo.pubkey);
encrypted[keyId] = encryption.encrypt(secret);
} finally {
encryption.free();
}
const keys = {[keyId]: keyInfo};
const [, encryption] = await this._getSecretStorageKey(keys, name);
encrypted[keyId] = await encryption.encrypt(secret);
break;
}
default:
@@ -238,29 +292,6 @@ export class SecretStorage extends EventEmitter {
await this._baseApis.setAccountData(name, {encrypted});
}
/**
* Store a secret defined to be the same as the given key.
* No secret information will be stored, instead the secret will
* be stored with a marker to say that the contents of the secret is
* the value of the given key.
* This is useful for migration from systems that predate SSSS such as
* key backup.
*
* @param {string} name The name of the secret
* @param {string} keyId The ID of the key whose value will be the
* value of the secret
* @returns {Promise} resolved when account data is saved
*/
storePassthrough(name, keyId) {
return this._baseApis.setAccountData(name, {
encrypted: {
[keyId]: {
passthrough: true,
},
},
});
}
/**
* Temporary method to fix up existing accounts where secrets
* are incorrectly stored without the 'encrypted' level
@@ -317,7 +348,12 @@ export class SecretStorage extends EventEmitter {
);
const encInfo = secretInfo.encrypted[keyId];
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
case SECRET_STORAGE_ALGORITHM_V1_AES:
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
keys[keyId] = keyInfo;
}
break;
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
if (
keyInfo.pubkey && (
(encInfo.ciphertext && encInfo.mac && encInfo.ephemeral) ||
@@ -341,18 +377,13 @@ export class SecretStorage extends EventEmitter {
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();
// since we just want to return the key itself. It must be base64
// encoded, since this is how a key would normally be stored.
if (encInfo.passthrough) return encodeBase64(decryption.get_private_key());
// decrypt secret
switch (keys[keyId].algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
return decryption.decrypt(
encInfo.ephemeral, encInfo.mac, encInfo.ciphertext,
);
}
return await decryption.decrypt(encInfo);
} finally {
if (decryption) decryption.free();
if (decryption && decryption.free) decryption.free();
}
}
@@ -362,22 +393,26 @@ export class SecretStorage extends EventEmitter {
* @param {string} name the name of the secret
* @param {boolean} checkKey check if the secret is encrypted by a trusted key
*
* @return {boolean} whether or not the secret is stored
* @return {object?} map of key name to key info the secret is encrypted
* with, or null if it is not present or not encrypted with a trusted
* key
*/
async isStored(name, checkKey) {
// check if secret exists
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
if (!secretInfo) return false;
if (!secretInfo) return null;
if (!secretInfo.encrypted) {
// try to fix it up
secretInfo = await this._fixupStoredSecret(name, secretInfo);
if (!secretInfo || !secretInfo.encrypted) {
return false;
return null;
}
}
if (checkKey === undefined) checkKey = true;
const ret = {};
// check if secret is encrypted by a known/trusted secret and
// encryption looks sane
for (const keyId of Object.keys(secretInfo.encrypted)) {
@@ -385,32 +420,55 @@ export class SecretStorage extends EventEmitter {
const keyInfo = await this._baseApis.getAccountDataFromServer(
"m.secret_storage.key." + keyId,
);
if (!keyInfo) return false;
if (!keyInfo) continue;
const encInfo = secretInfo.encrypted[keyId];
if (checkKey) {
pkVerify(
keyInfo,
this._crossSigningInfo.getId('master'),
this._crossSigningInfo.userId,
);
}
// We don't actually need the decryption object if it's a passthrough
// since we just want to return the key itself.
if (encInfo.passthrough) return true;
if (encInfo.passthrough) {
try {
pkVerify(
keyInfo,
this._crossSigningInfo.getId('master'),
this._crossSigningInfo.userId,
);
} catch (e) {
// not trusted, so move on to the next key
continue;
}
ret[keyId] = keyInfo;
continue;
}
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
case SECRET_STORAGE_ALGORITHM_V1_AES:
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
ret[keyId] = keyInfo;
}
break;
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
&& encInfo.ephemeral) {
return true;
if (checkKey) {
try {
pkVerify(
keyInfo,
this._crossSigningInfo.getId('master'),
this._crossSigningInfo.userId,
);
} catch (e) {
// not trusted, so move on to the next key
continue;
}
}
ret[keyId] = keyInfo;
}
break;
default:
// do nothing if we don't understand the encryption algorithm
}
}
return false;
return Object.keys(ret).length ? ret : null;
}
/**
@@ -462,6 +520,7 @@ export class SecretStorage extends EventEmitter {
for (const device of devices) {
toDevice[device] = requestData;
}
logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
this._baseApis.sendToDevice("m.secret.request", {
[this._baseApis.getUserId()]: toDevice,
});
@@ -515,6 +574,7 @@ export class SecretStorage extends EventEmitter {
device_trust: this._baseApis.checkDeviceTrust(sender, deviceId),
});
if (secret) {
logger.info(`Preparing ${content.name} secret for ${deviceId}`);
const payload = {
type: "m.secret.send",
content: {
@@ -551,7 +611,10 @@ export class SecretStorage extends EventEmitter {
},
};
logger.info(`Sending ${content.name} secret for ${deviceId}`);
this._baseApis.sendToDevice("m.room.encrypted", contentMap);
} else {
logger.info(`Request denied for ${content.name} secret for ${deviceId}`);
}
}
}
@@ -563,7 +626,7 @@ export class SecretStorage extends EventEmitter {
return;
}
const content = event.getContent();
logger.log("got secret share for request ", content.request_id);
logger.log("got secret share for request", content.request_id);
const requestControl = this._requests[content.request_id];
if (requestControl) {
// make sure that the device that sent it is one of the devices that
@@ -607,26 +670,48 @@ export class SecretStorage extends EventEmitter {
}
switch (keys[keyId].algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
{
const decryption = new global.Olm.PkDecryption();
let pubkey;
try {
pubkey = decryption.init_with_private_key(privateKey);
} catch (e) {
decryption.free();
throw new Error("getSecretStorageKey callback returned invalid key");
}
if (pubkey !== keys[keyId].pubkey) {
decryption.free();
throw new Error(
"getSecretStorageKey callback returned incorrect key",
);
}
return [keyId, decryption];
case SECRET_STORAGE_ALGORITHM_V1_AES:
{
const decryption = {
encrypt: async function(secret) {
return await encryptAES(secret, privateKey, name);
},
decrypt: async function(encInfo) {
return await decryptAES(encInfo, privateKey, name);
},
};
return [keyId, decryption];
}
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
{
const pkDecryption = new global.Olm.PkDecryption();
let pubkey;
try {
pubkey = pkDecryption.init_with_private_key(privateKey);
} catch (e) {
pkDecryption.free();
throw new Error("getSecretStorageKey callback returned invalid key");
}
default:
throw new Error("Unknown key type: " + keys[keyId].algorithm);
if (pubkey !== keys[keyId].pubkey) {
pkDecryption.free();
throw new Error(
"getSecretStorageKey callback returned incorrect key",
);
}
const decryption = {
free: pkDecryption.free.bind(pkDecryption),
decrypt: async function(encInfo) {
return pkDecryption.decrypt(
encInfo.ephemeral, encInfo.mac, encInfo.ciphertext,
);
},
// needed for passthrough
get_private_key: pkDecryption.get_private_key.bind(pkDecryption),
};
return [keyId, decryption];
}
default:
throw new Error("Unknown key type: " + keys[keyId].algorithm);
}
}
}
+251
View File
@@ -0,0 +1,251 @@
/*
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
* @param {string} ivStr the initialization vector to use
*/
async function encryptNode(data, key, name, ivStr) {
const crypto = getCrypto();
if (!crypto) {
throw new Error("No usable crypto implementation");
}
let iv;
if (ivStr) {
iv = decodeBase64(ivStr);
} else {
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
* @param {string} ivStr the initialization vector to use
*/
async function encryptBrowser(data, key, name, ivStr) {
let iv;
if (ivStr) {
iv = decodeBase64(ivStr);
} else {
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);
}
+9
View File
@@ -60,6 +60,15 @@ export class EncryptionAlgorithm {
this._roomId = params.roomId;
}
/**
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
*
* @param {module:models/room} room the room the event is in
*/
prepareToEncrypt(room) {
}
/**
* Encrypt a message event
*
+290 -179
View File
@@ -185,15 +185,15 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
*
* @param {Object} devicesInRoom The devices in this room, indexed by user ID
* @param {Object} blocked The devices that are blocked, indexed by user ID
* @param {boolean} [singleOlmCreationPhase] Only perform one round of olm
* session creation
*
* @return {Promise} Promise which resolves to the
* OutboundSessionInfo when setup is complete.
*/
MegolmEncryption.prototype._ensureOutboundSession = async function(
devicesInRoom, blocked,
devicesInRoom, blocked, singleOlmCreationPhase,
) {
const self = this;
let session;
// takes the previous OutboundSessionInfo, and considers whether to create
@@ -201,12 +201,12 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
// Updates `session` to hold the final OutboundSessionInfo.
//
// returns a promise which resolves once the keyshare is successful.
async function prepareSession(oldSession) {
const prepareSession = async (oldSession) => {
session = oldSession;
// need to make a brand new session?
if (session && session.needsRotation(self._sessionRotationPeriodMsgs,
self._sessionRotationPeriodMs)
if (session && session.needsRotation(this._sessionRotationPeriodMsgs,
this._sessionRotationPeriodMs)
) {
logger.log("Starting new megolm session because we need to rotate.");
session = null;
@@ -218,32 +218,20 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
}
if (!session) {
logger.log(`Starting new megolm session for room ${self._roomId}`);
session = await self._prepareNewSession();
logger.log(`Starting new megolm session for room ${this._roomId}`);
session = await this._prepareNewSession();
logger.log(`Started new megolm session ${session.sessionId} ` +
`for room ${self._roomId}`);
self._outboundSessions[session.sessionId] = session;
`for room ${this._roomId}`);
this._outboundSessions[session.sessionId] = session;
}
// now check if we need to share with any devices
const shareMap = {};
for (const userId in devicesInRoom) {
if (!devicesInRoom.hasOwnProperty(userId)) {
continue;
}
const userDevices = devicesInRoom[userId];
for (const deviceId in userDevices) {
if (!userDevices.hasOwnProperty(deviceId)) {
continue;
}
const deviceInfo = userDevices[deviceId];
for (const [userId, userDevices] of Object.entries(devicesInRoom)) {
for (const [deviceId, deviceInfo] of Object.entries(userDevices)) {
const key = deviceInfo.getIdentityKey();
if (key == self._olmDevice.deviceCurve25519Key) {
if (key == this._olmDevice.deviceCurve25519Key) {
// don't bother sending to ourself
continue;
}
@@ -258,51 +246,99 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
}
}
const errorDevices = [];
await self._shareKeyWithDevices(
session, shareMap, errorDevices,
const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
const payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session.sessionId,
session_key: key.key,
chain_index: key.chain_index,
},
};
const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(
this._olmDevice, this._baseApis, shareMap,
);
// are there any new blocked devices that we need to notify?
const blockedMap = {};
for (const userId in blocked) {
if (!blocked.hasOwnProperty(userId)) {
continue;
}
await Promise.all([
(async () => {
// share keys with devices that we already have a session for
await this._shareKeyWithOlmSessions(
session, key, payload, olmSessions,
);
})(),
(async () => {
const errorDevices = [];
const userBlockedDevices = blocked[userId];
// meanwhile, establish olm sessions for devices that we don't
// already have a session for, and share keys with them. If
// we're doing two phases of olm session creation, use a
// shorter timeout when fetching one-time keys for the first
// phase.
const start = Date.now();
const failedServers = [];
await this._shareKeyWithDevices(
session, key, payload, devicesWithoutSession, errorDevices,
singleOlmCreationPhase ? 10000 : 2000, failedServers,
);
for (const deviceId in userBlockedDevices) {
if (!userBlockedDevices.hasOwnProperty(deviceId)) {
continue;
if (!singleOlmCreationPhase && (Date.now() - start < 10000)) {
// perform the second phase of olm session creation if requested,
// and if the first phase didn't take too long
(async () => {
// Retry sending keys to devices that we were unable to establish
// an olm session for. This time, we use a longer timeout, but we
// do this in the background and don't block anything else while we
// do this. We only need to retry users from servers that didn't
// respond the first time.
const retryDevices = {};
const failedServerMap = new Set;
for (const server of failedServers) {
failedServerMap.add(server);
}
const failedDevices = [];
for (const {userId, deviceInfo} of errorDevices) {
const userHS = userId.slice(userId.indexOf(":") + 1);
if (failedServerMap.has(userHS)) {
retryDevices[userId] = retryDevices[userId] || [];
retryDevices[userId].push(deviceInfo);
} else {
// if we aren't going to retry, then handle it
// as a failed device
failedDevices.push({userId, deviceInfo});
}
}
await this._shareKeyWithDevices(
session, key, payload, retryDevices, failedDevices,
);
await this._notifyFailedOlmDevices(session, key, failedDevices);
})();
} else {
await this._notifyFailedOlmDevices(session, key, errorDevices);
}
})(),
(async () => {
// also, notify blocked devices that they're blocked
const blockedMap = {};
for (const [userId, userBlockedDevices] of Object.entries(blocked)) {
for (const [deviceId, device] of Object.entries(userBlockedDevices)) {
if (
!session.blockedDevicesNotified[userId] ||
session.blockedDevicesNotified[userId][deviceId] === undefined
) {
blockedMap[userId] = blockedMap[userId] || {};
blockedMap[userId][deviceId] = {device};
}
}
}
if (
!session.blockedDevicesNotified[userId] ||
session.blockedDevicesNotified[userId][deviceId] === undefined
) {
blockedMap[userId] = blockedMap[userId] || [];
blockedMap[userId].push(userBlockedDevices[deviceId]);
}
}
}
const filteredErrorDevices =
await self._olmDevice.filterOutNotifiedErrorDevices(errorDevices);
for (const {userId, deviceInfo} of filteredErrorDevices) {
blockedMap[userId] = blockedMap[userId] || [];
blockedMap[userId].push({
code: "m.no_olm",
reason: WITHHELD_MESSAGES["m.no_olm"],
deviceInfo,
});
}
// notify blocked devices that they're blocked
await self._notifyBlockedDevices(session, blockedMap);
}
await this._notifyBlockedDevices(session, blockedMap);
})(),
]);
};
// helper which returns the session prepared by prepareSession
function returnSession() {
@@ -349,96 +385,47 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
};
/**
* Splits the user device map into multiple chunks to reduce the number of
* devices we encrypt to per API call. Also filters out devices we don't have
* a session with.
* Determines what devices in devicesByUser don't have an olm session as given
* in devicemap.
*
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
* @param {object} devicemap the devices that have olm sessions, as returned by
* olmlib.ensureOlmSessionsForDevices.
* @param {object} devicesByUser a map of user IDs to array of deviceInfo
* @param {array} [noOlmDevices] an array to fill with devices that don't have
* olm sessions
*
* @param {number} chainIndex current chain index
*
* @param {object<userId, deviceId>} devicemap
* mapping from userId to deviceId to {@link module:crypto~OlmSessionResult}
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @param {array<object>} errorDevices
* array that will be populated with the devices that can't get an
* olm session for
*
* @return {array<object<userid, deviceInfo>>}
* @return {array} an array of devices that don't have olm sessions. If
* noOlmDevices is specified, then noOlmDevices will be returned.
*/
MegolmEncryption.prototype._splitUserDeviceMap = function(
session, chainIndex, devicemap, devicesByUser, errorDevices,
MegolmEncryption.prototype._getDevicesWithoutSessions = function(
devicemap, devicesByUser, noOlmDevices,
) {
const maxUsersPerRequest = 20;
noOlmDevices = noOlmDevices || [];
// use an array where the slices of a content map gets stored
const mapSlices = [];
let currentSliceId = 0; // start inserting in the first slice
let entriesInCurrentSlice = 0;
for (const userId of Object.keys(devicesByUser)) {
const devicesToShareWith = devicesByUser[userId];
for (const [userId, devicesToShareWith] of Object.entries(devicesByUser)) {
const sessionResults = devicemap[userId];
for (let i = 0; i < devicesToShareWith.length; i++) {
const deviceInfo = devicesToShareWith[i];
for (const deviceInfo of devicesToShareWith) {
const deviceId = deviceInfo.deviceId;
const sessionResult = sessionResults[deviceId];
if (!sessionResult.sessionId) {
// no session with this device, probably because there
// were no one-time keys.
//
// we could send them a to_device message anyway, as a
// signal that they have missed out on the key sharing
// message because of the lack of keys, but there's not
// much point in that really; it will mostly serve to clog
// up to_device inboxes.
// mark this device as "handled" because we don't want to try
// to claim a one-time-key for dead devices on every message.
session.markSharedWithDevice(userId, deviceId, chainIndex);
errorDevices.push({userId, deviceInfo});
noOlmDevices.push({userId, deviceInfo});
delete sessionResults[deviceId];
// ensureOlmSessionsForUsers has already done the logging,
// so just skip it.
continue;
}
logger.log(
"share keys with device " + userId + ":" + deviceId,
);
if (!mapSlices[currentSliceId]) {
mapSlices[currentSliceId] = [];
}
mapSlices[currentSliceId].push({
userId: userId,
deviceInfo: deviceInfo,
});
entriesInCurrentSlice++;
}
// We do this in the per-user loop as we prefer that all messages to the
// same user end up in the same API call to make it easier for the
// server (e.g. only have to send one EDU if a remote user, etc). This
// does mean that if a user has many devices we may go over the desired
// limit, but its not a hard limit so that is fine.
if (entriesInCurrentSlice > maxUsersPerRequest) {
// the current slice is filled up. Start inserting into the next slice
entriesInCurrentSlice = 0;
currentSliceId++;
}
}
return mapSlices;
return noOlmDevices;
};
/**
@@ -451,20 +438,18 @@ MegolmEncryption.prototype._splitUserDeviceMap = function(
*
* @return {array<array<object>>} the blocked devices, split into chunks
*/
MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
const maxUsersPerRequest = 20;
MegolmEncryption.prototype._splitDevices = function(devicesByUser) {
const maxDevicesPerRequest = 20;
// use an array where the slices of a content map gets stored
let currentSlice = [];
const mapSlices = [currentSlice];
for (const userId of Object.keys(devicesByUser)) {
const userBlockedDevicesToShareWith = devicesByUser[userId];
for (const blockedInfo of userBlockedDevicesToShareWith) {
for (const [userId, userDevices] of Object.entries(devicesByUser)) {
for (const deviceInfo of Object.values(userDevices)) {
currentSlice.push({
userId: userId,
blockedInfo: blockedInfo,
deviceInfo: deviceInfo.device,
});
}
@@ -473,7 +458,7 @@ MegolmEncryption.prototype._splitBlockedDevices = function(devicesByUser) {
// server (e.g. only have to send one EDU if a remote user, etc). This
// does mean that if a user has many devices we may go over the desired
// limit, but its not a hard limit so that is fine.
if (currentSlice.length > maxUsersPerRequest) {
if (currentSlice.length > maxDevicesPerRequest) {
// the current slice is filled up. Start inserting into the next slice
currentSlice = [];
mapSlices.push(currentSlice);
@@ -568,7 +553,7 @@ MegolmEncryption.prototype._sendBlockedNotificationsToDevices = async function(
for (const val of userDeviceMap) {
const userId = val.userId;
const blockedInfo = val.blockedInfo;
const blockedInfo = val.deviceInfo;
const deviceInfo = blockedInfo.deviceInfo;
const deviceId = deviceInfo.deviceId;
@@ -643,9 +628,7 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, {
[userId]: {
[device.deviceId]: device,
},
[userId]: [device],
},
);
@@ -688,37 +671,44 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
};
/**
* @private
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object} key the session key as returned by
* OlmDevice.getOutboundGroupSessionKey
*
* @param {object} payload the base to-device message payload for sharing keys
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices
*
* @param {array<object>} errorDevices
* array that will be populated with the devices that we can't get an
* olm session for
*
* @param {Number} [otkTimeout] The timeout in milliseconds when requesting
* one-time keys for establishing new olm sessions.
*
* @param {Array} [failedServers] An array to fill with remote servers that
* failed to respond to one-time-key requests.
*/
MegolmEncryption.prototype._shareKeyWithDevices = async function(
session, devicesByUser, errorDevices,
session, key, payload, devicesByUser, errorDevices, otkTimeout, failedServers,
) {
const key = this._olmDevice.getOutboundGroupSessionKey(session.sessionId);
const payload = {
type: "m.room_key",
content: {
algorithm: olmlib.MEGOLM_ALGORITHM,
room_id: this._roomId,
session_id: session.sessionId,
session_key: key.key,
chain_index: key.chain_index,
},
};
const devicemap = await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, devicesByUser,
this._olmDevice, this._baseApis, devicesByUser, otkTimeout, failedServers,
);
const userDeviceMaps = this._splitUserDeviceMap(
session, key.chain_index, devicemap, devicesByUser, errorDevices,
);
this._getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices);
await this._shareKeyWithOlmSessions(session, key, payload, devicemap);
};
MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
session, key, payload, devicemap,
) {
const userDeviceMaps = this._splitDevices(devicemap);
for (let i = 0; i < userDeviceMaps.length; i++) {
try {
@@ -736,6 +726,52 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(
}
};
/**
* Notify devices that we weren't able to create olm sessions.
*
* @param {module:crypto/algorithms/megolm.OutboundSessionInfo} session
*
* @param {object} key
*
* @param {Array<object>} failedDevices the devices that we were unable to
* create olm sessions for, as returned by _shareKeyWithDevices
*/
MegolmEncryption.prototype._notifyFailedOlmDevices = async function(
session, key, failedDevices,
) {
// mark the devices that failed as "handled" because we don't want to try
// to claim a one-time-key for dead devices on every message.
for (const {userId, deviceInfo} of failedDevices) {
const deviceId = deviceInfo.deviceId;
session.markSharedWithDevice(
userId, deviceId, key.chain_index,
);
}
const filteredFailedDevices =
await this._olmDevice.filterOutNotifiedErrorDevices(
failedDevices,
);
const blockedMap = {};
for (const {userId, deviceInfo} of filteredFailedDevices) {
blockedMap[userId] = blockedMap[userId] || {};
// we use a similar format to what
// olmlib.ensureOlmSessionsForDevices returns, so that
// we can use the same function to split
blockedMap[userId][deviceInfo.deviceId] = {
device: {
code: "m.no_olm",
reason: WITHHELD_MESSAGES["m.no_olm"],
deviceInfo,
},
};
}
// send the notifications
await this._notifyBlockedDevices(session, blockedMap);
};
/**
* Notify blocked devices that they have been blocked.
*
@@ -754,7 +790,7 @@ MegolmEncryption.prototype._notifyBlockedDevices = async function(
sender_key: this._olmDevice.deviceCurve25519Key,
};
const userDeviceMaps = this._splitBlockedDevices(devicesByUser);
const userDeviceMaps = this._splitDevices(devicesByUser);
for (let i = 0; i < userDeviceMaps.length; i++) {
try {
@@ -772,6 +808,38 @@ MegolmEncryption.prototype._notifyBlockedDevices = async function(
}
};
/**
* Perform any background tasks that can be done before a message is ready to
* send, in order to speed up sending of the message.
*
* @param {module:models/room} room the room the event is in
*/
MegolmEncryption.prototype.prepareToEncrypt = function(room) {
if (this.encryptionPreparation) {
// We're already preparing something, so don't do anything else.
// FIXME: check if we need to restart
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
return;
}
logger.debug(`Preparing to encrypt events for ${this._roomId}`);
this.encryptionPreparation = (async () => {
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
// Drop unknown devices for now. When the message gets sent, we'll
// throw an error, but we'll still be prepared to send to the known
// devices.
this._removeUnknownDevices(devicesInRoom);
}
await this._ensureOutboundSession(devicesInRoom, blocked, true);
delete this.encryptionPreparation;
})();
};
/**
* @inheritdoc
*
@@ -782,38 +850,48 @@ MegolmEncryption.prototype._notifyBlockedDevices = async function(
* @return {Promise} Promise which resolves to the new event body
*/
MegolmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
const self = this;
logger.log(`Starting to encrypt event for ${this._roomId}`);
if (this.encryptionPreparation) {
// If we started sending keys, wait for it to be done.
// FIXME: check if we need to cancel
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
try {
await this.encryptionPreparation;
} catch (e) {
// ignore any errors -- if the preparation failed, we'll just
// restart everything here
}
}
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
// check if any of these devices are not yet known to the user.
// if so, warn the user so they can verify or ignore.
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
self._checkForUnknownDevices(devicesInRoom);
this._checkForUnknownDevices(devicesInRoom);
}
const session = await self._ensureOutboundSession(devicesInRoom, blocked);
const session = await this._ensureOutboundSession(devicesInRoom, blocked);
const payloadJson = {
room_id: self._roomId,
room_id: this._roomId,
type: eventType,
content: content,
};
const ciphertext = self._olmDevice.encryptGroupMessage(
const ciphertext = this._olmDevice.encryptGroupMessage(
session.sessionId, JSON.stringify(payloadJson),
);
const encryptedContent = {
algorithm: olmlib.MEGOLM_ALGORITHM,
sender_key: self._olmDevice.deviceCurve25519Key,
sender_key: this._olmDevice.deviceCurve25519Key,
ciphertext: ciphertext,
session_id: session.sessionId,
// Include our device ID so that recipients can send us a
// m.new_device message if they don't have our session key.
// XXX: Do we still need this now that m.new_device messages
// no longer exist since #483?
device_id: self._deviceId,
device_id: this._deviceId,
};
session.useCount++;
@@ -861,6 +939,27 @@ MegolmEncryption.prototype._checkForUnknownDevices = function(devicesInRoom) {
}
};
/**
* Remove unknown devices from a set of devices. The devicesInRoom parameter
* will be modified.
*
* @param {Object} devicesInRoom userId -> {deviceId -> object}
* devices we should shared the session with.
*/
MegolmEncryption.prototype._removeUnknownDevices = function(devicesInRoom) {
for (const [userId, userDevices] of Object.entries(devicesInRoom)) {
for (const [deviceId, device] of Object.entries(userDevices)) {
if (device.isUnverified() && !device.isKnown()) {
delete userDevices[deviceId];
}
}
if (Object.keys(userDevices).length === 0) {
delete devicesInRoom[userId];
}
}
};
/**
* Get the list of unblocked devices for all users in the room
*
@@ -904,8 +1003,10 @@ MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
continue;
}
const deviceTrust = this._crypto.checkDeviceTrust(userId, deviceId);
if (userDevices[deviceId].isBlocked() ||
(userDevices[deviceId].isUnverified() && isBlacklisting)
(!deviceTrust.isVerified() && isBlacklisting)
) {
if (!blocked[userId]) {
blocked[userId] = {};
@@ -1247,6 +1348,9 @@ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
if (content.code === "m.no_olm") {
const sender = event.getSender();
logger.warn(
`${sender}:${senderKey} was unable to establish an olm session with us`,
);
// if the sender says that they haven't been able to establish an olm
// session, let's proactively establish one
@@ -1258,21 +1362,30 @@ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
if (await this._olmDevice.getSessionIdForDevice(senderKey)) {
// a session has already been established, so we don't need to
// create a new one.
logger.debug("New session already created. Not creating a new one.");
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true);
this.retryDecryptionFromSender(senderKey);
return;
}
const device = this._crypto._deviceList.getDeviceByIdentityKey(
let device = this._crypto._deviceList.getDeviceByIdentityKey(
content.algorithm, senderKey,
);
if (!device) {
logger.info(
"Couldn't find device for identity key " + senderKey +
": not establishing session",
// if we don't know about the device, fetch the user's devices again
// and retry before giving up
await this._crypto.downloadKeys([sender], false);
device = this._crypto._deviceList.getDeviceByIdentityKey(
content.algorithm, senderKey,
);
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", false);
this.retryDecryptionFromSender(senderKey);
return;
if (!device) {
logger.info(
"Couldn't find device for identity key " + senderKey +
": not establishing session",
);
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", false);
this.retryDecryptionFromSender(senderKey);
return;
}
}
await olmlib.ensureOlmSessionsForDevices(
this._olmDevice, this._baseApis, {[sender]: [device]}, false,
@@ -1481,7 +1594,6 @@ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionI
MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey) {
const senderPendingEvents = this._pendingEvents[senderKey];
logger.warn(senderPendingEvents);
if (!senderPendingEvents) {
return true;
}
@@ -1491,7 +1603,6 @@ MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey)
await Promise.all([...senderPendingEvents].map(async ([_sessionId, pending]) => {
await Promise.all([...pending].map(async (ev) => {
try {
logger.warn(ev.getId());
await ev.attemptDecryption(this._crypto);
} catch (e) {
// don't die if something goes wrong
+731 -161
View File
File diff suppressed because it is too large Load Diff
+6 -3
View File
@@ -19,6 +19,8 @@ import {randomString} from '../randomstring';
const DEFAULT_ITERATIONS = 500000;
const DEFAULT_BITSIZE = 256;
export async function keyFromAuthData(authData, password) {
if (!global.Olm) {
throw new Error("Olm is not available");
@@ -34,6 +36,7 @@ export async function keyFromAuthData(authData, password) {
return await deriveKey(
password, authData.private_key_salt,
authData.private_key_iterations,
authData.private_key_bits || DEFAULT_BITSIZE,
);
}
@@ -44,12 +47,12 @@ export async function keyFromPassphrase(password) {
const salt = randomString(32);
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS);
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS, DEFAULT_BITSIZE);
return { key, salt, iterations: DEFAULT_ITERATIONS };
}
export async function deriveKey(password, salt, iterations) {
export async function deriveKey(password, salt, iterations, numBits = DEFAULT_BITSIZE) {
const subtleCrypto = global.crypto.subtle;
const TextEncoder = global.TextEncoder;
if (!subtleCrypto || !TextEncoder) {
@@ -73,7 +76,7 @@ export async function deriveKey(password, salt, iterations) {
hash: 'SHA-512',
},
key,
global.Olm.PRIVATE_KEY_LENGTH * 8,
numBits,
);
return new Uint8Array(keybits);
+83 -16
View File
@@ -113,6 +113,57 @@ export async function encryptMessageForDevice(
);
}
/**
* Get the existing olm sessions for the given devices, and the devices that
* don't have olm sessions.
*
* @param {module:crypto/OlmDevice} olmDevice
*
* @param {module:base-apis~MatrixBaseApis} baseApis
*
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices to ensure sessions for
*
* @return {Promise} resolves to an array. The first element of the array is a
* a map of user IDs to arrays of deviceInfo, representing the devices that
* don't have established olm sessions. The second element of the array is
* a map from userId to deviceId to {@link module:crypto~OlmSessionResult}
*/
export async function getExistingOlmSessions(
olmDevice, baseApis, devicesByUser,
) {
const devicesWithoutSession = {};
const sessions = {};
const promises = [];
for (const [userId, devices] of Object.entries(devicesByUser)) {
for (const deviceInfo of devices) {
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
promises.push((async () => {
const sessionId = await olmDevice.getSessionIdForDevice(
key, true,
);
if (sessionId === null) {
devicesWithoutSession[userId] = devicesWithoutSession[userId] || [];
devicesWithoutSession[userId].push(deviceInfo);
} else {
sessions[userId] = sessions[userId] || {};
sessions[userId][deviceId] = {
device: deviceInfo,
sessionId: sessionId,
};
}
})());
}
}
await Promise.all(promises);
return [devicesWithoutSession, sessions];
}
/**
* Try to make sure we have established olm sessions for the given devices.
*
@@ -123,30 +174,37 @@ export async function encryptMessageForDevice(
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
* map from userid to list of devices to ensure sessions for
*
* @param {boolean} 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.
*
* @param {Number} [otkTimeout] The timeout in milliseconds when requesting
* one-time keys for establishing new olm sessions.
*
* @param {Array} [failedServers] An array to fill with remote servers that
* failed to respond to one-time-key requests.
*
* @return {Promise} resolves once the sessions are complete, to
* an Object mapping from userId to deviceId to
* {@link module:crypto~OlmSessionResult}
*/
export async function ensureOlmSessionsForDevices(
olmDevice, baseApis, devicesByUser, force,
olmDevice, baseApis, devicesByUser, force, otkTimeout, failedServers,
) {
if (typeof force === "number") {
failedServers = otkTimeout;
otkTimeout = force;
force = false;
}
const devicesWithoutSession = [
// [userId, deviceId], ...
];
const result = {};
const resolveSession = {};
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
for (const [userId, devices] of Object.entries(devicesByUser)) {
result[userId] = {};
const devices = devicesByUser[userId];
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
for (const deviceInfo of devices) {
const deviceId = deviceInfo.deviceId;
const key = deviceInfo.getIdentityKey();
if (!olmDevice._sessionsInProgress[key]) {
@@ -197,7 +255,7 @@ export async function ensureOlmSessionsForDevices(
let res;
try {
res = await baseApis.claimOneTimeKeys(
devicesWithoutSession, oneTimeKeyAlgorithm,
devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout,
);
} catch (e) {
for (const resolver of Object.values(resolveSession)) {
@@ -207,14 +265,14 @@ export async function ensureOlmSessionsForDevices(
throw e;
}
if (failedServers && "failures" in res) {
failedServers.push(...Object.keys(res.failures));
}
const otk_res = res.one_time_keys || {};
const promises = [];
for (const userId in devicesByUser) {
if (!devicesByUser.hasOwnProperty(userId)) {
continue;
}
for (const [userId, devices] of Object.entries(devicesByUser)) {
const userRes = otk_res[userId] || {};
const devices = devicesByUser[userId];
for (let j = 0; j < devices.length; j++) {
const deviceInfo = devices[j];
const deviceId = deviceInfo.deviceId;
@@ -407,6 +465,15 @@ export function encodeBase64(uint8Array) {
return Buffer.from(uint8Array).toString("base64");
}
/**
* Encode a typed array of uint8 as unpadded base64.
* @param {Uint8Array} uint8Array The data to encode.
* @return {string} The unpadded base64.
*/
export function encodeUnpaddedBase64(uint8Array) {
return encodeBase64(uint8Array).replace(/=+$/g, '');
}
/**
* Decode a base64 string to a typed array of uint8.
* @param {string} base64 The base64 to decode.
+2 -2
View File
@@ -59,8 +59,8 @@ export function decodeRecoveryKey(recoverykey) {
throw new Error("Incorrect length");
}
return result.slice(
return Uint8Array.from(result.slice(
OLM_RECOVERY_KEY_PREFIX.length,
OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH,
);
));
}
@@ -203,6 +203,23 @@ export class Backend {
return promiseifyTxn(txn).then(() => result);
}
/**
*
* @param {Number} wantedState
* @return {Promise<Array<*>>} All elements in a given state
*/
getAllOutgoingRoomKeyRequestsByState(wantedState) {
return new Promise((resolve, reject) => {
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
const store = txn.objectStore("outgoingRoomKeyRequests");
const index = store.index("state");
const request = index.getAll(wantedState);
request.onsuccess = (ev) => resolve(ev.target.result);
request.onerror = (ev) => reject(ev.target.error);
});
}
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
let stateIndex = 0;
const results = [];
@@ -341,18 +358,39 @@ export class Backend {
};
}
getSecretStorePrivateKey(txn, func, type) {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get(`ssss_cache:${type}`);
getReq.onsuccess = function() {
try {
func(getReq.result || null);
} catch (e) {
abortWithException(txn, e);
}
};
}
storeCrossSigningKeys(txn, keys) {
const objectStore = txn.objectStore("account");
objectStore.put(keys, "crossSigningKeys");
}
storeSecretStorePrivateKey(txn, type, key) {
const objectStore = txn.objectStore("account");
objectStore.put(key, `ssss_cache:${type}`);
}
// Olm Sessions
countEndToEndSessions(txn, func) {
const objectStore = txn.objectStore("sessions");
const countReq = objectStore.count();
countReq.onsuccess = function() {
func(countReq.result);
try {
func(countReq.result);
} catch (e) {
abortWithException(txn, e);
}
};
}
@@ -402,16 +440,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);
}
};
}
+83 -111
View File
@@ -46,6 +46,7 @@ export class IndexedDBCryptoStore {
this._indexedDB = indexedDB;
this._dbName = dbName;
this._backendPromise = null;
this._backend = null;
}
static exists(indexedDB, dbName) {
@@ -56,10 +57,12 @@ export class IndexedDBCryptoStore {
* Ensure the database exists and is up-to-date, or fall back to
* a local storage or in-memory store.
*
* This must be called before the store can be used.
*
* @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend,
* or a MemoryCryptoStore
*/
_connect() {
startup() {
if (this._backendPromise) {
return this._backendPromise;
}
@@ -135,6 +138,8 @@ export class IndexedDBCryptoStore {
);
return new MemoryCryptoStore();
}
}).then(backend => {
this._backend = backend;
});
return this._backendPromise;
@@ -189,9 +194,7 @@ export class IndexedDBCryptoStore {
* same instance as passed in, or the existing one.
*/
getOrAddOutgoingRoomKeyRequest(request) {
return this._connect().then((backend) => {
return backend.getOrAddOutgoingRoomKeyRequest(request);
});
return this._backend.getOrAddOutgoingRoomKeyRequest(request);
}
/**
@@ -205,9 +208,7 @@ export class IndexedDBCryptoStore {
* not found
*/
getOutgoingRoomKeyRequest(requestBody) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequest(requestBody);
});
return this._backend.getOutgoingRoomKeyRequest(requestBody);
}
/**
@@ -221,9 +222,18 @@ export class IndexedDBCryptoStore {
* requests in those states, an arbitrary one is chosen.
*/
getOutgoingRoomKeyRequestByState(wantedStates) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequestByState(wantedStates);
});
return this._backend.getOutgoingRoomKeyRequestByState(wantedStates);
}
/**
* Look for room key requests by state
* unlike above, return a list of all entries in one state.
*
* @param {Number} wantedState
* @return {Promise<Array<*>>} Returns an array of requests in the given state
*/
getAllOutgoingRoomKeyRequestsByState(wantedState) {
return this._backend.getAllOutgoingRoomKeyRequestsByState(wantedState);
}
/**
@@ -237,11 +247,9 @@ export class IndexedDBCryptoStore {
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
*/
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
return this._connect().then((backend) => {
return backend.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, wantedStates,
);
});
return this._backend.getOutgoingRoomKeyRequestsByTarget(
userId, deviceId, wantedStates,
);
}
/**
@@ -257,11 +265,9 @@ export class IndexedDBCryptoStore {
* updated request, or null if no matching row was found
*/
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
return this._connect().then((backend) => {
return backend.updateOutgoingRoomKeyRequest(
requestId, expectedState, updates,
);
});
return this._backend.updateOutgoingRoomKeyRequest(
requestId, expectedState, updates,
);
}
/**
@@ -274,9 +280,7 @@ export class IndexedDBCryptoStore {
* @returns {Promise} resolves once the operation is completed
*/
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
return this._connect().then((backend) => {
return backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
});
return this._backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
}
// Olm Account
@@ -289,9 +293,7 @@ export class IndexedDBCryptoStore {
* @param {function(string)} func Called with the account pickle
*/
getAccount(txn, func) {
this._backendPromise.then(backend => {
backend.getAccount(txn, func);
});
this._backend.getAccount(txn, func);
}
/**
@@ -302,9 +304,7 @@ export class IndexedDBCryptoStore {
* @param {string} newData The new account pickle to store.
*/
storeAccount(txn, newData) {
this._backendPromise.then(backend => {
backend.storeAccount(txn, newData);
});
this._backend.storeAccount(txn, newData);
}
/**
@@ -316,9 +316,16 @@ export class IndexedDBCryptoStore {
* { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed
*/
getCrossSigningKeys(txn, func) {
this._backendPromise.then(backend => {
backend.getCrossSigningKeys(txn, func);
});
this._backend.getCrossSigningKeys(txn, func);
}
/**
* @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the private key
* @param {string} type A key type
*/
getSecretStorePrivateKey(txn, func, type) {
this._backend.getSecretStorePrivateKey(txn, func, type);
}
/**
@@ -328,9 +335,18 @@ export class IndexedDBCryptoStore {
* @param {string} keys keys object as getCrossSigningKeys()
*/
storeCrossSigningKeys(txn, keys) {
this._backendPromise.then(backend => {
backend.storeCrossSigningKeys(txn, keys);
});
this._backend.storeCrossSigningKeys(txn, keys);
}
/**
* Write the cross-signing private keys back to the store
*
* @param {*} txn An active transaction. See doTxn().
* @param {string} type The type of cross-signing private key to store
* @param {string} key keys object as getCrossSigningKeys()
*/
storeSecretStorePrivateKey(txn, type, key) {
this._backend.storeSecretStorePrivateKey(txn, type, key);
}
// Olm sessions
@@ -341,9 +357,7 @@ export class IndexedDBCryptoStore {
* @param {function(int)} func Called with the count of sessions
*/
countEndToEndSessions(txn, func) {
this._backendPromise.then(backend => {
backend.countEndToEndSessions(txn, func);
});
this._backend.countEndToEndSessions(txn, func);
}
/**
@@ -359,9 +373,7 @@ export class IndexedDBCryptoStore {
* a message.
*/
getEndToEndSession(deviceKey, sessionId, txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndSession(deviceKey, sessionId, txn, func);
});
this._backend.getEndToEndSession(deviceKey, sessionId, txn, func);
}
/**
@@ -376,9 +388,7 @@ export class IndexedDBCryptoStore {
* a message.
*/
getEndToEndSessions(deviceKey, txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndSessions(deviceKey, txn, func);
});
this._backend.getEndToEndSessions(deviceKey, txn, func);
}
/**
@@ -389,9 +399,7 @@ export class IndexedDBCryptoStore {
* and session keys.
*/
getAllEndToEndSessions(txn, func) {
this._backendPromise.then(backend => {
backend.getAllEndToEndSessions(txn, func);
});
this._backend.getAllEndToEndSessions(txn, func);
}
/**
@@ -402,29 +410,21 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
this._backendPromise.then(backend => {
backend.storeEndToEndSession(
deviceKey, sessionId, sessionInfo, txn,
);
});
this._backend.storeEndToEndSession(
deviceKey, sessionId, sessionInfo, txn,
);
}
storeEndToEndSessionProblem(deviceKey, type, fixed) {
return this._backendPromise.then(async (backend) => {
await backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
});
return this._backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
}
getEndToEndSessionProblem(deviceKey, timestamp) {
return this._backendPromise.then(async (backend) => {
return await backend.getEndToEndSessionProblem(deviceKey, timestamp);
});
return this._backend.getEndToEndSessionProblem(deviceKey, timestamp);
}
filterOutNotifiedErrorDevices(devices) {
return this._backendPromise.then(async (backend) => {
return await backend.filterOutNotifiedErrorDevices(devices);
});
return this._backend.filterOutNotifiedErrorDevices(devices);
}
// Inbound group sessions
@@ -439,11 +439,9 @@ export class IndexedDBCryptoStore {
* to Base64 end-to-end session.
*/
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, txn, func,
);
});
this._backend.getEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, txn, func,
);
}
/**
@@ -454,9 +452,7 @@ export class IndexedDBCryptoStore {
* sessionData}, then once with null to indicate the end of the list.
*/
getAllEndToEndInboundGroupSessions(txn, func) {
this._backendPromise.then(backend => {
backend.getAllEndToEndInboundGroupSessions(txn, func);
});
this._backend.getAllEndToEndInboundGroupSessions(txn, func);
}
/**
@@ -469,11 +465,9 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backendPromise.then(backend => {
backend.addEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
});
this._backend.addEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
/**
@@ -486,21 +480,17 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
this._backendPromise.then(backend => {
backend.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
});
this._backend.storeEndToEndInboundGroupSession(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
) {
this._backendPromise.then(backend => {
backend.storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
);
});
this._backend.storeEndToEndInboundGroupSessionWithheld(
senderCurve25519Key, sessionId, sessionData, txn,
);
}
// End-to-end device tracking
@@ -516,9 +506,7 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndDeviceData(deviceData, txn) {
this._backendPromise.then(backend => {
backend.storeEndToEndDeviceData(deviceData, txn);
});
this._backend.storeEndToEndDeviceData(deviceData, txn);
}
/**
@@ -529,9 +517,7 @@ export class IndexedDBCryptoStore {
* device data
*/
getEndToEndDeviceData(txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndDeviceData(txn, func);
});
this._backend.getEndToEndDeviceData(txn, func);
}
// End to End Rooms
@@ -543,9 +529,7 @@ export class IndexedDBCryptoStore {
* @param {*} txn An active transaction. See doTxn().
*/
storeEndToEndRoom(roomId, roomInfo, txn) {
this._backendPromise.then(backend => {
backend.storeEndToEndRoom(roomId, roomInfo, txn);
});
this._backend.storeEndToEndRoom(roomId, roomInfo, txn);
}
/**
@@ -554,9 +538,7 @@ export class IndexedDBCryptoStore {
* @param {function(Object)} func Function called with the end to end encrypted rooms
*/
getEndToEndRooms(txn, func) {
this._backendPromise.then(backend => {
backend.getEndToEndRooms(txn, func);
});
this._backend.getEndToEndRooms(txn, func);
}
// session backups
@@ -568,9 +550,7 @@ export class IndexedDBCryptoStore {
* @returns {Promise} resolves to an array of inbound group sessions
*/
getSessionsNeedingBackup(limit) {
return this._connect().then((backend) => {
return backend.getSessionsNeedingBackup(limit);
});
return this._backend.getSessionsNeedingBackup(limit);
}
/**
@@ -579,9 +559,7 @@ export class IndexedDBCryptoStore {
* @returns {Promise} resolves to the number of sessions
*/
countSessionsNeedingBackup(txn) {
return this._connect().then((backend) => {
return backend.countSessionsNeedingBackup(txn);
});
return this._backend.countSessionsNeedingBackup(txn);
}
/**
@@ -591,9 +569,7 @@ export class IndexedDBCryptoStore {
* @returns {Promise} resolves when the sessions are unmarked
*/
unmarkSessionsNeedingBackup(sessions, txn) {
return this._connect().then((backend) => {
return backend.unmarkSessionsNeedingBackup(sessions, txn);
});
return this._backend.unmarkSessionsNeedingBackup(sessions, txn);
}
/**
@@ -603,9 +579,7 @@ export class IndexedDBCryptoStore {
* @returns {Promise} resolves when the sessions are marked
*/
markSessionsNeedingBackup(sessions, txn) {
return this._connect().then((backend) => {
return backend.markSessionsNeedingBackup(sessions, txn);
});
return this._backend.markSessionsNeedingBackup(sessions, txn);
}
/**
@@ -630,9 +604,7 @@ export class IndexedDBCryptoStore {
* exception will propagate to the caller of the getFoo method.
*/
doTxn(mode, stores, func) {
return this._connect().then((backend) => {
return backend.doTxn(mode, stores, func);
});
return this._backend.doTxn(mode, stores, func);
}
}
@@ -367,12 +367,23 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
func(keys);
}
getSecretStorePrivateKey(txn, func, type) {
const key = getJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`);
func(key ? Uint8Array.from(key) : key);
}
storeCrossSigningKeys(txn, keys) {
setJsonItem(
this.store, KEY_CROSS_SIGNING_KEYS, keys,
);
}
storeSecretStorePrivateKey(txn, type, key) {
setJsonItem(
this.store, E2E_PREFIX + `ssss_cache.${type}`, Array.from(key),
);
}
doTxn(mode, stores, func) {
return Promise.resolve(func(null));
}
+36
View File
@@ -33,6 +33,8 @@ export class MemoryCryptoStore {
this._outgoingRoomKeyRequests = [];
this._account = null;
this._crossSigningKeys = null;
this._privateKeys = {};
this._backupKeys = {};
// Map of {devicekey -> {sessionId -> session pickle}}
this._sessions = {};
@@ -51,6 +53,18 @@ export class MemoryCryptoStore {
this._sessionsNeedingBackup = {};
}
/**
* Ensure the database exists and is up-to-date.
*
* This must be called before the store can be used.
*
* @return {Promise} resolves to the store.
*/
async startup() {
// No startup work to do for the memory store.
return this;
}
/**
* Delete all data from this store.
*
@@ -152,6 +166,19 @@ export class MemoryCryptoStore {
return Promise.resolve(null);
}
/**
*
* @param {Number} wantedState
* @return {Promise<Array<*>>} All OutgoingRoomKeyRequests in state
*/
getAllOutgoingRoomKeyRequestsByState(wantedState) {
return Promise.resolve(
this._outgoingRoomKeyRequests.filter(
(r) => r.state == wantedState,
),
);
}
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
const results = [];
@@ -243,10 +270,19 @@ export class MemoryCryptoStore {
func(this._crossSigningKeys);
}
getSecretStorePrivateKey(txn, func, type) {
const result = this._privateKeys[type];
return func(result || null);
}
storeCrossSigningKeys(txn, keys) {
this._crossSigningKeys = keys;
}
storeSecretStorePrivateKey(txn, type, key) {
this._privateKeys[type] = key;
}
// Olm Sessions
countEndToEndSessions(txn, func) {
+103 -8
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.
@@ -24,6 +25,8 @@ import {EventEmitter} from 'events';
import {logger} from '../../logger';
import {DeviceInfo} from '../deviceinfo';
import {newTimeoutError} from "./Error";
import {CrossSigningInfo} from "../CrossSigning";
import {decodeBase64} from "../olmlib";
const timeoutException = new Error("Verification timed out");
@@ -76,6 +79,8 @@ export class VerificationBase extends EventEmitter {
this._transactionTimeoutTimer = null;
}
static keyRequestTimeoutMs = 1000 * 60;
get initiatedByMe() {
// if there is no start event yet,
// we probably want to send it,
@@ -130,6 +135,8 @@ export class VerificationBase extends EventEmitter {
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;
@@ -155,10 +162,13 @@ export class VerificationBase extends EventEmitter {
} else if (e.getType() === "m.key.verification.cancel") {
const reject = this._reject;
this._reject = undefined;
const content = e.getContent();
const {reason, code} = content;
reject(new Error(`Other side cancelled verification ` +
`because ${reason} (${code})`));
// 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
@@ -181,11 +191,95 @@ export class VerificationBase extends EventEmitter {
done() {
this._endTimer(); // always kill the activity timer
if (!this._done) {
if (this._channel.needsDoneMessage) {
// verification in DM requires a done message
this._send("m.key.verification.done", {});
}
this.request.onVerifierFinished();
this._resolve();
//#region Cross-signing keys request
// If this is a self-verification, ask the other party for keys
if (this._baseApis.getUserId() !== this.userId) {
return;
}
// FIXME: This is a lot of logic that isn't anything to do with verification
// and probably ought to be somewhere else.
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;
/* 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 } = client.requestSecret(
`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"),
);
});
// also request and cache the key backup key
const backupKeyPromise = new Promise(async resolve => {
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
if (!cachedKey) {
logger.info("No cached backup key found. Requesting...");
const secretReq = client.requestSecret(
'm.megolm_backup.v1', [this.deviceId],
);
const base64Key = await secretReq.promise;
logger.info("Got key backup key, decoding...");
const decodedKey = decodeBase64(base64Key);
logger.info("Decoded backup key, storing...");
client._crypto.storeSessionBackupPrivateKey(
Uint8Array.from(decodedKey),
);
logger.info("Backup key stored. Starting backup restore...");
const backupInfo = await client.getKeyBackupVersion();
// no need to await for this - just let it go in the bg
client.restoreKeyBackupWithCache(
undefined, undefined, backupInfo,
).then(() => {
logger.info("Backup restored.");
});
}
resolve();
});
/* We call getCrossSigningKey() for its side-effects */
return Promise.race([
Promise.all([
crossSigning.getCrossSigningKey("self_signing"),
crossSigning.getCrossSigningKey("user_signing"),
backupKeyPromise,
]),
timeout,
]).then(resolve, reject);
}).catch((e) => {
console.warn("VerificationBase: failure while requesting keys:", e);
});
//#endregion
}
}
@@ -193,6 +287,7 @@ export class VerificationBase extends EventEmitter {
this._endTimer(); // always kill the activity timer
if (!this._done) {
this.cancelled = true;
this.request.onVerifierCancelled();
if (this.userId && this.deviceId) {
// send a cancellation to the other user (if it wasn't
// cancelled by the other user)
+228 -36
View File
@@ -23,8 +23,9 @@ limitations under the License.
import {VerificationBase as Base} from "./Base";
import {
newKeyMismatchError,
newUserMismatchError,
newUserCancelledError,
} from './Error';
import {encodeUnpaddedBase64, decodeBase64} from "../olmlib";
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
@@ -49,47 +50,46 @@ export class ReciprocateQRCode extends Base {
"with this method yet.");
}
const targetUserId = this.startEvent.getSender();
if (!this.userId) {
console.log("Asking to confirm user ID");
this.userId = await new Promise((resolve, reject) => {
this.emit("confirm_user_id", {
userId: targetUserId,
confirm: resolve, // takes a userId
cancel: () => reject(newUserMismatchError()),
});
});
} else if (targetUserId !== this.userId) {
throw newUserMismatchError({
expected: this.userId,
actual: targetUserId,
});
}
if (this.startEvent.getContent()['secret'] !== this.request.encodedSharedSecret) {
const {qrCodeData} = this.request;
// 1. check the secret
if (this.startEvent.getContent()['secret'] !== qrCodeData.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.estimatedTargetDevice.deviceId;
// 2. ask if other user shows shield as well
await new Promise((resolve, reject) => {
this.reciprocateQREvent = {
confirm: resolve,
cancel: () => reject(newUserCancelledError()),
};
this.emit("show_reciprocate_qr", this.reciprocateQREvent);
});
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];
// 3. determine key to sign / mark as trusted
const keys = {};
switch (qrCodeData.mode) {
case MODE_VERIFY_OTHER_USER: {
// add master key to keys to be signed, only if we're not doing self-verification
const masterKey = qrCodeData.otherUserMasterKey;
keys[`ed25519:${masterKey}`] = masterKey;
break;
}
case MODE_VERIFY_SELF_TRUSTED: {
const deviceId = this.request.targetDevice.deviceId;
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey;
break;
}
case MODE_VERIFY_SELF_UNTRUSTED: {
const masterKey = qrCodeData.myMasterKey;
keys[`ed25519:${masterKey}`] = masterKey;
break;
}
}
// 4. sign the key (or mark own MSK as verified in case of MODE_VERIFY_SELF_TRUSTED)
await this._verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
// make sure the device has the expected keys
const targetKey = keys[keyId];
if (!targetKey) throw newKeyMismatchError();
@@ -106,8 +106,200 @@ export class ReciprocateQRCode extends Base {
throw newKeyMismatchError();
}
}
// Otherwise it is probably fine
});
}
}
const CODE_VERSION = 0x02; // the version of binary QR codes we support
const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us
const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
export class QRCodeData {
constructor(
mode, sharedSecret, otherUserMasterKey,
otherDeviceKey, myMasterKey, buffer,
) {
this._sharedSecret = sharedSecret;
this._mode = mode;
this._otherUserMasterKey = otherUserMasterKey;
this._otherDeviceKey = otherDeviceKey;
this._myMasterKey = myMasterKey;
this._buffer = buffer;
}
static async create(request, client) {
const sharedSecret = QRCodeData._generateSharedSecret();
const mode = QRCodeData._determineMode(request, client);
let otherUserMasterKey = null;
let otherDeviceKey = null;
let myMasterKey = null;
if (mode === MODE_VERIFY_OTHER_USER) {
const otherUserCrossSigningInfo =
client.getStoredCrossSigningForUser(request.otherUserId);
otherUserMasterKey = otherUserCrossSigningInfo.getId("master");
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
otherDeviceKey = await QRCodeData._getOtherDeviceKey(request, client);
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
const myUserId = client.getUserId();
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
myMasterKey = myCrossSigningInfo.getId("master");
}
const qrData = QRCodeData._generateQrData(
request, client, mode,
sharedSecret,
otherUserMasterKey,
otherDeviceKey,
myMasterKey,
);
const buffer = QRCodeData._generateBuffer(qrData);
return new QRCodeData(mode, sharedSecret,
otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
}
get buffer() {
return this._buffer;
}
get mode() {
return this._mode;
}
/**
* only set when mode is MODE_VERIFY_SELF_TRUSTED
* @return {string} device key of other party at time of generating QR code
*/
get otherDeviceKey() {
return this._otherDeviceKey;
}
/**
* only set when mode is MODE_VERIFY_OTHER_USER
* @return {string} master key of other party at time of generating QR code
*/
get otherUserMasterKey() {
return this._otherUserMasterKey;
}
/**
* only set when mode is MODE_VERIFY_SELF_UNTRUSTED
* @return {string} own master key at time of generating QR code
*/
get myMasterKey() {
return this._myMasterKey;
}
/**
* The unpadded base64 encoded shared secret.
*/
get encodedSharedSecret() {
return this._sharedSecret;
}
static _generateSharedSecret() {
const secretBytes = new Uint8Array(11);
global.crypto.getRandomValues(secretBytes);
return encodeUnpaddedBase64(secretBytes);
}
static async _getOtherDeviceKey(request, client) {
const myUserId = client.getUserId();
const otherDevice = request.targetDevice;
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
const device = await client.getStoredDevice(myUserId, otherDeviceId);
if (!device) {
throw new Error("could not find device " + otherDeviceId);
}
const key = device.getFingerprint();
return key;
}
static _determineMode(request, client) {
const myUserId = client.getUserId();
const otherUserId = request.otherUserId;
let mode = MODE_VERIFY_OTHER_USER;
if (myUserId === otherUserId) {
// Mode changes depending on whether or not we trust the master cross signing key
const myTrust = client.checkUserTrust(myUserId);
if (myTrust.isCrossSigningVerified()) {
mode = MODE_VERIFY_SELF_TRUSTED;
} else {
mode = MODE_VERIFY_SELF_UNTRUSTED;
}
}
return mode;
}
static _generateQrData(request, client, mode,
encodedSharedSecret, otherUserMasterKey,
otherDeviceKey, myMasterKey,
) {
const myUserId = client.getUserId();
const transactionId = request.channel.transactionId;
const qrData = {
prefix: BINARY_PREFIX,
version: CODE_VERSION,
mode,
transactionId,
firstKeyB64: '', // worked out shortly
secondKeyB64: '', // worked out shortly
secretB64: encodedSharedSecret,
};
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
if (mode === MODE_VERIFY_OTHER_USER) {
// First key is our master cross signing key
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
// Second key is the other user's master cross signing key
qrData.secondKeyB64 = otherUserMasterKey;
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
// First key is our master cross signing key
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
qrData.secondKeyB64 = otherDeviceKey;
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
// First key is our device's key
qrData.firstKeyB64 = client.getDeviceEd25519Key();
// Second key is what we think our master cross signing key is
qrData.secondKeyB64 = myMasterKey;
}
return qrData;
}
static _generateBuffer(qrData) {
let buf = Buffer.alloc(0); // we'll concat our way through life
const appendByte = (b) => {
const tmpBuf = Buffer.from([b]);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendInt = (i) => {
const tmpBuf = Buffer.alloc(2);
tmpBuf.writeInt16BE(i, 0);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendStr = (s, enc, withLengthPrefix = true) => {
const tmpBuf = Buffer.from(s, enc);
if (withLengthPrefix) appendInt(tmpBuf.byteLength);
buf = Buffer.concat([buf, tmpBuf]);
};
const appendEncBase64 = (b64) => {
const b = decodeBase64(b64);
const tmpBuf = Buffer.from(b);
buf = Buffer.concat([buf, tmpBuf]);
};
// Actually build the buffer for the QR code
appendStr(qrData.prefix, "ascii", false);
appendByte(qrData.version);
appendByte(qrData.mode);
appendStr(qrData.transactionId, "utf-8");
appendEncBase64(qrData.firstKeyB64);
appendEncBase64(qrData.secondKeyB64);
appendEncBase64(qrData.secretB64);
return buf;
}
}
+18 -10
View File
@@ -295,7 +295,7 @@ export class SAS extends Base {
const hashCommitment = content.commitment;
const olmSAS = new global.Olm.SAS();
try {
this._send("m.key.verification.key", {
await this._send("m.key.verification.key", {
key: olmSAS.get_pubkey(),
});
@@ -318,9 +318,13 @@ export class SAS extends Base {
const verifySAS = new Promise((resolve, reject) => {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS, macMethod);
resolve();
confirm: async () => {
try {
await this._sendMAC(olmSAS, macMethod);
resolve();
} catch (err) {
reject(err);
}
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
@@ -377,7 +381,7 @@ export class SAS extends Base {
const olmSAS = new global.Olm.SAS();
try {
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
this._send("m.key.verification.accept", {
await this._send("m.key.verification.accept", {
key_agreement_protocol: keyAgreement,
hash: hashMethod,
message_authentication_code: macMethod,
@@ -391,7 +395,7 @@ export class SAS extends Base {
// FIXME: make sure event is properly formed
content = e.getContent();
olmSAS.set_their_key(content.key);
this._send("m.key.verification.key", {
await this._send("m.key.verification.key", {
key: olmSAS.get_pubkey(),
});
@@ -403,9 +407,13 @@ export class SAS extends Base {
const verifySAS = new Promise((resolve, reject) => {
this.sasEvent = {
sas: generateSas(sasBytes, sasMethods),
confirm: () => {
this._sendMAC(olmSAS, macMethod);
resolve();
confirm: async () => {
try {
await this._sendMAC(olmSAS, macMethod);
resolve();
} catch(err) {
reject(err);
}
},
cancel: () => reject(newUserCancelledError()),
mismatch: () => reject(newMismatchedSASError()),
@@ -461,7 +469,7 @@ export class SAS extends Base {
keyList.sort().join(","),
baseInfo + "KEY_IDS",
);
this._send("m.key.verification.mac", { mac, keys });
return this._send("m.key.verification.mac", { mac, keys });
}
async _checkMAC(olmSAS, content, method) {
@@ -44,11 +44,6 @@ export class InRoomChannel {
this._requestEventId = null;
}
/** Whether this channel needs m.key.verification.done messages to be sent after a successful verification */
get needsDoneMessage() {
return true;
}
get receiveStartFromOtherDevices() {
return true;
}
@@ -183,6 +178,11 @@ export class InRoomChannel {
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
*/
async handleEvent(event, request, isLiveEvent) {
// prevent processing the same event multiple times, as under
// some circumstances Room.timeline can get emitted twice for the same event
if (request.hasEventId(event.getId())) {
return;
}
const type = InRoomChannel.getEventType(event);
// do validations that need state (roomId, userId),
// ignore if invalid
@@ -61,10 +61,6 @@ export class ToDeviceChannel {
return this._deviceId;
}
get needsDoneMessage() {
return true;
}
static getEventType(event) {
return event.getType();
}
@@ -23,7 +23,7 @@ import {
newUnexpectedMessageError,
newUnknownMethodError,
} from "../Error";
import * as olmlib from "../../olmlib";
import {QRCodeData, SCAN_QR_CODE_METHOD} from "../QRCode";
// the recommended amount of time before a verification request
// should be (automatically) cancelled without user interaction
@@ -70,9 +70,16 @@ export class VerificationRequest extends EventEmitter {
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;
this._cancelled = false;
this._chosenMethod = null;
// we keep a copy of the QR Code data (including other user master key) around
// for QR reciprocate verification, to protect against
// cross-signing identity reset between the .ready and .start event
// and signing the wrong key after .start
this._qrCodeData = null;
}
/**
@@ -153,6 +160,11 @@ export class VerificationRequest extends EventEmitter {
return this._commonMethods;
}
/** the method picked in the .start event */
get chosenMethod() {
return this._chosenMethod;
}
/** The current remaining amount of ms before the request should be automatically cancelled */
get timeout() {
const requestEvent = this._getEventByEither(REQUEST_TYPE);
@@ -200,19 +212,34 @@ export class VerificationRequest extends EventEmitter {
this._phase !== PHASE_CANCELLED;
}
/** Only set after a .ready if the other party can scan a QR code */
get qrCodeData() {
return this._qrCodeData;
}
/** 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
* @param {boolean} force to check even if the phase is not ready or started yet, internal usage
* @return {bool} whether or not the other party said the supported the method */
otherPartySupportsMethod(method) {
if (!this.ready && !this.started) {
otherPartySupportsMethod(method, force = false) {
if (!force && !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();
@@ -276,6 +303,10 @@ export class VerificationRequest extends EventEmitter {
return this.channel.userId;
}
get isSelfVerification() {
return this._client.getUserId() === this.otherUserId;
}
/**
* The id of the user that cancelled the request,
* only defined when phase is PHASE_CANCELLED
@@ -305,14 +336,6 @@ export class VerificationRequest extends EventEmitter {
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
@@ -359,6 +382,7 @@ export class VerificationRequest extends EventEmitter {
if (!this._verifier) {
throw newUnknownMethodError();
}
this._chosenMethod = method;
}
}
return this._verifier;
@@ -372,7 +396,6 @@ export class VerificationRequest extends EventEmitter {
if (!this.observeOnly && this._phase === PHASE_UNSENT) {
const methods = [...this._verificationMethods.keys()];
await this.channel.send(REQUEST_TYPE, {methods});
this._generateSharedSecret();
}
}
@@ -405,16 +428,9 @@ export class VerificationRequest extends EventEmitter {
this._accepting = true;
this.emit("change");
await this.channel.send(READY_TYPE, {methods});
this._generateSharedSecret();
}
}
_generateSharedSecret() {
const secretBytes = new Uint8Array(32); // 256bits
global.crypto.getRandomValues(secretBytes);
this._sharedSecret = olmlib.encodeBase64(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.
@@ -505,12 +521,12 @@ export class VerificationRequest extends EventEmitter {
}
const ourDoneEvent = this._eventsByUs.get(DONE_TYPE);
if (ourDoneEvent && phase() === PHASE_STARTED) {
if (this._verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
transitions.push({phase: PHASE_DONE});
}
const cancelEvent = this._getEventByEither(CANCEL_TYPE);
if (cancelEvent && phase() !== PHASE_DONE) {
if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) {
transitions.push({phase: PHASE_CANCELLED, event: cancelEvent});
return transitions;
}
@@ -549,10 +565,80 @@ export class VerificationRequest extends EventEmitter {
const {method} = event.getContent();
if (!this._verifier && !this.observeOnly) {
this._verifier = this._createVerifier(method, event);
if (!this._verifier) {
this.cancel({
code: "m.unknown_method",
reason: `Unknown method: ${method}`,
});
} else {
this._chosenMethod = method;
}
}
}
}
_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;
let oldRaceIdentifier;
if (this.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 (this.isSelfVerification) {
const newContent = newEvent.getContent();
newRaceIdentifier = newContent && newContent.from_device;
} else {
newRaceIdentifier = newEvent.getSender();
}
return newRaceIdentifier < oldRaceIdentifier;
}
hasEventId(eventId) {
for (const event of this._eventsByUs.values()) {
if (event.getId() === eventId) {
return true;
}
}
for (const event of this._eventsByThem.values()) {
if (event.getId() === eventId) {
return true;
}
}
return false;
}
/**
* 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.
@@ -565,7 +651,7 @@ export class VerificationRequest extends EventEmitter {
*/
async handleEvent(type, event, isLiveEvent, isRemoteEcho, isSentByUs) {
// if reached phase cancelled or done, ignore anything else that comes
if (!this.pending) {
if (this.done || this.cancelled) {
return;
}
const wasObserveOnly = this._observeOnly;
@@ -578,31 +664,32 @@ export class VerificationRequest extends EventEmitter {
}
}
// This assumes verification won't need to send an event with
// the same type for the same party twice.
// This is true for QR and SAS verification, and was
// added here to prevent verification getting cancelled
// when the server duplicates an event (https://github.com/matrix-org/synapse/issues/3365)
const isDuplicateEvent = isSentByUs ?
this._eventsByUs.has(type) :
this._eventsByThem.has(type);
if (isDuplicateEvent) {
return;
}
const oldPhase = this.phase;
this._addEvent(type, event, isSentByUs);
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);
}
// 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 oldEvent = this._verifier.startEvent;
// if the verifier does not have a startEvent, it is because it's still sending and we are on the initator side
const oldSender = oldEvent ?
oldEvent.getSender() :
this._client.getUserId();
const newEventWinsRace = event.getSender() < oldSender;
const newEventWinsRace = this._isWinningStartRace(event);
if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) {
this._verifier.switchStartEvent(event);
} else if (!isRemoteEcho) {
if (type === CANCEL_TYPE || (this._verifier.events
if (type === CANCEL_TYPE || (this._verifier.events
&& this._verifier.events.includes(type))) {
this._verifier.handleEvent(event);
}
@@ -610,6 +697,20 @@ export class VerificationRequest extends EventEmitter {
}
if (newTransitions.length) {
// create QRCodeData if the other side can scan
// important this happens before emitting a phase change,
// so listeners can rely on it being there already
// We only do this for live events because it is important that
// we sign the keys that were in the QR code, and not the keys
// we happen to have at some later point in time.
if (isLiveEvent && newTransitions.some(t => t.phase === PHASE_READY)) {
const shouldGenerateQrCode =
this.otherPartySupportsMethod(SCAN_QR_CODE_METHOD, true);
if (shouldGenerateQrCode) {
this._qrCodeData = await QRCodeData.create(this, this._client);
}
}
const lastTransition = newTransitions[newTransitions.length - 1];
const {phase} = lastTransition;
@@ -622,9 +723,11 @@ export class VerificationRequest extends EventEmitter {
} 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 ${JSON.stringify(event.getContent())} ` +
`deviceId:${this.channel.deviceId} ` +
`sender:${event.getSender()}, isSentByUs:${isSentByUs} ` +
`${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}`);
}
@@ -668,7 +771,12 @@ export class VerificationRequest extends EventEmitter {
const isUnexpectedRequest = type === REQUEST_TYPE && this.phase !== PHASE_UNSENT;
const isUnexpectedReady = type === READY_TYPE && this.phase !== PHASE_REQUESTED;
if (isUnexpectedRequest || isUnexpectedReady) {
// 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}`;
@@ -750,4 +858,23 @@ export class VerificationRequest extends EventEmitter {
}
return true;
}
onVerifierCancelled() {
this._cancelled = true;
// move to cancelled phase
const newTransitions = this._applyPhaseTransitions();
if (newTransitions.length) {
this._setPhase(newTransitions[newTransitions.length - 1].phase);
}
}
onVerifierFinished() {
this.channel.send("m.key.verification.done", {});
this._verifierHasFinished = true;
// move to .done phase
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;
}
}
+1
View File
@@ -47,6 +47,7 @@ export const PREFIX_UNSTABLE = "/_matrix/client/unstable";
/**
* URI path for v1 of the the identity API
* @deprecated Use v2.
*/
export const PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
+7
View File
@@ -21,5 +21,12 @@ import request from "request";
matrixcs.request(request);
utils.runPolyfills();
try {
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {
console.log('nodejs was compiled without crypto support');
}
export * from "./matrix";
export default matrixcs;
+7 -1
View File
@@ -163,6 +163,8 @@ InteractiveAuth.prototype = {
*/
poll: async function() {
if (!this._data.session) return;
// likewise don't poll if there is no auth session in progress
if (!this._resolveFunc) return;
// if we currently have a request in flight, there's no point making
// another just to check what the status is
if (this._submitPromise) return;
@@ -318,6 +320,8 @@ InteractiveAuth.prototype = {
try {
const result = await this._requestCallback(auth, background);
this._resolveFunc(result);
this._resolveFunc = null;
this._rejectFunc = null;
} catch (error) {
// sometimes UI auth errors don't come with flows
const errorFlows = error.data ? error.data.flows : null;
@@ -379,8 +383,10 @@ InteractiveAuth.prototype = {
// (or not being registered, depending on what we're trying
// to do) or it could be a network failure. Either way, pass
// the failure up as the user can't complete auth if we can't
// send the email, foe whatever reason.
// send the email, for whatever reason.
this._rejectFunc(e);
this._resolveFunc = null;
this._rejectFunc = null;
} finally {
this._requestingEmailToken = false;
}
+23 -3
View File
@@ -861,11 +861,23 @@ Room.prototype.getAliases = function() {
Room.prototype.getCanonicalAlias = function() {
const canonicalAlias = this.currentState.getStateEvents("m.room.canonical_alias", "");
if (canonicalAlias) {
return canonicalAlias.getContent().alias;
return canonicalAlias.getContent().alias || null;
}
return null;
};
/**
* Get this room's alternative aliases
* @return {array} The room's alternative aliases, or an empty array
*/
Room.prototype.getAltAliases = function() {
const canonicalAlias = this.currentState.getStateEvents("m.room.canonical_alias", "");
if (canonicalAlias) {
return canonicalAlias.getContent().alt_aliases || [];
}
return [];
};
/**
* Add events to a timeline
*
@@ -1266,6 +1278,11 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
const newEventId = remoteEvent.getId();
const oldStatus = localEvent.status;
logger.debug(
`Got remote echo for event ${oldEventId} -> ${newEventId} ` +
`old status ${oldStatus}`,
);
// no longer pending
delete this._txnToEvent[remoteEvent.getUnsigned().transaction_id];
@@ -1335,7 +1352,10 @@ ALLOWED_TRANSITIONS[EventStatus.CANCELLED] =
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
*/
Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
logger.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()}`);
logger.log(
`setting pendingEvent status to ${newStatus} in ${event.getRoomId()} ` +
`event ID ${event.getId()} -> ${newEventId}`,
);
// if the message was sent, we expect an event id
if (newStatus == EventStatus.SENT && !newEventId) {
@@ -1821,7 +1841,7 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
let alias = room.getCanonicalAlias();
if (!alias) {
const aliases = room.getAliases();
const aliases = room.getAltAliases();
if (aliases.length) {
alias = aliases[0];
+14
View File
@@ -734,3 +734,17 @@ export async function promiseMapSeries<T>(
export function promiseTry<T>(fn: () => T): Promise<T> {
return new Promise((resolve) => resolve(fn()));
}
// We need to be able to access the Node.js crypto library from within the
// Matrix SDK without needing to `require("crypto")`, which will fail in
// browsers. So `index.ts` will call `setCrypto` to store it, and when we need
// it, we can call `getCrypto`.
let crypto: Object;
export function setCrypto(c: Object) {
crypto = c;
}
export function getCrypto(): Object {
return crypto;
}
+14 -34
View File
@@ -165,29 +165,26 @@ MatrixCall.prototype.placeVideoCall = function(remoteVideoElement, localVideoEle
* @throws If you have not specified a listener for 'error' events.
*/
MatrixCall.prototype.placeScreenSharingCall =
function(remoteVideoElement, localVideoElement) {
async function(remoteVideoElement, localVideoElement) {
debuglog("placeScreenSharingCall");
checkForErrorListener(this);
const screenConstraints = _getScreenSharingConstraints(this);
if (!screenConstraints) {
return;
}
this.localVideoElement = localVideoElement;
this.remoteVideoElement = remoteVideoElement;
const self = this;
this.webRtc.getUserMedia(screenConstraints, function(stream) {
self.screenSharingStream = stream;
try {
self.screenSharingStream = await this.webRtc.getDisplayMedia({'audio': false});
debuglog("Got screen stream, requesting audio stream...");
const audioConstraints = _getUserMediaVideoContraints('voice');
_placeCallWithConstraints(self, audioConstraints);
}, function(err) {
} catch(err) {
self.emit("error",
callError(
MatrixCall.ERR_NO_USER_MEDIA,
"Failed to get screen-sharing stream: " + err,
),
);
});
}
this.type = 'video';
_tryPlayRemoteStream(this);
};
@@ -1231,31 +1228,6 @@ const _createPeerConnection = function(self) {
return pc;
};
const _getScreenSharingConstraints = function(call) {
const screen = global.screen;
if (!screen) {
call.emit("error", callError(
MatrixCall.ERR_NO_USER_MEDIA,
"Couldn't determine screen sharing constaints.",
));
return;
}
return {
video: {
mediaSource: 'screen',
mandatory: {
chromeMediaSource: "screen",
chromeMediaSourceId: "" + Date.now(),
maxWidth: screen.width,
maxHeight: screen.height,
minFrameRate: 1,
maxFrameRate: 10,
},
},
};
};
const _getUserMediaVideoContraints = function(callType) {
const isWebkit = !!global.window.navigator.webkitGetUserMedia;
@@ -1371,6 +1343,14 @@ export function createNewMatrixCall(client, roomId, options) {
};
}
const getDisplayMedia = (
w.navigator.mediaDevices && w.navigator.mediaDevices.getDisplayMedia ||
w.navigator.getDisplayMedia
);
if (getDisplayMedia) {
webRtc.getDisplayMedia = getDisplayMedia.bind(w.navigator.mediaDevices);
}
// Firefox throws on so little as accessing the RTCPeerConnection when operating in
// a secure mode. There's some information at https://bugzilla.mozilla.org/show_bug.cgi?id=1542616
// though the concern is that the browser throwing a SecurityError will brick the
+1250 -1108
View File
File diff suppressed because it is too large Load Diff