Compare commits

...

383 Commits

Author SHA1 Message Date
RiotRobot c8ab82010a v2.4.5 2019-11-27 10:17:54 +00:00
RiotRobot bf1bec9c6c Prepare changelog for v2.4.5 2019-11-27 10:17:54 +00:00
J. Ryan Stinnett 396db30fbf Update tests 2019-11-26 12:04:14 -07:00
J. Ryan Stinnett 6b38868de6 Relax identity server discovery checks to FAIL_PROMPT
As discussed in MSC2284, this relaxes the identity server discovery to a
`FAIL_PROMPT` state so that clients can choose to warn and continue.

Part of https://github.com/vector-im/riot-web/issues/11102
Implements https://github.com/matrix-org/matrix-doc/pull/2284
2019-11-26 12:04:07 -07:00
Matthew Hodgson 01a46ad880 log outbound to_device msgs for tracking keyshares 2019-11-26 12:03:34 -07:00
Matthew Hodgson 46f8251e94 s/console/logger/ as per review 2019-11-26 12:03:28 -07:00
Matthew Hodgson 77f882f45a log keyshare ID 2019-11-26 12:03:24 -07:00
Matthew Hodgson 8c72fd104e lint 2019-11-26 12:03:20 -07:00
Matthew Hodgson 549656884b expand e2ee logging to better debug UISIs 2019-11-26 12:03:11 -07:00
RiotRobot c30a8b5a29 v2.4.4 2019-11-25 13:10:04 +00:00
RiotRobot 295010893d Prepare changelog for v2.4.4 2019-11-25 13:10:04 +00:00
RiotRobot 12bb0b86dd v2.4.4-rc.1 2019-11-20 18:19:30 +00:00
RiotRobot 165c1fc0b6 Prepare changelog for v2.4.4-rc.1 2019-11-20 18:19:30 +00:00
Bruno Windels c785b10603 Merge pull request #1077 from matrix-org/bwindels/dm-verif-in-e2ee-rooms
Fix SAS verification in encrypted DMs
2019-11-19 14:32:01 +00:00
Bruno Windels 90512bdd5f also listen for non-encrypted events when verifying over DM 2019-11-19 15:23:05 +01:00
J. Ryan Stinnett 10751e9a6d Merge pull request #1078 from matrix-org/jryans/4s-new-key-backup
Cross-signing / secret storage tweaks
2019-11-19 12:54:38 +00:00
J. Ryan Stinnett d2ebc58c3c Use secret storage alg const in tests 2019-11-19 11:46:08 +00:00
J. Ryan Stinnett d51c5a2d68 Rename secret storage file to match the default class 2019-11-19 11:21:20 +00:00
J. Ryan Stinnett 1f24845431 Standardise naming of key ID variables in secret storage
Keys in secret storage have both an ID and an optional name, but most `keyName`
variables were actually storing the ID value. This renames and standardises to
avoid confusion.
2019-11-19 10:48:47 +00:00
Bruno Windels 3b02b62ba5 add m.relates_to back to the content on the requesting side for e2e room
as it needs to be added to the commitment hash
as before, getContent() in an e2ee room doesn't return the relation
2019-11-18 18:34:05 +01:00
Bruno Windels 24ae787736 make it explicit that the transaction id is added for the start event
as it should included in the commitment hash
2019-11-18 18:33:11 +01:00
Bruno Windels cd735ef459 use getRelation as getContent()[m.relates_to] doesn't work in e2ee rooms 2019-11-18 18:31:39 +01:00
Bruno Windels 180fea8ace only send decrypted events to Verifier in e2ee rooms 2019-11-18 18:30:43 +01:00
J. Ryan Stinnett 5f02c4b5ad Namespace default secret storage key methods 2019-11-18 15:19:18 +00:00
David Baker 41680f6089 Merge pull request #1075 from matrix-org/dbkr/fix_key_backup_local_trust
Fix local trust for key backups
2019-11-18 14:57:35 +00:00
J. Ryan Stinnett 730f7d3dff Extract secret storage algorithm to constant 2019-11-18 14:38:02 +00:00
David Baker d32033f105 Merge remote-tracking branch 'origin/develop' into dbkr/fix_key_backup_local_trust 2019-11-18 14:10:04 +00:00
David Baker 440274d639 Fix local trust for key backups
https://github.com/matrix-org/matrix-js-sdk/pull/832 added
cross-signing checks for backup trust but we failed to merge in the
check for the the backup being trusted locally.

Fixes https://github.com/vector-im/riot-web/issues/11404
2019-11-18 13:30:00 +00:00
J. Ryan Stinnett f93130a8a7 Add method to check whether cross-signing has keys 2019-11-18 12:32:39 +00:00
Bruno Windels 3d9bddfb9f Merge pull request #1072 from matrix-org/bwindels/userinfomakeover
Add method to get last active timestamp in room
2019-11-15 16:39:31 +00:00
Travis Ralston 439abbcce9 Merge pull request #1071 from matrix-org/travis/new-deactivate
Check the right Synapse endpoint for determining admin capabilities
2019-11-15 09:17:12 -07:00
David Baker ac91367801 Merge pull request #832 from matrix-org/dbkr/cross_signing
Cross Signing Support
2019-11-15 16:09:06 +00:00
David Baker 2a63cc474c Update import 2019-11-15 15:57:25 +00:00
David Baker 56261263f5 Rename backup_password & functions
Not Just For Backups Anymore
2019-11-15 15:54:43 +00:00
David Baker 04b57bbe9d Remove ghost of some old code 2019-11-15 15:27:02 +00:00
David Baker c550f83a04 update jsdoc 2019-11-15 14:57:29 +00:00
David Baker 5224ef4b1f This is now implemented
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-11-15 14:56:05 +00:00
David Baker 2ab033e76e is now implemented 2019-11-15 14:45:43 +00:00
David Baker fa2e669eda More jsdoc updates
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-11-15 14:44:08 +00:00
David Baker f0ba1f2ac0 c+p fail
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-11-15 14:43:41 +00:00
David Baker 6d0237ec71 This now returns DeviceTrustLevel too 2019-11-15 14:42:26 +00:00
David Baker 97dff4640a Capitalise jsdoc
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-11-15 14:41:12 +00:00
David Baker 00b571a429 c+p fail
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-11-15 14:40:57 +00:00
David Baker 86e0f49231 c+p fail
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-11-15 14:40:16 +00:00
David Baker f2f205f9bd Typo
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-11-15 14:38:44 +00:00
David Baker f84ec090cb backticks in jsdoc
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-11-15 14:38:27 +00:00
David Baker d37ed9ff6f lint 2019-11-15 12:39:14 +00:00
David Baker f5a5f5e51a Update yarn.lock 2019-11-15 12:31:22 +00:00
David Baker fe010242d9 Why is 'cross-signing' so hard to type?
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-11-15 12:30:05 +00:00
David Baker 545ebf81bf Move Crypto.prototype.init back to its rightful place 2019-11-15 12:29:03 +00:00
David Baker 408934932a copy jsdoc to internal methods 2019-11-15 12:27:14 +00:00
David Baker 6f42824c35 Typo
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-11-15 12:26:24 +00:00
David Baker c3215d51bd Switch the CroosSigningLevel constants
we check in resetKeys and set all if it's & 4 anyway, so may as well
make the constants a normal bitmask and then we can use the MASTER
constant below.
2019-11-15 12:23:37 +00:00
David Baker e541b96a71 Change check{User|Device}Trust interfaces
...to return objects with functions rather than a bitmask
2019-11-15 12:15:13 +00:00
Travis Ralston 904a2f466e Maybe use the right user ID too 2019-11-14 11:54:13 -07:00
Travis Ralston bad48da11a Check the right Synapse endpoint for determining admin capabilities 2019-11-14 11:50:06 -07:00
David Baker ce2d1d6e2b Don't emit event here, as per comment 2019-11-14 17:41:58 +00:00
Bruno Windels 2820071db1 add method to get last active timestamp in room 2019-11-14 17:00:28 +01:00
David Baker 5937185ce9 Assert usage of setDeviceVerification for cross-signing keys
We can't mark a cross-signing key as blocked/unblocked, known/unknown
or unverified, so throw an exception instead of doing nothing.

Also comment what's going on in this function.
2019-11-14 14:24:41 +00:00
David Baker be9b7a0d24 Remove getPublicKey
which was the same as getId
2019-11-14 14:10:13 +00:00
David Baker 7ca09ad749 tariling space 2019-11-14 12:18:07 +00:00
David Baker 686a7a40f9 Remove outdated comment
uhoreg says this is fine now...
2019-11-14 12:11:07 +00:00
David Baker 2a7b2835b6 remove unused function 2019-11-14 12:11:07 +00:00
David Baker 69ecf3b145 jsdoc formatting 2019-11-14 12:11:07 +00:00
David Baker 2cd748b50c Add matrix foundation copyright
The creation of this file just predates matrix.org foundation so it
should have both
2019-11-14 12:11:07 +00:00
David Baker 291133beb9 Fix comment 2019-11-14 12:11:07 +00:00
David Baker e10c17c866 Use official name for SSSS
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-11-14 12:10:56 +00:00
David Baker 0048cbef08 Mark cross siging / SSSS APIs as unstable
also add missing jsdoc
2019-11-14 11:51:44 +00:00
David Baker d9d65309b3 More s/cross-signing/crossSigning/ 2019-11-14 11:29:08 +00:00
David Baker d5d8032b5b Camelcase event names
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-11-14 11:04:37 +00:00
David Baker 693c749da0 lint 2019-11-13 17:59:25 +00:00
David Baker 7218e31a9c Sign & verify SSSS keys 2019-11-13 17:52:24 +00:00
David Baker 1798f3921f Make setDeafultKeyId wait for event 2019-11-13 14:42:08 +00:00
David Baker d12c56a623 lint 2019-11-13 14:11:50 +00:00
David Baker 26aa3d3ce7 Support default keys 2019-11-13 14:09:40 +00:00
David Baker c97a87d1f6 Throw if an unknown key is specified
It's probably important that the app knows if a secret isn't going
to be stored under one or more of the keys it thought it was going
to be stored under.

Also add a test to assert it.
2019-11-12 14:07:05 +00:00
David Baker 9bc185d459 Fix what was probablyt a c+p fail 2019-11-12 13:21:37 +00:00
David Baker 4c651c15ea Convert secrets events to callbacks too 2019-11-11 20:01:11 +00:00
David Baker a98e6964ef Missed bits of callback renaming 2019-11-11 16:51:49 +00:00
David Baker 6f8d9c4693 Rename getPrivateKeys to getCrossSigningKeys 2019-11-11 16:45:01 +00:00
David Baker fbc4bd0c96 Merge pull request #1067 from matrix-org/bwindels/verification-over-dm
Support for verification requests in the timeline
2019-11-08 16:19:50 +00:00
J. Ryan Stinnett 03c9241783 Merge pull request #1066 from matrix-org/jryans/privacy-prefix-r0
Use stable API prefix for 3PID APIs when supported
2019-11-07 20:00:57 +02:00
David Baker 3a983271d6 add comments 2019-11-07 16:21:53 +00:00
David Baker 03fe4afe32 lint 2019-11-07 15:20:07 +00:00
David Baker 12627022d1 Convert sas verification test to callbacks 2019-11-07 15:18:16 +00:00
David Baker fabfe16d45 lint 2019-11-07 12:35:39 +00:00
David Baker a34758f938 Convert event interface to callbacks
Use options.cryptoCallbacks for things that require information
from the app rather than events, since events can have zero, one
or many listeners and the emitter doesn't know how many, so if
nobody's listening then we would have just waited forever for a
response.

Also a collection of other changes like renaming 'fu' to 'firstUse'
2019-11-07 12:31:44 +00:00
J. Ryan Stinnett 20f5c3ea28 Use stable API prefix for 3PID APIs when supported
If the server advertises spec version r0.6.0, it must have the 3PID APIs
available under the stable API prefix.

Fixes https://github.com/vector-im/riot-web/issues/11246
2019-11-06 18:02:10 +00:00
Bruno Windels 62e490cfe4 add FIXME note for (expected) uncaught rejection 2019-11-06 12:36:50 +01:00
Bruno Windels a9dba39623 include redacted event so has same signature as other Relations events 2019-11-04 15:56:15 +01:00
Bruno Windels f1d417597c only emit Event.relationsCreated once event has been added
so if we ask for the relations in the handler, we don't get
an empty result
2019-11-04 15:55:29 +01:00
RiotRobot 549f679bf1 Merge branch 'master' into develop 2019-11-04 13:59:24 +00:00
RiotRobot 6ba052dcc4 v2.4.3 2019-11-04 13:51:50 +00:00
RiotRobot de873b84f5 Prepare changelog for v2.4.3 2019-11-04 13:51:49 +00:00
Bruno Windels 37558ac1b4 detect other end cancelling and reject main promise 2019-11-04 14:43:44 +01:00
Bruno Windels 9140d5a091 don't clear expected type before including it in error 2019-11-04 14:37:12 +01:00
J. Ryan Stinnett 7827af0d90 Merge pull request #1063 from matrix-org/jryans/rm-jenkins
Remove Jenkins scripts
2019-11-01 18:23:16 +02:00
J. Ryan Stinnett 1af8d20adf Remove Jenkins scripts
We haven't used Jenkins for a while, so it seems safe to remove the scripts that
supported it.
2019-11-01 16:08:29 +00:00
David Baker f3073e120d Space 2019-11-01 10:51:49 +00:00
David Baker a571624e13 Typo 2019-11-01 10:46:43 +00:00
David Baker 74b649c04c Typo 2019-11-01 10:45:41 +00:00
RiotRobot e8f5a8b89d v2.4.3-rc.1 2019-10-30 16:40:25 +00:00
RiotRobot 2d0bda933c Prepare changelog for v2.4.3-rc.1 2019-10-30 16:40:25 +00:00
David Baker 49588da73d Fix more tests 2019-10-29 19:39:31 +00:00
David Baker 3e2d845342 Merge remote-tracking branch 'origin/develop' into dbkr/cross_signing 2019-10-28 16:47:16 +00:00
David Baker e92d2bd70a Fix test again
That one was part of the protocol - don't camelcase that
2019-10-28 16:07:26 +00:00
David Baker de1b545df1 lint 2019-10-28 15:42:42 +00:00
David Baker 3bec28b2ff make other tests pass 2019-10-28 15:23:58 +00:00
David Baker 8cad116dd7 Make tests pass
* Pass the http backend out of makeTestClients so we can tell it
   to expect queries and flush requests out
 * Change colons to dots in the key events
2019-10-28 14:56:35 +00:00
Hubert Chathi 35adb75d80 Merge pull request #1056 from uhoreg/fix_logger_path
fix the path in references to logger.js
2019-10-26 13:45:24 -04:00
Hubert Chathi e9908b1d97 fix the path in references to logger.js 2019-10-25 23:24:30 -04:00
Hubert Chathi fffd2eb70a Merge pull request #1050 from uhoreg/verification_in_dms
verification in DMs
2019-10-23 12:09:52 -04:00
Hubert Chathi 136b9c0f50 remove unnecessary async 2019-10-23 12:03:17 -04:00
Hubert Chathi 0f1206b4ee apply suggestions from review 2019-10-22 13:29:24 -04:00
RiotRobot 46d7e4c707 Merge branch 'master' into develop 2019-10-18 14:38:58 +01:00
RiotRobot c874783742 v2.4.2 2019-10-18 14:36:57 +01:00
RiotRobot bb296f50d9 Prepare changelog for v2.4.2 2019-10-18 14:36:57 +01:00
Michael Telatynski da68b53ff9 Merge pull request #1054 from rcsm/develop
Properly document the function possible returns for getRelationsForEvent
2019-10-17 15:35:33 +01:00
Rafael Cascalho bbe141d44e chore: updated throws docs in function getRelationsForEvent 2019-10-17 11:30:41 -03:00
Rafael Cascalho 8a03e41a7c chore: corrected the docs the getRelationsForEvent function 2019-10-17 10:51:34 -03:00
Rafael Cascalho a79e1bc976 Merge remote-tracking branch 'upstream/develop' into develop 2019-10-17 10:15:10 -03:00
J. Ryan Stinnett 056bfbf7a3 Merge pull request #1055 from matrix-org/jryans/bluebird-3.5
Downgrade to Bluebird 3.5.5 to fix Firefox
2019-10-17 00:10:54 +02:00
J. Ryan Stinnett e0b64a487d Downgrade to Bluebird 3.5.5 to fix Firefox
Bluebird 3.6.0+ currently breaks in at least Firefox with errors in their
rejection tracking approach. For now, this retreats back to the version we had
used for a long time.

Regressed by https://github.com/matrix-org/matrix-js-sdk/pull/1053
Fixes https://github.com/vector-im/riot-web/issues/11148
2019-10-17 00:05:47 +02:00
Rafael Cascalho d47d1d8f26 Merge remote-tracking branch 'upstream/develop' into develop 2019-10-15 11:12:00 -03:00
Rafael Cascalho 42a07de9a7 chore: corrected getRelationForEvent return docs 2019-10-15 10:28:23 -03:00
J. Ryan Stinnett aead855470 Merge pull request #1053 from matrix-org/jryans/major-deps-2
Upgrade safe deps to latest major version
2019-10-11 13:53:58 +01:00
J. Ryan Stinnett 335b2314f1 Upgrade safe deps to latest major version 2019-10-11 12:44:36 +01:00
Travis Ralston 89bab24c14 Merge pull request #1052 from clokep/no-js-import
Don't include .js in the import string.
2019-10-10 00:14:52 +01:00
Patrick Cloke 3a439dcdad Don't include .js in the import string.
Signed-off-by: Patrick Cloke <clokep@patrick.cloke.us>
2019-10-09 18:44:41 -04:00
RiotRobot 20d82eb92f v2.4.2-rc.1 2019-10-09 16:49:43 +01:00
RiotRobot 319e1d1191 Prepare changelog for v2.4.2-rc.1 2019-10-09 16:49:42 +01:00
Hubert Chathi 5f3492dbf8 send the m.key.verification.done message when done 2019-10-09 11:13:32 -04:00
David Baker 107c8c0b1f Merge pull request #1047 from matrix-org/dbkr/olm_session_describe
Log state of Olm sessions
2019-10-09 15:48:54 +01:00
David Baker 8c6d9586bf Update docs to reflect minimum olm version 2019-10-09 15:41:34 +01:00
David Baker 1271fc6bf3 Actually bump olm 2019-10-09 15:38:47 +01:00
David Baker c9df03c40c Bump to olm 3.1.4 which has olm_session_describe 2019-10-09 10:19:13 +01:00
Hubert Chathi d8e8dddd25 initial implementation of verification in DMs 2019-10-08 15:44:51 -04:00
Bruno Windels 27f6745123 Merge pull request #1048 from matrix-org/bwindels/redact-all-more-robust
Add method to get access to all timelines
2019-10-08 06:58:13 +00:00
Bruno Windels 964f448334 moar doc fix 2019-10-02 15:30:28 +02:00
Bruno Windels 20ee03bb44 fix docs 2019-10-02 15:25:19 +02:00
Bruno Windels 77bd677182 add method to get access to all timelines 2019-10-02 10:00:11 +02:00
RiotRobot e024d047e3 v2.4.1 2019-10-01 11:30:13 +01:00
RiotRobot 40943edc06 Prepare changelog for v2.4.1 2019-10-01 11:30:12 +01:00
David Baker e6699c5424 Log state of Olm sessions
...whenever we encrypt or decrypt a message on them. This adds
another line of logging for every device in the room, so will
be reasonably verbose if you're in large encrypted rooms, but
the information ought to be valuable.

Requires https://gitlab.matrix.org/matrix-org/olm/merge_requests/9

Don't merge before a new version of Olm is released with this merge
request (it won't work).
2019-10-01 11:19:52 +01:00
J. Ryan Stinnett bd8a307e50 Merge pull request #1046 from matrix-org/jryans/deps-2019-09-27
Upgrade deps
2019-09-27 16:01:50 +01:00
J. Ryan Stinnett f71301cafc Upgrade deps 2019-09-27 15:04:21 +01:00
RiotRobot 562bf9331b Merge branch 'master' into develop 2019-09-27 11:44:14 +01:00
RiotRobot 11e6eb94b5 v2.4.0 2019-09-27 11:42:17 +01:00
RiotRobot cee3aa2a7a Prepare changelog for v2.4.0 2019-09-27 11:42:17 +01:00
David Baker 81e3783488 Merge pull request #1043 from matrix-org/dbkr/ignore_empty_crypto_event
Ignore crypto events with no content
2019-09-27 10:44:17 +01:00
J. Ryan Stinnett fc7f9786f8 Merge pull request #1045 from matrix-org/jryans/yarn-cache-clean
Clean Yarn cache during release
2019-09-26 11:37:18 +01:00
J. Ryan Stinnett 0808c0edf1 Clean Yarn cache during release
Always run `yarn cache clean` during the `dist` step to workaround a Yarn bug
with Git commit package dependencies.
2019-09-26 11:18:20 +01:00
RiotRobot 8de6746efd v2.4.0-rc.1 2019-09-25 17:14:17 +01:00
RiotRobot eb9b8ef7c6 Prepare changelog for v2.4.0-rc.1 2019-09-25 17:14:16 +01:00
J. Ryan Stinnett b09621b915 Merge pull request #1044 from matrix-org/jryans/rm-id-server-creds
Remove id_server from creds for interactive auth
2019-09-25 16:02:41 +01:00
J. Ryan Stinnett 8d667f9367 Remove id_server from creds for interactive auth
For HSes that do not require an IS, we can remove `id_server` from the auth
params.

Fixes https://github.com/vector-im/riot-web/issues/10959
2019-09-25 14:59:32 +01:00
David Baker 56dfe6630f Ignore crypto events with no content 2019-09-25 12:05:52 +01:00
J. Ryan Stinnett 8b3b181a48 Merge pull request #1041 from matrix-org/jryans/rm-id-params-request-token
Remove IS details from requestToken to HS
2019-09-23 14:43:03 +01:00
J. Ryan Stinnett c952768542 Remove IS details from requestToken to HS
This removes the IS details (server and access token) from `requestToken` calls
to the HS, as long as the HS supports the new separate add and bind mode. In
this mode, all of the 3PID validation is handled by the HS, so the IS details
are not used.

Fixes https://github.com/vector-im/riot-web/issues/10933
2019-09-23 13:36:07 +01:00
J. Ryan Stinnett 1a368aa996 Merge pull request #1040 from matrix-org/jryans/msisdn-submit-url
Add support for sending MSISDN tokens to alternate URLs
2019-09-23 13:21:28 +01:00
J. Ryan Stinnett 61449458cf Add support for sending MSISDN tokens to alternate URLs
Part of https://github.com/vector-im/riot-web/issues/10923
2019-09-23 12:20:28 +01:00
J. Ryan Stinnett 4eb547e535 Merge pull request #1038 from matrix-org/jryans/msc2290
Add separate 3PID add and bind APIs
2019-09-20 14:37:00 +01:00
J. Ryan Stinnett b54acffaef Tweak unbind 3PID params 2019-09-19 17:28:50 +01:00
J. Ryan Stinnett 65a1833e1f Add 3PID unbind API 2019-09-19 15:28:58 +01:00
J. Ryan Stinnett 1ce4f25811 Use unstable prefix for add and bind 2019-09-19 15:28:35 +01:00
J. Ryan Stinnett 3127105516 Merge pull request #1037 from matrix-org/dependabot/npm_and_yarn/eslint-utils-1.4.2
Bump eslint-utils from 1.4.0 to 1.4.2
2019-09-18 12:52:28 +01:00
dependabot[bot] d59ea4be78 Bump eslint-utils from 1.4.0 to 1.4.2
Bumps [eslint-utils](https://github.com/mysticatea/eslint-utils) from 1.4.0 to 1.4.2.
- [Release notes](https://github.com/mysticatea/eslint-utils/releases)
- [Commits](https://github.com/mysticatea/eslint-utils/compare/v1.4.0...v1.4.2)

Signed-off-by: dependabot[bot] <support@github.com>
2019-09-18 11:48:29 +00:00
J. Ryan Stinnett f256f04440 Add MSISDN validation API on the IS
This API has existed for quite a while, but historically we've instead proxied
this request via the homeserver. As part of MSC2290 work, we are changing
approaches such that we will request tokens directly from the IS when binding
for discovery.

Part of https://github.com/vector-im/riot-web/issues/10839
2019-09-18 11:47:37 +01:00
J. Ryan Stinnett b444aaa67e Add separate add and bind HS APIs
Part of https://github.com/vector-im/riot-web/issues/10839
2019-09-18 11:16:23 +01:00
J. Ryan Stinnett 745185e689 Fix function doc syntax 2019-09-18 10:38:52 +01:00
J. Ryan Stinnett 2bfa891f0a Add function to check for separate 3PID add and bind
This adds a way to test for MSC2290 support on the homeserver with separate add
and bind functions. It checks the unstable feature flag as well as the upcoming
r0.6.0 spec version.

Part of https://github.com/vector-im/riot-web/issues/10839
2019-09-18 10:36:41 +01:00
Travis Ralston 147167bed3 Merge pull request #1036 from matrix-org/travis/wrap-ff
Handle WebRTC security errors as non-fatal
2019-09-17 13:52:19 -06:00
Travis Ralston 565e18e8a3 Handle WebRTC security errors as non-fatal
Fixes https://github.com/vector-im/riot-web/issues/10898

In some restricted modes of Firefox, the WebRTC classes aren't super available: accessing them can cause SecurityErrors to be raised. In these conditions, we should just disable WebRTC support instead of falling apart.
2019-09-17 13:49:50 -06:00
Travis Ralston 55b4595bbf Merge pull request #1035 from matrix-org/travis/r0.6
Check for r0.6.0 support in addition to unstable feature flags
2019-09-16 14:43:00 -06:00
Travis Ralston eeb2c463dc Check for r0.6.0 support in addition to unstable feature flags
To avoid the same problem that happened with lazy-loading (see https://github.com/matrix-org/synapse/issues/5528).

Note that as of writing r0.6.0 is not yet released, however it is the next scheduled release of the client-server API.
2019-09-16 14:30:18 -06:00
RiotRobot d9bb0e9a52 Merge branch 'master' into develop 2019-09-16 17:43:01 +01:00
RiotRobot 7d07ab7c7e v2.3.2 2019-09-16 17:40:19 +01:00
RiotRobot f8ff3aac58 Prepare changelog for v2.3.2 2019-09-16 17:40:19 +01:00
David Baker 299a7728d1 Merge pull request #1034 from matrix-org/travis/t2
[Release] Fix addPendingEvent with pending event order == chronological
2019-09-16 17:02:32 +01:00
David Baker 39dc6a1742 Fix addPendingEvent with pending event order == chronological
When the pending event order setting was set to 'chronological'
(the default) `addPendingEvent` would NPE because Room no longer
has a `this._filter` property. It should get the filter from the
event timeline set instead, as it does in the previous line when
checking or the presence of a filter.

We should strongly consider changing the default pending event order
to 'detached' and probably removing 'chronological' or comitting to
support it properly: it's not really tested and is prone to breakage
like this.

Applies flumpt's fix from https://github.com/matrix-org/matrix-js-sdk/issues/599
Fixes https://github.com/matrix-org/matrix-js-sdk/issues/599
2019-09-16 09:36:14 -06:00
RiotRobot f21c5aa7f2 v2.3.2-rc.1 2019-09-13 16:13:27 +01:00
RiotRobot e9bc3f26a5 Prepare changelog for v2.3.2-rc.1 2019-09-13 16:13:27 +01:00
David Baker 23eaddd6ea Merge pull request #1033 from matrix-org/travis/t1
Synapse admin functions to release
2019-09-13 16:07:58 +01:00
Travis Ralston 8143ce8450 Update src/client.js
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-09-13 09:01:16 -06:00
Travis Ralston 0a487ec43e Add Synapse admin functions for deactivating a user
For https://github.com/matrix-org/matrix-react-sdk/pull/3371
2019-09-13 09:01:05 -06:00
Travis Ralston 0edb483802 Merge pull request #1032 from matrix-org/travis/t1
[To Release] Add matrix base API to report an event
2019-09-13 08:27:31 -06:00
Michael Telatynski 06a32ce0a1 Add matrix base API to report an event
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-09-13 08:24:49 -06:00
RiotRobot 8cae00407a Merge branch 'master' into develop 2019-09-12 12:51:29 +01:00
RiotRobot a57ec87c67 v2.3.1 2019-09-12 12:48:36 +01:00
RiotRobot 4e62491ea4 Prepare changelog for v2.3.1 2019-09-12 12:48:36 +01:00
RiotRobot 5758029c1e v2.3.1-rc.1 2019-09-11 18:38:22 +01:00
RiotRobot 8f08710c58 Prepare changelog for v2.3.1-rc.1 2019-09-11 18:38:21 +01:00
David Baker 90f98105f0 Merge pull request #1031 from matrix-org/dbkr/update_profile_on_redact_2
Update room members on member event redaction
2019-09-11 18:12:46 +01:00
David Baker 90354aa330 Update room members on member event redaction
If a member event was redacted, we weren't updating the current
state.
2019-09-11 18:09:54 +01:00
David Baker aaabebe7f5 Merge pull request #1030 from matrix-org/dbkr/update_profile_on_redact
Update room members on member event redaction
2019-09-11 18:07:49 +01:00
David Baker 80a92dcdc2 Update room members on member event redaction
If a member event was redacted, we weren't updating the current
state.
2019-09-11 16:07:34 +01:00
Travis Ralston dc9081e9d4 Merge pull request #1028 from matrix-org/travis/hidden_rr
Support hidden read receipts
2019-09-10 10:55:58 -06:00
Travis Ralston 3c299637b6 Merge pull request #1029 from matrix-org/travis/lowercase-lookups
Do 3pid lookups in lowercase
2019-09-09 15:19:48 -06:00
Travis Ralston 07af333943 clarify comment 2019-09-09 14:44:51 -06:00
Travis Ralston 0bbc781d0c Do 3pid lookups in lowercase
Fixes https://github.com/vector-im/riot-web/issues/10754
2019-09-07 14:04:30 -06:00
Travis Ralston 79bf64f079 Appease the linter 2019-09-05 20:40:16 -06:00
Travis Ralston ed67d39456 Support hidden read receipts 2019-09-05 19:38:49 -06:00
Travis Ralston 2f8cc75432 Merge pull request #1027 from matrix-org/travis/synapse_admin
Add Synapse admin functions for deactivating a user
2019-09-02 11:08:39 -06:00
Travis Ralston 03cccef805 Update src/client.js
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-09-02 11:06:54 -06:00
Travis Ralston 6d5a0c2718 Add Synapse admin functions for deactivating a user
For https://github.com/matrix-org/matrix-react-sdk/pull/3371
2019-09-01 18:05:12 -06:00
David Baker 42b359eb5c Merge pull request #1026 from matrix-org/dbkr/fix_add_pending_events_chronological
Fix addPendingEvent with pending event order == chronological
2019-08-28 17:02:20 -04:00
David Baker 3071587f11 Fix addPendingEvent with pending event order == chronological
When the pending event order setting was set to 'chronological'
(the default) `addPendingEvent` would NPE because Room no longer
has a `this._filter` property. It should get the filter from the
event timeline set instead, as it does in the previous line when
checking or the presence of a filter.

We should strongly consider changing the default pending event order
to 'detached' and probably removing 'chronological' or comitting to
support it properly: it's not really tested and is prone to breakage
like this.

Applies flumpt's fix from https://github.com/matrix-org/matrix-js-sdk/issues/599
Fixes https://github.com/matrix-org/matrix-js-sdk/issues/599
2019-08-28 16:34:09 -04:00
Hubert Chathi f3ec9768bc update to follow latest MSC 2019-08-27 16:53:36 -07:00
Travis Ralston 23159807b0 Merge pull request #1024 from matrix-org/travis/wk-raw
Add AutoDiscovery.getRawClientConfig() for easy .well-known lookups
2019-08-27 08:22:55 -06:00
Travis Ralston b1ba9f76b8 Merge pull request #1025 from matrix-org/travis/fix-identity-json-error
Don't convert errors to JSON if they are JSON already
2019-08-23 12:14:58 -06:00
Travis Ralston 0e51dfed46 Don't convert errors to JSON if they are JSON already
For example, if the identity server throws a 401 on `/account`, we end up here with a JSON object. Don't convert the string `object Object` to JSON because it'll fail: just use the object.
2019-08-23 12:00:07 -06:00
J. Ryan Stinnett 09b00335f8 Merge pull request #1022 from matrix-org/jryans/is-token-to-hs
Send id_access_token to HS for use in proxied IS requests
2019-08-23 17:13:41 +01:00
J. Ryan Stinnett 3d274815d9 Change to provider object 2019-08-23 11:17:39 +01:00
Travis Ralston 70d60b905d Add AutoDiscovery.getRawClientConfig() for easy .well-known lookups
Useful in cases where you don't need to validate the homeserver information.
2019-08-22 14:45:42 -06:00
Travis Ralston 3e2ffb25a6 Merge pull request #1023 from matrix-org/travis/cleanup-identity-http
Clean up JSON handling in identity server requests
2019-08-22 10:55:08 -06:00
Travis Ralston 8b9bef5cb3 Clean up JSON handling in identity server requests 2019-08-22 08:32:20 -06:00
J. Ryan Stinnett 31e72efc91 Send id_access_token to HS for use in proxied IS requests
This passes along the `id_access_token` to the HS, which it will need when
speaking v2 IS APIs to the IS.

Unfortunately, some HSes seem to explode when given this new parameter, so we
only pass it along for the moment if an unstable feature `m.id_access_token` is
also set.

Part of https://github.com/vector-im/riot-web/issues/10525
Defined in MSC2140
2019-08-22 14:31:41 +01:00
Travis Ralston 60b7252597 Merge pull request #1021 from matrix-org/travis/v2-identity-lookups
Use the v2 (hashed) lookup for identity server queries
2019-08-22 07:05:16 -06:00
Travis Ralston 3980b62df2 js-doc syntax is unknown to our js-doc 2019-08-21 14:32:38 -06:00
Travis Ralston b306df726a Lookups are URL safe 2019-08-21 14:30:24 -06:00
Travis Ralston 3d5a79be3b Hashes need tokens too 2019-08-21 14:30:16 -06:00
Travis Ralston ba78d1a9ae JSON is JSON 2019-08-21 14:30:04 -06:00
Travis Ralston 241811298f Appease the js-doc 2019-08-21 14:17:55 -06:00
Travis Ralston 8a0ddc43ab Use the v2 (hashed) lookup for identity server queries
Fixes https://github.com/vector-im/riot-web/issues/10556
Implements [MSC2134](https://github.com/matrix-org/matrix-doc/pull/2134) with assistance from [MSC2140](https://github.com/matrix-org/matrix-doc/pull/2140) for auth.

Note: this also changes all identity server requests to use JSON as a request body. URL encoded forms were allowed in v1 but deprecated in favour of JSON. v2 APIs do not allow URL encoded forms.
2019-08-21 14:08:46 -06:00
David Baker 898fa0e41b Merge pull request #1018 from matrix-org/dbkr/getversions
Add getIdServer() & doesServerRequireIdServerParam()
2019-08-19 11:23:53 +01:00
David Baker 081ff4dec0 Merge pull request #1019 from matrix-org/dbkr/requesttoken_optional_id_server
Make requestToken endpoints work without ID Server
2019-08-19 11:23:05 +01:00
David Baker 3c69b8511d cache should expire TODO 2019-08-19 11:21:32 +01:00
David Baker 6843d86ecf Truthiness not null check
Co-Authored-By: Travis Ralston <travpc@gmail.com>
2019-08-16 19:41:37 +01:00
David Baker 2e91200136 lint
also WE FIXED SOME WARNINGS! Go us.
2019-08-16 18:13:42 +01:00
David Baker 852304c417 Make requestToken endpoints work without ID Server
Hopefully with doc in appropriate functions
2019-08-16 18:09:29 +01:00
David Baker ee752e3885 Add getIdServer() & doesServerRequireIdServerParam()
Remove individual cache for lazy loading and just cache the whole
versions response, then we can cache both of these flags
2019-08-16 15:02:49 +01:00
David Baker b9480e4302 Merge pull request #1016 from matrix-org/dbkr/fix_setidentityserver
Fix setIdentityServer
2019-08-15 15:11:54 +01:00
David Baker 2ae4d07971 Fix setIdentityServer 2019-08-15 12:01:40 +01:00
J. Ryan Stinnett 90cac8a118 Merge pull request #1015 from matrix-org/jryans/stun-turn-fallback
Change ICE fallback server and make fallback opt-in
2019-08-15 10:50:56 +01:00
J. Ryan Stinnett db18274f6e Revert no TURN servers event
The intended flow has changed so we prompt about this case in context of making
a call, so this event is no longer needed.
2019-08-14 14:09:05 +01:00
J. Ryan Stinnett 172bad8b55 Support configuring ICE fallback at client init time
This adds a new client init option to configure whether an ICE fallback server
is allowed to be used.
2019-08-14 13:44:50 +01:00
J. Ryan Stinnett dfe454e18f Change ICE server fallback to opt-in
This changes the ICE server fallback to be disabled by default. The SDK consumer
will receive a new event in case the homeserver has no ICE servers of its own,
and can prompt the user to agree to the fallback if desired.

Part of https://github.com/vector-im/riot-web/issues/10173
2019-08-13 15:55:31 +01:00
J. Ryan Stinnett 3d8dd29b4c Change STUN fallback to turn.matrix.org
This changes the STUN fallback server from a Google server to one hosted at
`turn.matrix.org`.

Part of https://github.com/vector-im/riot-web/issues/10173
2019-08-13 13:31:43 +01:00
J. Ryan Stinnett c3ff213ec9 Improve WebRTC object logging 2019-08-13 13:29:59 +01:00
J. Ryan Stinnett e80e5e1f8c Firefox supports urls on RTCIceServer
There's no longer a need to translate ICE server objects for Firefox, as it
supports the `urls` array since version 37.
2019-08-13 13:15:56 +01:00
David Baker bba249d5ce Merge pull request #1014 from matrix-org/dbkr/throw_if_no_id_url
Throw an exception if trying to do an ID server request with no ID server
2019-08-13 10:41:04 +01:00
David Baker f57df2bee5 Catch no ID server on other method 2019-08-12 16:06:37 +01:00
David Baker b930638156 Be more verbose
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-08-12 16:00:40 +01:00
David Baker 39c1de19fc Throw an exception if trying to do an ID server request with no ID server
Will help with https://github.com/vector-im/riot-web/issues/10540
2019-08-12 15:39:50 +01:00
David Baker 17724fc8d3 Merge pull request #1013 from matrix-org/dbkr/set_is
Add setIdentityServerUrl
2019-08-09 18:11:45 +01:00
David Baker 4c6d11d9ed Add setIdentityServerUrl
Hopefully fairly self explanatory. The ID server URL can be changed
fairly readily, whereas the HS URL would require a different access
token etc.
2019-08-09 18:05:37 +01:00
Michael Telatynski 05d77a85c9 Merge pull request #1011 from matrix-org/t3chguy/add-rooms-roomid-report-eventid
Add matrix base API to report an event
2019-08-08 15:21:20 +01:00
J. Ryan Stinnett e95a133cdd Merge pull request #1010 from matrix-org/jryans/user-settings-toggle-3pid
Fix POST body for v2 IS requests
2019-08-08 15:17:53 +01:00
Michael Telatynski c21382d721 Add matrix base API to report an event
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-08-08 15:16:11 +01:00
J. Ryan Stinnett 8c15125e23 Fix POST body for v2 IS requests
POST bodies for v2 IS requests must be sent as JSON.

Part of https://github.com/vector-im/riot-web/issues/10159
2019-08-08 10:29:14 +01:00
J. Ryan Stinnett 64ddbd97dd Merge pull request #1009 from matrix-org/jryans/user-settings-toggle-3pid
Add API for bulk lookup on the Identity Server
2019-08-07 18:05:58 +01:00
J. Ryan Stinnett 9c24bcb7a9 Tweak the type 2019-08-07 18:05:32 +01:00
J. Ryan Stinnett 8f016726f0 Add API for bulk lookup on the Identity Server
This adds support for querying `/bulk_lookup` on the IS to check several 3PIDs
at the same time.

Part of https://github.com/vector-im/riot-web/issues/10159
2019-08-07 17:45:13 +01:00
J. Ryan Stinnett 649fe7a490 Merge pull request #1000 from matrix-org/t3chguy/remove_authedRequestWithPrefix
Remove deprecated authedRequestWithPrefix and requestWithPrefix
2019-08-06 18:58:24 +01:00
RiotRobot 35f1cdf89c Merge branch 'master' into develop 2019-08-05 11:48:35 +01:00
RiotRobot 06adc34fb3 v2.3.0 2019-08-05 11:46:46 +01:00
RiotRobot 87bf07f95e Prepare changelog for v2.3.0 2019-08-05 11:46:46 +01:00
J. Ryan Stinnett f05bf3f845 Merge pull request #1007 from matrix-org/jryans/is-account-info
Add API for checking IS account info
2019-08-01 17:28:08 +01:00
Travis Ralston ab512d087c Merge pull request #1008 from matrix-org/travis/tombstone-push-rel
[release] Support rewriting push rules when our internal defaults change
2019-08-01 08:30:01 -06:00
Travis Ralston 6799c29921 Appease the tests 2019-08-01 08:23:19 -06:00
Travis Ralston a3f1da1981 Appease the linter 2019-08-01 08:23:19 -06:00
Travis Ralston 3b225651cc Support rewriting push rules when our internal defaults change
and change our internal default for tombstones
2019-08-01 08:23:19 -06:00
Travis Ralston a40d691159 Merge pull request #1006 from matrix-org/travis/tombstone-push
Support rewriting push rules when our internal defaults change
2019-08-01 08:22:47 -06:00
J. Ryan Stinnett 4ebe60b2ad Add API for checking IS account info
Part of https://github.com/vector-im/riot-web/issues/10452
2019-08-01 12:02:56 +01:00
J. Ryan Stinnett 5a70859593 Merge pull request #1005 from matrix-org/jryans/upgrade-deps-2019-07-31
Upgrade dependencies
2019-07-31 18:51:14 +01:00
Travis Ralston c7be810e65 Appease the tests 2019-07-31 11:00:44 -06:00
Travis Ralston 101217cfb6 Appease the linter 2019-07-31 11:00:38 -06:00
Travis Ralston 5c2aa4677f Support rewriting push rules when our internal defaults change
and change our internal default for tombstones
2019-07-31 10:52:44 -06:00
J. Ryan Stinnett ab9bfa68ae Upgrade dependencies 2019-07-31 17:36:35 +01:00
RiotRobot aa8c2ca277 v2.3.0-rc.1 2019-07-31 16:20:54 +01:00
RiotRobot 84509087ac Prepare changelog for v2.3.0-rc.1 2019-07-31 16:20:54 +01:00
J. Ryan Stinnett 2450d461fd Merge pull request #1002 from matrix-org/jryans/is-v2-auth
Add support for IS v2 API with authentication
2019-07-30 18:13:08 +01:00
J. Ryan Stinnett 50c590ae26 Note cleanup issue 2019-07-30 10:38:53 +01:00
J. Ryan Stinnett 516dff06ee Rename isAccessToken to identityAccessToken 2019-07-30 10:06:52 +01:00
Travis Ralston 9a8af05bfb Merge pull request #1001 from matrix-org/hs/recursive-tombstone-fixes
Tombstone bugfixes
2019-07-29 08:52:59 -06:00
Will Hunt c9bf61c387 Simplify Set 2019-07-29 15:29:18 +01:00
Will Hunt 4f0f2e8c16 Fix issues with recursive tombstones 2019-07-29 15:27:32 +01:00
J. Ryan Stinnett 6f042a2142 Add IS v2 support to other IS APIs
This adds v2 support with fallback to other IS APIs in the SDK.
2019-07-29 14:55:40 +01:00
J. Ryan Stinnett 91416bdbb2 Add IS v1 API fallback for lookup 2019-07-29 14:44:15 +01:00
J. Ryan Stinnett 9b093f7569 Add first pass of IS v2 API with authentication
This only updates the `/lookup` API so far. It also doesn't handle falling back
to v1.
2019-07-29 13:15:19 +01:00
Michael Telatynski b004d1602d Remove deprecated authedRequestWithPrefix and requestWithPrefix
replacing as documented with authedRequest

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
2019-07-29 12:25:38 +01:00
David Baker 6cca73b999 Merge pull request #988 from matrix-org/dbkr/terms
Support for MSC2140 (terms of service for IS/IM)
2019-07-23 10:32:05 +01:00
David Baker fafd6df13e Use standard _matrix path for terms endpoints 2019-07-22 18:53:36 +01:00
RiotRobot 8f77870526 Merge branch 'master' into develop 2019-07-18 15:44:16 +01:00
RiotRobot eb0462e89b v2.2.0 2019-07-18 15:42:21 +01:00
RiotRobot 80748d7d85 Prepare changelog for v2.2.0 2019-07-18 15:42:20 +01:00
David Baker b694d53b73 Revert 8004e82c50
We need to switch the paths over all at once, so we can't commit
this yet: leave it until scalar suypports the new API then we can
update develop to use the _matrix paths.

(Also for some reason I broke the IS path too)
2019-07-15 14:26:16 +01:00
David Baker 8004e82c50 Use _matrix prefix for terms API 2019-07-15 13:51:39 +01:00
J. Ryan Stinnett 85b5849228 Upgrade lodash 2019-07-12 19:25:47 +01:00
J. Ryan Stinnett 6b86777e96 Upgrade lodash 2019-07-12 19:25:21 +01:00
RiotRobot efe64a4817 v2.2.0-rc.2 2019-07-12 17:25:20 +01:00
RiotRobot ad777f36b2 Prepare changelog for v2.2.0-rc.2 2019-07-12 17:25:20 +01:00
J. Ryan Stinnett de77ad867c Merge pull request #995 from matrix-org/jryans/v2.2.0/devices
Add a request method to /devices
2019-07-12 16:47:06 +01:00
Travis Ralston 9b35f86497 Add a request method to /devices
Turns out `HTTP /devices undefined` is invalid.

Regressed in https://github.com/matrix-org/matrix-js-sdk/pull/990
2019-07-12 16:38:43 +01:00
Travis Ralston 84fc8b1931 Merge pull request #994 from matrix-org/travis/fix-devices
Add a request method to /devices
2019-07-12 08:12:59 -06:00
RiotRobot 455c85fb69 v2.2.0-rc.1 2019-07-12 11:12:26 +01:00
RiotRobot 2a2fed695b Prepare changelog for v2.2.0-rc.1 2019-07-12 11:12:25 +01:00
Travis Ralston c1f28bd410 Add a request method to /devices
Turns out `HTTP /devices undefined` is invalid.

Regressed in https://github.com/matrix-org/matrix-js-sdk/pull/990
2019-07-11 14:35:04 -06:00
David Baker c74e0bb6b3 tell people what an IS/IM are 2019-07-11 16:29:27 +01:00
David Baker 5b9e158035 unused param
getTerms is un-authed so doesn't need the access token
2019-07-11 16:28:41 +01:00
Travis Ralston a8d200dd02 Merge pull request #993 from matrix-org/travis/sas-is-done
End the verification timer when verification is done
2019-07-11 08:20:30 -06:00
Travis Ralston 34ae967cb8 Merge pull request #990 from matrix-org/travis/stably-use-stable-apis
Stabilize usage of stably stable APIs (in a stable way)
2019-07-11 08:20:14 -06:00
RiotRobot a67f14825e Merge branch 'master' into develop 2019-07-11 10:36:44 +01:00
Bruno Windels 83b74070aa Merge pull request #987 from matrix-org/bwindels/include-orig-in-history
Expose original_event for /relations
2019-07-11 08:09:44 +00:00
Travis Ralston f80af68686 End the verification timer when verification is done
Fixes https://github.com/matrix-org/matrix-js-sdk/issues/980

This also improves cleanliness in the tests to cancel/terminate timers when needed.
2019-07-10 14:51:12 -06:00
Travis Ralston fe4ac06f43 Use the correct media endpoints 2019-07-10 13:24:11 -06:00
Travis Ralston eaaa3e980a Use unstable prefix for key backup 2019-07-10 13:17:31 -06:00
Travis Ralston 07629bfb9a unstable -> stable 2019-07-10 13:11:46 -06:00
Travis Ralston 88fdeca2bf Merge pull request #989 from matrix-org/travis/edu-timeline
Process ephemeral events outside timeline handling
2019-07-10 10:38:02 -06:00
Travis Ralston a3c8eac38b Process ephemeral events outside timeline handling 2019-07-10 10:26:21 -06:00
Bruno Windels bd5380c0b4 Merge pull request #986 from matrix-org/bwindels/include-ssa-for-replace
Don't accept any locally known edits earlier than the last known server-side aggregated edit
2019-07-10 14:38:26 +00:00
David Baker c3b5767999 update comment to reflect right version 2019-07-10 12:19:12 +01:00
David Baker 9e5c2732c9 consistent spacing 2019-07-10 12:18:41 +01:00
David Baker b8957fa917 omit null params 2019-07-10 12:17:52 +01:00
David Baker 52c139dcdc Forgot /terms for ISes
and IMs shouldn't have a slash
2019-07-10 12:15:31 +01:00
David Baker 39d4bf1494 SERVICE_TYPES 2019-07-10 12:08:13 +01:00
David Baker 4c713e3387 s/servicetypes/service-types/ 2019-07-10 11:53:59 +01:00
David Baker bb486f5148 SERVICE_TYPES
Co-Authored-By: J. Ryan Stinnett <jryans@gmail.com>
2019-07-10 11:47:36 +01:00
David Baker 524fea1297 lint 2019-07-10 10:43:54 +01:00
David Baker e9528ebb98 Support for MSC2140 (terms of service for IS/IM) 2019-07-09 18:50:01 +01:00
Bruno Windels de18283c3b map, decrypt and return original_event if present 2019-07-09 17:52:58 +02:00
Bruno Windels cc1c7561a3 Merge pull request #984 from matrix-org/bwindels/replace-server-date
Get edit date transparently from server aggregations or local echo
2019-07-09 15:03:02 +00:00
Bruno Windels 5d928f07a0 don't accept any edit earlier than the server-side set edit. 2019-07-09 15:06:07 +02:00
Bruno Windels 01b882480f method to get edit date transparently from server aggregations or local echo 2019-07-09 11:29:19 +02:00
Travis Ralston 21d52fdbdd Merge pull request #982 from matrix-org/travis/soft-logout-keys
Add a function to flag keys for backup without scheduling a backup
2019-07-08 11:48:09 -06:00
Hubert Chathi 7f8b9de560 offer to upgrade device verifications to cross-signing 2019-07-08 12:26:00 -04:00
Hubert Chathi 761f22b63d minor cleanups 2019-07-08 12:25:28 -04:00
RiotRobot f9baff2a3a Merge branch 'master' into develop 2019-07-08 10:46:33 +01:00
Travis Ralston 3a0f27fa7e Add a function to flag keys for backup without scheduling a backup
For https://github.com/vector-im/riot-web/issues/10263

Starting/scheduling the backup won't help us because the token would be invalid from a server perspective. Instead, we should update what needs to be done and return a count.
2019-07-05 13:50:11 -06:00
J. Ryan Stinnett 60e339bac0 Merge pull request #981 from matrix-org/jryans/reactions-send-marks-unread
Block read marker and read receipt from advancing into pending events
2019-07-05 17:55:52 +01:00
J. Ryan Stinnett ecb88f45b7 Merge pull request #977 from matrix-org/jryans/upgrade-deps
Upgrade dependencies
2019-07-05 17:50:10 +01:00
J. Ryan Stinnett 24a869d15b Update copyright header 2019-07-05 15:07:56 +01:00
J. Ryan Stinnett 1d427a1ea8 Block read marker and read receipt from advancing into pending events
This changes the methods that update the read marker and read receipts to
prevent advancing into pending events.

Part of https://github.com/vector-im/riot-web/issues/9952
2019-07-05 13:59:04 +01:00
J. Ryan Stinnett 90e25867ad Merge pull request #976 from matrix-org/jryans/push-rule-reactions
Add default push rule to ignore reactions
2019-07-05 12:01:31 +01:00
David Baker 2ae56e61cb Merge pull request #979 from matrix-org/dbkr/fix_sync_exception
Fix exception whilst syncing
2019-07-05 11:08:00 +01:00
David Baker 56c0830328 Lint 2019-07-05 10:32:33 +01:00
David Baker 093f139d34 Fix exception whilst syncing
event.getPushRules() may return null (for better or worse...).
Use client.getPushRulesForEvent which will calculate them if they
haven't already been calculated.

Fixes https://github.com/vector-im/riot-web/issues/10269
2019-07-05 10:26:47 +01:00
Travis Ralston 1083efc212 Merge pull request #975 from matrix-org/travis/soft-logout-base
Include the error object when raising Session.logged_out
2019-07-04 09:57:22 -06:00
J. Ryan Stinnett 69773c2619 Upgrade dependencies 2019-07-04 15:04:34 +01:00
J. Ryan Stinnett 2525b5a5d8 Add default push rule to ignore reactions
This adds a default push rule to ignore reactions as proposed in
[MSC2153](https://github.com/matrix-org/matrix-doc/pull/2153). By adding it here
in the client directly, we can try out the idea early even if it hasn't appeared
in the user's HS yet.

Part of https://github.com/vector-im/riot-web/issues/10208
2019-07-04 14:41:04 +01:00
Hubert Chathi b00804102d obsolete todo 2019-07-03 21:37:18 -04:00
Hubert Chathi 8d1d657c44 add unit test for backups signed by cross-signing key 2019-07-03 19:16:26 -04:00
Travis Ralston ff9c84ff94 Fix tests 2019-07-03 16:50:24 -06:00
Travis Ralston 3aa2bf8a76 Include the error object when raising Session.logged_out
Note: The `call` argument previously defined in the SDK was never actually populated, and appears to be a documentation error when the definition was copied from `Call.incoming` directly above it.
2019-07-03 16:42:33 -06:00
Hubert Chathi 6cd09c6af2 pksign was moved to olmlib 2019-07-03 16:00:44 -04:00
Hubert Chathi 46a8486245 rename m.secrets.share to m.secrets.send to agree with latest MSC 2019-07-03 15:15:56 -04:00
Hubert Chathi c5caf8f8f4 sign backups with master key 2019-07-03 15:15:41 -04:00
Hubert Chathi 4356603665 save public part of cross-signing keys 2019-06-27 23:37:57 -04:00
Hubert Chathi 1cae5e8b97 fix unit tests to match event name changes 2019-06-27 23:33:07 -04:00
Hubert Chathi 07c2e34d87 Merge branch 'develop' into dbkr/cross_signing 2019-06-14 22:57:02 -04:00
Hubert Chathi 5bcbe76f2c cleanups and a lot more docs 2019-06-14 22:50:29 -04:00
Hubert Chathi 4c6fa89053 various cross-signing fixes and improvements 2019-06-12 11:47:12 -04:00
Hubert Chathi 98815ffdf6 allow http request stub to ignore unhandled syncs 2019-06-12 11:41:26 -04:00
Hubert Chathi 6f6e7ea921 verify cross-signing key with SAS 2019-06-05 15:27:31 -04:00
Hubert Chathi 0c714ba4a1 some cleanups 2019-06-05 15:24:03 -04:00
Hubert Chathi 5f539aacd9 Merge branch 'develop' into dbkr/cross_signing 2019-06-05 15:21:17 -04:00
Hubert Chathi 6a77df7b41 Merge branch 'develop' into dbkr/cross_signing 2019-06-05 12:48:17 -04:00
Hubert Chathi 4a9a1b40e9 initial implementation of secret storage and sharing 2019-06-04 15:04:45 -04:00
Hubert Chathi dc971b9a59 add missing semicolon 2019-06-04 14:58:46 -04:00
Hubert Chathi 95131c7658 add test for syncing trust on another user 2019-05-29 17:01:25 -04:00
Hubert Chathi 936eef194a minor fixes to tests 2019-05-29 17:01:13 -04:00
Hubert Chathi 941d871daf fix check for empty cross-signing repsonse 2019-05-29 16:59:51 -04:00
Hubert Chathi 609ee663fa use the right path for logger 2019-05-29 16:58:49 -04:00
Hubert Chathi 53804cac5c save cross-signing keys from sync and verify new keys for user 2019-05-28 22:28:54 -04:00
Hubert Chathi 193ad9e09d use 3 keys for cross-signing 2019-05-23 18:18:21 -04:00
Hubert Chathi 405451d783 complete some more unit tests 2019-05-03 23:23:08 -04:00
Hubert Chathi b0275afac2 remove some debugging lines 2019-05-03 23:22:51 -04:00
Hubert Chathi ae71f41138 add missing files 2019-05-03 18:12:17 -04:00
Hubert Chathi ec2f07e1aa add methods for signing and checking users and devices with cross-signing 2019-05-03 18:05:36 -04:00
Hubert Chathi 32814d1833 Merge branch 'develop' into dbkr/cross_signing 2019-04-03 19:28:51 -04:00
David Baker e54f71718f Olm pre2 for cross-signing 2019-02-05 13:41:14 +00:00
David Baker 7f5584e4f5 All the linting 2019-02-05 13:03:27 +00:00
David Baker b3513dc8f8 Make linting rules more consistent
* Put back babel-eslint for class-properties
 * Allow arrow functions without params

This makes the style more consistent with react-sdk.

NB. The line lengths are still inconsistent but it's not clear which
way to go on that yet.
2019-02-05 11:56:08 +00:00
David Baker 1b82dffcb4 Merge remote-tracking branch 'origin/develop' into dbkr/cross_signing 2019-02-01 22:40:14 +00:00
David Baker 5500f0d794 Re-track own device list
Sp we don't stop tracking our own
2019-02-01 22:39:12 +00:00
David Baker c8082535de Always track your own devices
This was causing all the cross-signing stuff to fail and was almost
certainly the cause of https://github.com/vector-im/riot-web/issues/8213
2019-02-01 19:19:00 +00:00
David Baker 7dedcb82b2 Lint
or at least the rules that are consistent with the rest of our
codebase
2019-02-01 18:12:27 +00:00
David Baker 7195365188 Update package-lock.json
because Travis and npm now have a thing where they combust if your
package-lock is out of sync
2019-02-01 15:59:53 +00:00
David Baker 910d0ec9c1 Sign & trust the key backup from the SSK 2019-02-01 15:49:20 +00:00
David Baker 1d58a64ee1 Track SSKs for users
and verify our own against our locally stored private part
2019-02-01 13:04:21 +00:00
David Baker 1f77cc6d1a Cross sign the current device with the SSK
whenever we get the SSK, ie. when creating or restoring a backup
2019-01-31 21:13:01 +00:00
David Baker 02d4dcb128 Store SSK & USK in crypto store
and restore them from the key backup.

NB. This has an interface change to restoreKeyBackup where I've
changed it to take a backupInfo rather than a version (this also
saves us re-fetching the backup metadata in the case of a passphrase
restore).
2019-01-31 15:48:05 +00:00
David Baker 2b54f442d1 Add cross signing key creation into key backup
Start of cross-signing impl
2019-01-30 18:10:40 +00:00
61 changed files with 6439 additions and 1109 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"presets": ["es2015"],
"presets": ["es2015", "es2016"],
"plugins": [
"transform-class-properties",
-1
View File
@@ -10,7 +10,6 @@ build/Release
coverage
lib-cov
out
reports
/dist
/lib
/specbuild
+249
View File
@@ -1,3 +1,252 @@
Changes in [2.4.5](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.5) (2019-11-27)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.4...v2.4.5)
* Relax identity server discovery checks to FAIL_PROMPT
* Expand E2EE debug logging to diagnose "unable to decrypt" errors
Changes in [2.4.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.4) (2019-11-25)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.4-rc.1...v2.4.4)
* No changes since rc.1
Changes in [2.4.4-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.4-rc.1) (2019-11-20)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.3...v2.4.4-rc.1)
* Fix SAS verification in encrypted DMs
[\#1077](https://github.com/matrix-org/matrix-js-sdk/pull/1077)
* Cross-signing / secret storage tweaks
[\#1078](https://github.com/matrix-org/matrix-js-sdk/pull/1078)
* Fix local trust for key backups
[\#1075](https://github.com/matrix-org/matrix-js-sdk/pull/1075)
* Add method to get last active timestamp in room
[\#1072](https://github.com/matrix-org/matrix-js-sdk/pull/1072)
* Check the right Synapse endpoint for determining admin capabilities
[\#1071](https://github.com/matrix-org/matrix-js-sdk/pull/1071)
* Cross Signing Support
[\#832](https://github.com/matrix-org/matrix-js-sdk/pull/832)
* Don't double cancel verification request
[\#1064](https://github.com/matrix-org/matrix-js-sdk/pull/1064)
* Support for verification requests in the timeline
[\#1067](https://github.com/matrix-org/matrix-js-sdk/pull/1067)
* Use stable API prefix for 3PID APIs when supported
[\#1066](https://github.com/matrix-org/matrix-js-sdk/pull/1066)
* Remove Jenkins scripts
[\#1063](https://github.com/matrix-org/matrix-js-sdk/pull/1063)
Changes in [2.4.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.3) (2019-11-04)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.3-rc.1...v2.4.3)
* No changes since rc.1
Changes in [2.4.3-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.3-rc.1) (2019-10-30)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.2...v2.4.3-rc.1)
* fix the path in references to logger.js
[\#1056](https://github.com/matrix-org/matrix-js-sdk/pull/1056)
* verification in DMs
[\#1050](https://github.com/matrix-org/matrix-js-sdk/pull/1050)
* Properly documented the function possible returns
[\#1054](https://github.com/matrix-org/matrix-js-sdk/pull/1054)
* Downgrade to Bluebird 3.5.5 to fix Firefox
[\#1055](https://github.com/matrix-org/matrix-js-sdk/pull/1055)
* Upgrade safe deps to latest major version
[\#1053](https://github.com/matrix-org/matrix-js-sdk/pull/1053)
* Don't include .js in the import string.
[\#1052](https://github.com/matrix-org/matrix-js-sdk/pull/1052)
Changes in [2.4.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.2) (2019-10-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.2-rc.1...v2.4.2)
* No changes since v2.4.2-rc.1
Changes in [2.4.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.2-rc.1) (2019-10-09)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.1...v2.4.2-rc.1)
* Log state of Olm sessions
[\#1047](https://github.com/matrix-org/matrix-js-sdk/pull/1047)
* Add method to get access to all timelines
[\#1048](https://github.com/matrix-org/matrix-js-sdk/pull/1048)
Changes in [2.4.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.1) (2019-10-01)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.0...v2.4.1)
* Upgrade deps
[\#1046](https://github.com/matrix-org/matrix-js-sdk/pull/1046)
* Ignore crypto events with no content
[\#1043](https://github.com/matrix-org/matrix-js-sdk/pull/1043)
Changes in [2.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.0) (2019-09-27)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.0-rc.1...v2.4.0)
* Clean Yarn cache during release
[\#1045](https://github.com/matrix-org/matrix-js-sdk/pull/1045)
Changes in [2.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.0-rc.1) (2019-09-25)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.2...v2.4.0-rc.1)
* Remove id_server from creds for interactive auth
[\#1044](https://github.com/matrix-org/matrix-js-sdk/pull/1044)
* Remove IS details from requestToken to HS
[\#1041](https://github.com/matrix-org/matrix-js-sdk/pull/1041)
* Add support for sending MSISDN tokens to alternate URLs
[\#1040](https://github.com/matrix-org/matrix-js-sdk/pull/1040)
* Add separate 3PID add and bind APIs
[\#1038](https://github.com/matrix-org/matrix-js-sdk/pull/1038)
* Bump eslint-utils from 1.4.0 to 1.4.2
[\#1037](https://github.com/matrix-org/matrix-js-sdk/pull/1037)
* Handle WebRTC security errors as non-fatal
[\#1036](https://github.com/matrix-org/matrix-js-sdk/pull/1036)
* Check for r0.6.0 support in addition to unstable feature flags
[\#1035](https://github.com/matrix-org/matrix-js-sdk/pull/1035)
* Update room members on member event redaction
[\#1030](https://github.com/matrix-org/matrix-js-sdk/pull/1030)
* Support hidden read receipts
[\#1028](https://github.com/matrix-org/matrix-js-sdk/pull/1028)
* Do 3pid lookups in lowercase
[\#1029](https://github.com/matrix-org/matrix-js-sdk/pull/1029)
* Add Synapse admin functions for deactivating a user
[\#1027](https://github.com/matrix-org/matrix-js-sdk/pull/1027)
* Fix addPendingEvent with pending event order == chronological
[\#1026](https://github.com/matrix-org/matrix-js-sdk/pull/1026)
* Add AutoDiscovery.getRawClientConfig() for easy .well-known lookups
[\#1024](https://github.com/matrix-org/matrix-js-sdk/pull/1024)
* Don't convert errors to JSON if they are JSON already
[\#1025](https://github.com/matrix-org/matrix-js-sdk/pull/1025)
* Send id_access_token to HS for use in proxied IS requests
[\#1022](https://github.com/matrix-org/matrix-js-sdk/pull/1022)
* Clean up JSON handling in identity server requests
[\#1023](https://github.com/matrix-org/matrix-js-sdk/pull/1023)
* Use the v2 (hashed) lookup for identity server queries
[\#1021](https://github.com/matrix-org/matrix-js-sdk/pull/1021)
* Add getIdServer() & doesServerRequireIdServerParam()
[\#1018](https://github.com/matrix-org/matrix-js-sdk/pull/1018)
* Make requestToken endpoints work without ID Server
[\#1019](https://github.com/matrix-org/matrix-js-sdk/pull/1019)
* Fix setIdentityServer
[\#1016](https://github.com/matrix-org/matrix-js-sdk/pull/1016)
* Change ICE fallback server and make fallback opt-in
[\#1015](https://github.com/matrix-org/matrix-js-sdk/pull/1015)
* Throw an exception if trying to do an ID server request with no ID server
[\#1014](https://github.com/matrix-org/matrix-js-sdk/pull/1014)
* Add setIdentityServerUrl
[\#1013](https://github.com/matrix-org/matrix-js-sdk/pull/1013)
* Add matrix base API to report an event
[\#1011](https://github.com/matrix-org/matrix-js-sdk/pull/1011)
* Fix POST body for v2 IS requests
[\#1010](https://github.com/matrix-org/matrix-js-sdk/pull/1010)
* Add API for bulk lookup on the Identity Server
[\#1009](https://github.com/matrix-org/matrix-js-sdk/pull/1009)
* Remove deprecated authedRequestWithPrefix and requestWithPrefix
[\#1000](https://github.com/matrix-org/matrix-js-sdk/pull/1000)
* Add API for checking IS account info
[\#1007](https://github.com/matrix-org/matrix-js-sdk/pull/1007)
* Support rewriting push rules when our internal defaults change
[\#1006](https://github.com/matrix-org/matrix-js-sdk/pull/1006)
* Upgrade dependencies
[\#1005](https://github.com/matrix-org/matrix-js-sdk/pull/1005)
Changes in [2.3.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.2) (2019-09-16)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.2-rc.1...v2.3.2)
* [Release] Fix addPendingEvent with pending event order == chronological
[\#1034](https://github.com/matrix-org/matrix-js-sdk/pull/1034)
Changes in [2.3.2-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.2-rc.1) (2019-09-13)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.1...v2.3.2-rc.1)
* Synapse admin functions to release
[\#1033](https://github.com/matrix-org/matrix-js-sdk/pull/1033)
* [To Release] Add matrix base API to report an event
[\#1032](https://github.com/matrix-org/matrix-js-sdk/pull/1032)
Changes in [2.3.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.1) (2019-09-12)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.1-rc.1...v2.3.1)
* No changes since rc.1
Changes in [2.3.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.1-rc.1) (2019-09-11)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.0...v2.3.1-rc.1)
* Update room members on member event redaction
[\#1031](https://github.com/matrix-org/matrix-js-sdk/pull/1031)
Changes in [2.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.0) (2019-08-05)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.3.0-rc.1...v2.3.0)
* [release] Support rewriting push rules when our internal defaults change
[\#1008](https://github.com/matrix-org/matrix-js-sdk/pull/1008)
Changes in [2.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.3.0-rc.1) (2019-07-31)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.2.0...v2.3.0-rc.1)
* Add support for IS v2 API with authentication
[\#1002](https://github.com/matrix-org/matrix-js-sdk/pull/1002)
* Tombstone bugfixes
[\#1001](https://github.com/matrix-org/matrix-js-sdk/pull/1001)
* Support for MSC2140 (terms of service for IS/IM)
[\#988](https://github.com/matrix-org/matrix-js-sdk/pull/988)
* Add a request method to /devices
[\#994](https://github.com/matrix-org/matrix-js-sdk/pull/994)
Changes in [2.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.2.0) (2019-07-18)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.2.0-rc.2...v2.2.0)
* Upgrade lodash dependencies
Changes in [2.2.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.2.0-rc.2) (2019-07-12)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.2.0-rc.1...v2.2.0-rc.2)
* Fix regression from 2.2.0-rc.1 in request to /devices
[\#995](https://github.com/matrix-org/matrix-js-sdk/pull/995)
Changes in [2.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.2.0-rc.1) (2019-07-12)
==========================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.1.1...v2.2.0-rc.1)
* End the verification timer when verification is done
[\#993](https://github.com/matrix-org/matrix-js-sdk/pull/993)
* Stabilize usage of stably stable APIs (in a stable way)
[\#990](https://github.com/matrix-org/matrix-js-sdk/pull/990)
* Expose original_event for /relations
[\#987](https://github.com/matrix-org/matrix-js-sdk/pull/987)
* Process ephemeral events outside timeline handling
[\#989](https://github.com/matrix-org/matrix-js-sdk/pull/989)
* Don't accept any locally known edits earlier than the last known server-side
aggregated edit
[\#986](https://github.com/matrix-org/matrix-js-sdk/pull/986)
* Get edit date transparently from server aggregations or local echo
[\#984](https://github.com/matrix-org/matrix-js-sdk/pull/984)
* Add a function to flag keys for backup without scheduling a backup
[\#982](https://github.com/matrix-org/matrix-js-sdk/pull/982)
* Block read marker and read receipt from advancing into pending events
[\#981](https://github.com/matrix-org/matrix-js-sdk/pull/981)
* Upgrade dependencies
[\#977](https://github.com/matrix-org/matrix-js-sdk/pull/977)
* Add default push rule to ignore reactions
[\#976](https://github.com/matrix-org/matrix-js-sdk/pull/976)
* Fix exception whilst syncing
[\#979](https://github.com/matrix-org/matrix-js-sdk/pull/979)
* Include the error object when raising Session.logged_out
[\#975](https://github.com/matrix-org/matrix-js-sdk/pull/975)
Changes in [2.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.1.1) (2019-07-11)
================================================================================================
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.1.0...v2.1.1)
+2 -2
View File
@@ -322,13 +322,13 @@ To provide the Olm library in a browser application:
To provide the Olm library in a node.js application:
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.0.0.tgz``
* ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``
(replace the URL with the latest version you want to use from
https://packages.matrix.org/npm/olm/)
* ``global.Olm = require('olm');`` *before* loading ``matrix-js-sdk``.
If you want to package Olm as dependency for your node.js application, you can
use ``yarn add https://packages.matrix.org/npm/olm/olm-3.0.0.tgz``. If your
use ``yarn add https://packages.matrix.org/npm/olm/olm-3.1.4.tgz``. If your
application also works without e2e crypto enabled, add ``--optional`` to mark it
as an optional dependency.
-36
View File
@@ -1,36 +0,0 @@
#!/bin/bash -l
set -x
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"
nvm use 10 || exit $?
yarn install || exit $?
RC=0
function fail {
echo $@ >&2
RC=1
}
# don't use last time's test reports
rm -rf reports coverage || exit $?
yarn test || fail "yarn test finished with return code $?"
yarn -s lint -f checkstyle > eslint.xml ||
fail "eslint finished with return code $?"
# delete the old tarball, if it exists
rm -f matrix-js-sdk-*.tgz
# `yarn pack` doesn't seem to run scripts, however that seems okay here as we
# just built as part of `install` above.
yarn pack ||
fail "yarn pack finished with return code $?"
yarn gendoc || fail "JSDoc failed with code $?"
exit $RC
+13 -14
View File
@@ -1,11 +1,11 @@
{
"name": "matrix-js-sdk",
"version": "2.1.1",
"version": "2.4.5",
"description": "Matrix Client-Server SDK for Javascript",
"main": "index.js",
"scripts": {
"test:build": "babel -s -d specbuild spec",
"test:run": "istanbul cover --report text --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" node_modules/mocha/bin/_mocha -- --recursive specbuild --colors --reporter mocha-jenkins-reporter --reporter-options junit_report_path=reports/test-results.xml",
"test:run": "istanbul cover --report text --report cobertura --config .istanbul.yml -i \"lib/**/*.js\" node_modules/mocha/bin/_mocha -- --recursive specbuild --colors",
"test:watch": "mocha --watch --compilers js:babel-core/register --recursive spec --colors",
"test": "yarn test:build && yarn test:run",
"check": "yarn test:build && _mocha --recursive specbuild --colors",
@@ -17,7 +17,7 @@
"build": "babel -s -d lib src && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && terser -c -m -o dist/browser-matrix.min.js --source-map \"content='dist/browser-matrix.js.map'\" dist/browser-matrix.js",
"dist": "yarn build",
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
"lint": "eslint --max-warnings 101 src spec",
"lint": "eslint --max-warnings 93 src spec",
"prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt"
},
"repository": {
@@ -44,7 +44,6 @@
"git-revision.txt",
"index.js",
"browser-index.js",
"jenkins.sh",
"lib",
"package.json",
"release.sh",
@@ -54,11 +53,11 @@
"dependencies": {
"another-json": "^0.2.0",
"babel-runtime": "^6.26.0",
"bluebird": "^3.5.0",
"bluebird": "3.5.5",
"browser-request": "^0.3.3",
"bs58": "^4.0.1",
"content-type": "^1.0.2",
"loglevel": "1.6.1",
"loglevel": "^1.6.4",
"qs": "^6.5.2",
"request": "^2.88.0",
"unhomoglyph": "^1.0.2"
@@ -70,24 +69,24 @@
"babel-plugin-transform-class-properties": "^6.24.1",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-es2015": "^6.18.0",
"babel-preset-es2016": "^6.24.1",
"browserify": "^16.2.3",
"browserify-shim": "^3.8.13",
"eslint": "^5.12.0",
"eslint-config-google": "^0.7.1",
"eslint-plugin-babel": "^5.3.0",
"exorcist": "^0.4.0",
"exorcist": "^1.0.1",
"expect": "^1.20.2",
"istanbul": "^0.4.5",
"jsdoc": "^3.5.5",
"lolex": "^1.5.2",
"matrix-mock-request": "^1.2.3",
"mocha": "^5.2.0",
"mocha-jenkins-reporter": "^0.4.0",
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.0.tgz",
"rimraf": "^2.5.4",
"source-map-support": "^0.4.11",
"sourceify": "^0.1.0",
"terser": "^4.0.0",
"mocha": "^6.2.1",
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
"rimraf": "^3.0.0",
"source-map-support": "^0.5.13",
"sourceify": "^1.0.0",
"terser": "^4.3.8",
"watchify": "^3.11.1"
},
"browserify": {
+5
View File
@@ -195,6 +195,11 @@ if [ $dodist -eq 0 ]; then
pushd "$builddir"
git clone "$projdir" .
git checkout "$rel_branch"
# We use Git branch / commit dependencies for some packages, and Yarn seems
# to have a hard time getting that right. See also
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
# global cache here to ensure we get the right thing.
yarn cache clean
yarn install
# We haven't tagged yet, so tell the dist script what version
# it's building
+23 -2
View File
@@ -302,11 +302,32 @@ describe("MatrixClient events", function() {
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN", function() {
httpBackend.when("GET", "/sync").respond(401, { errcode: 'M_UNKNOWN_TOKEN' });
const error = { errcode: 'M_UNKNOWN_TOKEN' };
httpBackend.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(event, member) {
client.on("Session.logged_out", function(errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client.startClient();
return httpBackend.flushAllExpected().then(function() {
expect(sessionLoggedOutCount).toEqual(
1, "Session.logged_out fired wrong number of times",
);
});
});
it("should emit Session.logged_out on M_UNKNOWN_TOKEN (soft logout)", function() {
const error = { errcode: 'M_UNKNOWN_TOKEN', soft_logout: true };
httpBackend.when("GET", "/sync").respond(401, error);
let sessionLoggedOutCount = 0;
client.on("Session.logged_out", function(errObj) {
sessionLoggedOutCount++;
expect(errObj.data).toEqual(error);
});
client.startClient();
+4 -4
View File
@@ -48,7 +48,7 @@ describe("MatrixClient", function() {
const buf = new Buffer('hello world');
it("should upload the file", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
"POST", "/_matrix/media/r0/upload",
).check(function(req) {
expect(req.rawData).toEqual(buf);
expect(req.queryParams.filename).toEqual("hi.txt");
@@ -87,7 +87,7 @@ describe("MatrixClient", function() {
it("should parse the response if rawResponse=false", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
"POST", "/_matrix/media/r0/upload",
).check(function(req) {
expect(req.opts.json).toBeFalsy();
}).respond(200, { "content_uri": "uri" });
@@ -107,7 +107,7 @@ describe("MatrixClient", function() {
it("should parse errors into a MatrixError", function(done) {
httpBackend.when(
"POST", "/_matrix/media/v1/upload",
"POST", "/_matrix/media/r0/upload",
).check(function(req) {
expect(req.rawData).toEqual(buf);
expect(req.opts.json).toBeFalsy();
@@ -396,7 +396,7 @@ describe("MatrixClient", function() {
const auth = {a: 1};
it("should pass through an auth dict", function(done) {
httpBackend.when(
"DELETE", "/_matrix/client/unstable/devices/my_device",
"DELETE", "/_matrix/client/r0/devices/my_device",
).check(function(req) {
expect(req.data).toEqual({auth: auth});
}).respond(200);
+141
View File
@@ -242,3 +242,144 @@ module.exports.awaitDecryption = function(event) {
});
});
};
const HttpResponse = module.exports.HttpResponse = function(
httpLookups, acceptKeepalives, ignoreUnhandledSync,
) {
this.httpLookups = httpLookups;
this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives;
this.ignoreUnhandledSync = ignoreUnhandledSync;
this.pendingLookup = null;
};
HttpResponse.prototype.request = function HttpResponse(
cb, method, path, qp, data, prefix,
) {
if (path === HttpResponse.KEEP_ALIVE_PATH && this.acceptKeepalives) {
return Promise.resolve();
}
const next = this.httpLookups.shift();
const logLine = (
"MatrixClient[UT] RECV " + method + " " + path + " " +
"EXPECT " + (next ? next.method : next) + " " + (next ? next.path : next)
);
logger.log(logLine);
if (!next) { // no more things to return
if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
logger.log("MatrixClient[UT] Ignoring.");
return Promise.defer().promise;
}
if (this.pendingLookup) {
if (this.pendingLookup.method === method
&& this.pendingLookup.path === path) {
return this.pendingLookup.promise;
}
// >1 pending thing, and they are different, whine.
expect(false).toBe(
true, ">1 pending request. You should probably handle them. " +
"PENDING: " + JSON.stringify(this.pendingLookup) + " JUST GOT: " +
method + " " + path,
);
}
this.pendingLookup = {
promise: Promise.defer().promise,
method: method,
path: path,
};
return this.pendingLookup.promise;
}
if (next.path === path && next.method === method) {
logger.log(
"MatrixClient[UT] Matched. Returning " +
(next.error ? "BAD" : "GOOD") + " response",
);
if (next.expectBody) {
expect(next.expectBody).toEqual(data);
}
if (next.expectQueryParams) {
Object.keys(next.expectQueryParams).forEach(function(k) {
expect(qp[k]).toEqual(next.expectQueryParams[k]);
});
}
if (next.thenCall) {
process.nextTick(next.thenCall, 0); // next tick so we return first.
}
if (next.error) {
return Promise.reject({
errcode: next.error.errcode,
httpStatus: next.error.httpStatus,
name: next.error.errcode,
message: "Expected testing error",
data: next.error,
});
}
return Promise.resolve(next.data);
} else if (method === "GET" && path === "/sync" && this.ignoreUnhandledSync) {
logger.log("MatrixClient[UT] Ignoring.");
this.httpLookups.unshift(next);
return Promise.defer().promise;
}
expect(true).toBe(false, "Expected different request. " + logLine);
return Promise.defer().promise;
};
HttpResponse.KEEP_ALIVE_PATH = "/_matrix/client/versions";
HttpResponse.PUSH_RULES_RESPONSE = {
method: "GET",
path: "/pushrules/",
data: {},
};
HttpResponse.USER_ID = "@alice:bar";
HttpResponse.filterResponse = function(userId) {
const filterPath = "/user/" + encodeURIComponent(userId) + "/filter";
return {
method: "POST",
path: filterPath,
data: { filter_id: "f1lt3r" },
};
};
HttpResponse.SYNC_DATA = {
next_batch: "s_5_3",
presence: { events: [] },
rooms: {},
};
HttpResponse.SYNC_RESPONSE = {
method: "GET",
path: "/sync",
data: HttpResponse.SYNC_DATA,
};
HttpResponse.defaultResponses = function(userId) {
return [
HttpResponse.PUSH_RULES_RESPONSE,
HttpResponse.filterResponse(userId),
HttpResponse.SYNC_RESPONSE,
];
};
module.exports.setHttpResponses = function setHttpResponses(
client, responses, acceptKeepalives, ignoreUnhandledSyncs,
) {
const httpResponseObj = new HttpResponse(
responses, acceptKeepalives, ignoreUnhandledSyncs,
);
const httpReq = httpResponseObj.request.bind(httpResponseObj);
client._http = [
"authedRequest", "authedRequestWithPrefix", "getContentUri",
"request", "requestWithPrefix", "uploadContent",
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
client._http.authedRequest.andCall(httpReq);
client._http.authedRequestWithPrefix.andCall(httpReq);
client._http.requestWithPrefix.andCall(httpReq);
client._http.request.andCall(httpReq);
};
+20 -20
View File
@@ -416,8 +416,8 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(missing base_url)", function() {
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
"is wrong (missing base_url)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
@@ -438,14 +438,14 @@ describe("AutoDiscovery", function() {
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_IS,
state: "SUCCESS",
error: null,
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
base_url: null,
},
@@ -456,8 +456,8 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(empty base_url)", function() {
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
"is wrong (empty base_url)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
@@ -478,14 +478,14 @@ describe("AutoDiscovery", function() {
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_IS,
state: "SUCCESS",
error: null,
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_IS_BASE_URL,
base_url: null,
},
@@ -496,8 +496,8 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(validation error: 404)", function() {
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
"is wrong (validation error: 404)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
@@ -519,14 +519,14 @@ describe("AutoDiscovery", function() {
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_IS,
state: "SUCCESS",
error: null,
// We still expect the base_url to be here for debugging purposes.
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
base_url: "https://identity.example.org",
},
@@ -537,8 +537,8 @@ describe("AutoDiscovery", function() {
]);
});
it("should return FAIL_ERROR when the identity server configuration is wrong " +
"(validation error: 500)", function() {
it("should return SUCCESS / FAIL_PROMPT when the identity server configuration " +
"is wrong (validation error: 500)", function() {
httpBackend.when("GET", "/_matrix/client/versions").check((req) => {
expect(req.opts.uri)
.toEqual("https://chat.example.org/_matrix/client/versions");
@@ -560,14 +560,14 @@ describe("AutoDiscovery", function() {
AutoDiscovery.findClientConfig("example.org").then((conf) => {
const expected = {
"m.homeserver": {
state: "FAIL_ERROR",
error: AutoDiscovery.ERROR_INVALID_IS,
state: "SUCCESS",
error: null,
// We still expect the base_url to be here for debugging purposes
base_url: "https://chat.example.org",
},
"m.identity_server": {
state: "FAIL_ERROR",
state: "FAIL_PROMPT",
error: AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER,
base_url: "https://identity.example.org",
},
+7 -7
View File
@@ -31,7 +31,7 @@ describe("ContentRepo", function() {
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid",
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
);
});
@@ -43,7 +43,7 @@ describe("ContentRepo", function() {
function() {
const mxcUri = "mxc://server.name/resourceid";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32&height=64&method=crop",
);
});
@@ -52,7 +52,7 @@ describe("ContentRepo", function() {
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
baseUrl + "/_matrix/media/v1/thumbnail/server.name/resourceid" +
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
"?width=32#automade",
);
});
@@ -61,7 +61,7 @@ describe("ContentRepo", function() {
function() {
const mxcUri = "mxc://server.name/resourceid#automade";
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
baseUrl + "/_matrix/media/v1/download/server.name/resourceid#automade",
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
);
});
});
@@ -73,21 +73,21 @@ describe("ContentRepo", function() {
it("should set w/h by default to 96", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" +
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
"?width=96&height=96",
);
});
it("should be able to set custom w/h", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foobar" +
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
"?width=32&height=64",
);
});
it("should URL encode the identicon string", function() {
expect(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
baseUrl + "/_matrix/media/v1/identicon/foo%23bar" +
baseUrl + "/_matrix/media/unstable/identicon/foo%23bar" +
"?width=32&height=64",
);
});
+77
View File
@@ -29,6 +29,7 @@ import testUtils from '../../test-utils';
import OlmDevice from '../../../lib/crypto/OlmDevice';
import Crypto from '../../../lib/crypto';
import logger from '../../../src/logger';
import olmlib from '../../../lib/crypto/olmlib';
const Olm = global.Olm;
@@ -83,6 +84,16 @@ const BACKUP_INFO = {
},
};
const keys = {};
function getCrossSigningKey(type) {
return keys[type];
}
function saveCrossSigningKeys(k) {
Object.assign(keys, k);
}
function makeTestClient(sessionStore, cryptoStore) {
const scheduler = [
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
@@ -108,6 +119,7 @@ function makeTestClient(sessionStore, cryptoStore) {
deviceId: "device",
sessionStore: sessionStore,
cryptoStore: cryptoStore,
cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys },
});
}
@@ -296,6 +308,71 @@ describe("MegolmBackup", function() {
});
});
it('signs backups with the cross-signing master key', async function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
const ibGroupSession = new Olm.InboundGroupSession();
ibGroupSession.create(groupSession.session_key());
const client = makeTestClient(sessionStore, cryptoStore);
megolmDecryption = new MegolmDecryption({
userId: '@user:id',
crypto: mockCrypto,
olmDevice: olmDevice,
baseApis: client,
roomId: ROOM_ID,
});
megolmDecryption.olmlib = mockOlmLib;
await client.initCrypto();
let privateKeys;
client.uploadDeviceSigningKeys = async function(e) {return;};
client.uploadKeySignatures = async function(e) {return;};
client.on("crossSigning.saveCrossSigningKeys", function(e) {
privateKeys = e;
});
client.on("crossSigning.getKey", function(e) {
e.done(privateKeys[e.type]);
});
await client.resetCrossSigningKeys();
let numCalls = 0;
await new Promise(async (resolve, reject) => {
client._http.authedRequest = function(
callback, method, path, queryParams, data, opts,
) {
++numCalls;
expect(numCalls).toBeLessThanOrEqualTo(1);
if (numCalls >= 2) {
// exit out of retry loop if there's something wrong
reject(new Error("authedRequest called too many timmes"));
return Promise.resolve({});
}
expect(method).toBe("POST");
expect(path).toBe("/room_keys/version");
try {
// make sure auth_data is signed by the master key
olmlib.pkVerify(
data.auth_data, client.getCrossSigningId(), "@alice:bar",
);
} catch (e) {
reject(e);
return Promise.resolve({});
}
resolve();
return Promise.resolve({});
};
await client.createKeyBackupVersion({
algorithm: "m.megolm_backup.v1",
auth_data: {
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
},
});
});
expect(numCalls).toBe(1);
});
it('retries when a backup fails', function() {
const groupSession = new Olm.OutboundGroupSession();
groupSession.create();
+800
View File
@@ -0,0 +1,800 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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 expect from 'expect';
import anotherjson from 'another-json';
import olmlib from '../../../lib/crypto/olmlib';
import TestClient from '../../TestClient';
import {HttpResponse, setHttpResponses} from '../../test-utils';
async function makeTestClient(userInfo, options, keys) {
if (!keys) keys = {};
function getCrossSigningKey(type) {
return keys[type];
}
function saveCrossSigningKeys(k) {
Object.assign(keys, k);
}
if (!options) options = {};
options.cryptoCallbacks = Object.assign(
{}, { getCrossSigningKey, saveCrossSigningKeys }, options.cryptoCallbacks || {},
);
const client = (new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
)).client;
await client.initCrypto();
return client;
}
describe("Cross Signing", function() {
if (!global.Olm) {
console.warn('Not running megolm backup unit tests: libolm not present');
return;
}
beforeEach(async function() {
await global.Olm.init();
});
it("should sign the master key with the device key", async function() {
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
);
alice.uploadDeviceSigningKeys = expect.createSpy()
.andCall(async (auth, keys) => {
await olmlib.verifySignature(
alice._crypto._olmDevice, keys.master_key, "@alice:example.com",
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key,
);
});
alice.uploadKeySignatures = async () => {};
// set Alice's cross-signing key
await alice.resetCrossSigningKeys();
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
});
it("should upload a signature when a user is verified", async function() {
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
// set Alice's cross-signing key
await alice.resetCrossSigningKeys();
// Alice downloads Bob's device key
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
usage: ["master"],
keys: {
"ed25519:bobs+master+pubkey": "bobs+master+pubkey",
},
},
},
});
// Alice verifies Bob's key
const promise = new Promise((resolve, reject) => {
alice.uploadKeySignatures = (...args) => {
resolve(...args);
};
});
await alice.setDeviceVerified("@bob:example.com", "bobs+master+pubkey", true);
// Alice should send a signature of Bob's key to the server
await promise;
});
it("should get cross-signing keys from sync", async function() {
const masterKey = 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 selfSigningKey = new Uint8Array([
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0,
0x17, 0xb5, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49,
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
]);
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
{
cryptoCallbacks: {
// will be called to sign our own device
getCrossSigningKey: type => {
if (type === 'master') {
return masterKey;
} else {
return selfSigningKey;
}
},
},
},
);
const keyChangePromise = new Promise((resolve, reject) => {
alice.once("crossSigning.keysChanged", async (e) => {
resolve(e);
await alice.checkOwnCrossSigningTrust();
});
});
const uploadSigsPromise = new Promise((resolve, reject) => {
alice.uploadKeySignatures = expect.createSpy().andCall(async (content) => {
await olmlib.verifySignature(
alice._crypto._olmDevice,
content["@alice:example.com"][
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"
],
"@alice:example.com",
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key,
);
olmlib.pkVerify(
content["@alice:example.com"]["Osborne2"],
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
"@alice:example.com",
);
resolve();
});
});
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
.Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
device_id: "Osborne2",
};
aliceDevice.keys = deviceInfo.keys;
aliceDevice.algorithms = deviceInfo.algorithms;
await alice._crypto._signObject(aliceDevice);
olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com");
// feed sync result that includes master key, ssk, device key
const responses = [
HttpResponse.PUSH_RULES_RESPONSE,
{
method: "POST",
path: "/keys/upload/Osborne2",
data: {
one_time_key_counts: {
curve25519: 100,
signed_curve25519: 100,
},
},
},
HttpResponse.filterResponse("@alice:example.com"),
{
method: "GET",
path: "/sync",
data: {
next_batch: "abcdefg",
device_lists: {
changed: [
"@alice:example.com",
"@bob:example.com",
],
},
},
},
{
method: "POST",
path: "/keys/query",
data: {
"failures": {},
"device_keys": {
"@alice:example.com": {
"Osborne2": aliceDevice,
},
},
"master_keys": {
"@alice:example.com": {
user_id: "@alice:example.com",
usage: ["master"],
keys: {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
},
},
},
"self_signing_keys": {
"@alice:example.com": {
user_id: "@alice:example.com",
usage: ["self-signing"],
keys: {
"ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ":
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
},
signatures: {
"@alice:example.com": {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"Wqx/HXR851KIi8/u/UX+fbAMtq9Uj8sr8FsOcqrLfVYa6lAmbXs"
+ "Vhfy4AlZ3dnEtjgZx0U0QDrghEn2eYBeOCA",
},
},
},
},
},
},
{
method: "POST",
path: "/keys/upload/Osborne2",
data: {
one_time_key_counts: {
curve25519: 100,
signed_curve25519: 100,
},
},
},
];
setHttpResponses(alice, responses, true, true);
await alice.startClient();
// once ssk is confirmed, device key should be trusted
await keyChangePromise;
await uploadSigsPromise;
const aliceTrust = alice.checkUserTrust("@alice:example.com");
expect(aliceTrust.isCrossSigningVerified()).toBeTruthy();
expect(aliceTrust.isTofu()).toBeTruthy();
expect(aliceTrust.isVerified()).toBeTruthy();
const aliceDeviceTrust = alice.checkDeviceTrust("@alice:example.com", "Osborne2");
expect(aliceDeviceTrust.isCrossSigningVerified()).toBeTruthy();
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
expect(aliceDeviceTrust.isTofu()).toBeTruthy();
expect(aliceDeviceTrust.isVerified()).toBeTruthy();
});
it("should use trust chain to determine device verification", async function() {
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
// set Alice's cross-signing key
await alice.resetCrossSigningKeys();
// Alice downloads Bob's ssk and device key
const bobMasterSigning = new global.Olm.PkSigning();
const bobMasterPrivkey = bobMasterSigning.generate_seed();
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
const bobSigning = new global.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
["ed25519:" + bobPubkey]: bobPubkey,
},
};
const sskSig = bobMasterSigning.sign(anotherjson.stringify(bobSSK));
bobSSK.signatures = {
"@bob:example.com": {
["ed25519:" + bobMasterPubkey]: sskSig,
},
};
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
usage: ["master"],
keys: {
["ed25519:" + bobMasterPubkey]: bobMasterPubkey,
},
},
self_signing: bobSSK,
},
firstUse: 1,
unsigned: {},
});
const bobDevice = {
user_id: "@bob:example.com",
device_id: "Dynabook",
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: {
"curve25519:Dynabook": "somePubkey",
"ed25519:Dynabook": "someOtherPubkey",
},
};
const sig = bobSigning.sign(anotherjson.stringify(bobDevice));
bobDevice.signatures = {
"@bob:example.com": {
["ed25519:" + bobPubkey]: sig,
},
};
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
// Bob's device key should be TOFU
const bobTrust = alice.checkUserTrust("@bob:example.com");
expect(bobTrust.isVerified()).toBeFalsy();
expect(bobTrust.isTofu()).toBeTruthy();
const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
expect(bobDeviceTrust.isVerified()).toBeFalsy();
expect(bobDeviceTrust.isTofu()).toBeTruthy();
// Alice verifies Bob's SSK
alice.uploadKeySignatures = () => {};
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true);
// Bob's device key should be trusted
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
expect(bobTrust2.isCrossSigningVerified()).toBeTruthy();
expect(bobTrust2.isTofu()).toBeTruthy();
const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
expect(bobDeviceTrust2.isCrossSigningVerified()).toBeTruthy();
expect(bobDeviceTrust2.isLocallyVerified()).toBeFalsy();
expect(bobDeviceTrust2.isTofu()).toBeTruthy();
});
it("should trust signatures received from other devices", async function() {
const aliceKeys = {};
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
null,
aliceKeys,
);
alice._crypto._deviceList.startTrackingDeviceList("@bob:example.com");
alice._crypto._deviceList.stopTrackingAllDeviceLists = () => {};
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
// set Alice's cross-signing key
await alice.resetCrossSigningKeys();
const selfSigningKey = new Uint8Array([
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
0x6b, 0xf8, 0x93, 0xf5, 0xb0, 0x4d, 0x17, 0xc0,
0x17, 0xb5, 0xa5, 0xf6, 0x59, 0x11, 0x8b, 0x49,
0x34, 0xf2, 0x4b, 0x64, 0x9b, 0x52, 0xf8, 0x5f,
]);
const keyChangePromise = new Promise((resolve, reject) => {
alice._crypto._deviceList.once("userCrossSigningUpdated", (userId) => {
if (userId === "@bob:example.com") {
resolve();
}
});
});
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
.Osborne2;
const aliceDevice = {
user_id: "@alice:example.com",
device_id: "Osborne2",
};
aliceDevice.keys = deviceInfo.keys;
aliceDevice.algorithms = deviceInfo.algorithms;
await alice._crypto._signObject(aliceDevice);
const bobOlmAccount = new global.Olm.Account();
bobOlmAccount.create();
const bobKeys = JSON.parse(bobOlmAccount.identity_keys());
const bobDevice = {
user_id: "@bob:example.com",
device_id: "Dynabook",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobKeys.ed25519,
"curve25519:Dynabook": bobKeys.curve25519,
},
};
const deviceStr = anotherjson.stringify(bobDevice);
bobDevice.signatures = {
"@bob:example.com": {
"ed25519:Dynabook": bobOlmAccount.sign(deviceStr),
},
};
olmlib.pkSign(bobDevice, selfSigningKey, "@bob:example.com");
const bobMaster = {
user_id: "@bob:example.com",
usage: ["master"],
keys: {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk",
},
};
olmlib.pkSign(bobMaster, aliceKeys.user_signing, "@alice:example.com");
// Alice downloads Bob's keys
// - device key
// - ssk
// - master key signed by her usk (pretend that it was signed by another
// of Alice's devices)
const responses = [
HttpResponse.PUSH_RULES_RESPONSE,
{
method: "POST",
path: "/keys/upload/Osborne2",
data: {
one_time_key_counts: {
curve25519: 100,
signed_curve25519: 100,
},
},
},
HttpResponse.filterResponse("@alice:example.com"),
{
method: "GET",
path: "/sync",
data: {
next_batch: "abcdefg",
device_lists: {
changed: [
"@bob:example.com",
],
},
},
},
{
method: "POST",
path: "/keys/query",
data: {
"failures": {},
"device_keys": {
"@alice:example.com": {
"Osborne2": aliceDevice,
},
"@bob:example.com": {
"Dynabook": bobDevice,
},
},
"master_keys": {
"@bob:example.com": bobMaster,
},
"self_signing_keys": {
"@bob:example.com": {
user_id: "@bob:example.com",
usage: ["self-signing"],
keys: {
"ed25519:EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ":
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
},
signatures: {
"@bob:example.com": {
"ed25519:nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk":
"2KLiufImvEbfJuAFvsaZD+PsL8ELWl7N1u9yr/9hZvwRghBfQMB"
+ "LAI86b1kDV9+Cq1lt85ykReeCEzmTEPY2BQ",
},
},
},
},
},
},
{
method: "POST",
path: "/keys/upload/Osborne2",
data: {
one_time_key_counts: {
curve25519: 100,
signed_curve25519: 100,
},
},
},
];
setHttpResponses(alice, responses);
await alice.startClient();
await keyChangePromise;
// Bob's device key should be trusted
const bobTrust = alice.checkUserTrust("@bob:example.com");
expect(bobTrust.isCrossSigningVerified()).toBeTruthy();
expect(bobTrust.isTofu()).toBeTruthy();
const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
expect(bobDeviceTrust.isCrossSigningVerified()).toBeTruthy();
expect(bobDeviceTrust.isLocallyVerified()).toBeFalsy();
expect(bobDeviceTrust.isTofu()).toBeTruthy();
});
it("should dis-trust an unsigned device", async function() {
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
// set Alice's cross-signing key
await alice.resetCrossSigningKeys();
// Alice downloads Bob's ssk and device key
// (NOTE: device key is not signed by ssk)
const bobMasterSigning = new global.Olm.PkSigning();
const bobMasterPrivkey = bobMasterSigning.generate_seed();
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
const bobSigning = new global.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
["ed25519:" + bobPubkey]: bobPubkey,
},
};
const sskSig = bobMasterSigning.sign(anotherjson.stringify(bobSSK));
bobSSK.signatures = {
"@bob:example.com": {
["ed25519:" + bobMasterPubkey]: sskSig,
},
};
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
usage: ["master"],
keys: {
["ed25519:" + bobMasterPubkey]: bobMasterPubkey,
},
},
self_signing: bobSSK,
},
firstUse: 1,
unsigned: {},
});
const bobDevice = {
user_id: "@bob:example.com",
device_id: "Dynabook",
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: {
"curve25519:Dynabook": "somePubkey",
"ed25519:Dynabook": "someOtherPubkey",
},
};
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
// Bob's device key should be untrusted
const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
expect(bobDeviceTrust.isVerified()).toBeFalsy();
expect(bobDeviceTrust.isTofu()).toBeFalsy();
// Alice verifies Bob's SSK
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true);
// Bob's device key should be untrusted
const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
expect(bobDeviceTrust2.isVerified()).toBeFalsy();
expect(bobDeviceTrust2.isTofu()).toBeFalsy();
});
it("should dis-trust a user when their ssk changes", async function() {
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
await alice.resetCrossSigningKeys();
// Alice downloads Bob's keys
const bobMasterSigning = new global.Olm.PkSigning();
const bobMasterPrivkey = bobMasterSigning.generate_seed();
const bobMasterPubkey = bobMasterSigning.init_with_seed(bobMasterPrivkey);
const bobSigning = new global.Olm.PkSigning();
const bobPrivkey = bobSigning.generate_seed();
const bobPubkey = bobSigning.init_with_seed(bobPrivkey);
const bobSSK = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
["ed25519:" + bobPubkey]: bobPubkey,
},
};
const sskSig = bobMasterSigning.sign(anotherjson.stringify(bobSSK));
bobSSK.signatures = {
"@bob:example.com": {
["ed25519:" + bobMasterPubkey]: sskSig,
},
};
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
usage: ["master"],
keys: {
["ed25519:" + bobMasterPubkey]: bobMasterPubkey,
},
},
self_signing: bobSSK,
},
firstUse: 1,
unsigned: {},
});
const bobDevice = {
user_id: "@bob:example.com",
device_id: "Dynabook",
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: {
"curve25519:Dynabook": "somePubkey",
"ed25519:Dynabook": "someOtherPubkey",
},
};
const bobDeviceString = anotherjson.stringify(bobDevice);
const sig = bobSigning.sign(bobDeviceString);
bobDevice.signatures = {};
bobDevice.signatures["@bob:example.com"] = {};
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig;
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
// Alice verifies Bob's SSK
alice.uploadKeySignatures = () => {};
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey, true);
// Bob's device key should be trusted
const bobDeviceTrust = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
expect(bobDeviceTrust.isVerified()).toBeTruthy();
expect(bobDeviceTrust.isTofu()).toBeTruthy();
// Alice downloads new SSK for Bob
const bobMasterSigning2 = new global.Olm.PkSigning();
const bobMasterPrivkey2 = bobMasterSigning2.generate_seed();
const bobMasterPubkey2 = bobMasterSigning2.init_with_seed(bobMasterPrivkey2);
const bobSigning2 = new global.Olm.PkSigning();
const bobPrivkey2 = bobSigning2.generate_seed();
const bobPubkey2 = bobSigning2.init_with_seed(bobPrivkey2);
const bobSSK2 = {
user_id: "@bob:example.com",
usage: ["self_signing"],
keys: {
["ed25519:" + bobPubkey2]: bobPubkey2,
},
};
const sskSig2 = bobMasterSigning2.sign(anotherjson.stringify(bobSSK2));
bobSSK2.signatures = {
"@bob:example.com": {
["ed25519:" + bobMasterPubkey2]: sskSig2,
},
};
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
keys: {
master: {
user_id: "@bob:example.com",
usage: ["master"],
keys: {
["ed25519:" + bobMasterPubkey2]: bobMasterPubkey2,
},
},
self_signing: bobSSK2,
},
firstUse: 0,
unsigned: {},
});
// Bob's and his device should be untrusted
const bobTrust = alice.checkUserTrust("@bob:example.com");
expect(bobTrust.isVerified()).toBeFalsy();
expect(bobTrust.isTofu()).toBeFalsy();
const bobDeviceTrust2 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
expect(bobDeviceTrust2.isVerified()).toBeFalsy();
expect(bobDeviceTrust2.isTofu()).toBeFalsy();
// Alice verifies Bob's SSK
alice.uploadKeySignatures = () => {};
await alice.setDeviceVerified("@bob:example.com", bobMasterPubkey2, true);
// Bob should be trusted but not his device
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
expect(bobTrust2.isVerified()).toBeTruthy();
const bobDeviceTrust3 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
expect(bobDeviceTrust3.isVerified()).toBeFalsy();
// Alice gets new signature for device
const sig2 = bobSigning2.sign(bobDeviceString);
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey2] = sig2;
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: bobDevice,
});
// Bob's device should be trusted again (but not TOFU)
const bobTrust3 = alice.checkUserTrust("@bob:example.com");
expect(bobTrust3.isVerified()).toBeTruthy();
const bobDeviceTrust4 = alice.checkDeviceTrust("@bob:example.com", "Dynabook");
expect(bobDeviceTrust4.isCrossSigningVerified()).toBeTruthy();
});
it("should offer to upgrade device verifications to cross-signing", async function() {
let upgradeResolveFunc;
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
{
cryptoCallbacks: {
shouldUpgradeDeviceVerifications: (verifs) => {
expect(verifs.users["@bob:example.com"]).toExist();
upgradeResolveFunc();
return ["@bob:example.com"];
},
},
},
);
const bob = await makeTestClient(
{userId: "@bob:example.com", deviceId: "Dynabook"},
);
bob.uploadDeviceSigningKeys = async () => {};
bob.uploadKeySignatures = async () => {};
// set Bob's cross-signing key
await bob.resetCrossSigningKeys();
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
Dynabook: {
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
keys: {
"curve25519:Dynabook": bob._crypto._olmDevice.deviceCurve25519Key,
"ed25519:Dynabook": bob._crypto._olmDevice.deviceEd25519Key,
},
verified: 1,
known: true,
},
});
alice._crypto._deviceList.storeCrossSigningForUser(
"@bob:example.com",
bob._crypto._crossSigningInfo.toStorage(),
);
alice.uploadDeviceSigningKeys = async () => {};
alice.uploadKeySignatures = async () => {};
// when alice sets up cross-signing, she should notice that bob's
// cross-signing key is signed by his Dynabook, which alice has
// verified, and ask if the device verification should be upgraded to a
// cross-signing verification
let upgradePromise = new Promise((resolve) => {
upgradeResolveFunc = resolve;
});
await alice.resetCrossSigningKeys();
await upgradePromise;
const bobTrust = alice.checkUserTrust("@bob:example.com");
expect(bobTrust.isCrossSigningVerified()).toBeTruthy();
expect(bobTrust.isTofu()).toBeTruthy();
// "forget" that Bob is trusted
delete alice._crypto._deviceList._crossSigningInfo["@bob:example.com"]
.keys.master.signatures["@alice:example.com"];
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
expect(bobTrust2.isCrossSigningVerified()).toBeFalsy();
expect(bobTrust2.isTofu()).toBeTruthy();
upgradePromise = new Promise((resolve) => {
upgradeResolveFunc = resolve;
});
alice._crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
await upgradePromise;
const bobTrust3 = alice.checkUserTrust("@bob:example.com");
expect(bobTrust3.isCrossSigningVerified()).toBeTruthy();
expect(bobTrust3.isTofu()).toBeTruthy();
});
});
+248
View File
@@ -0,0 +1,248 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import '../../olm-loader';
import expect from 'expect';
import { MatrixEvent } from '../../../lib/models/event';
import { SECRET_STORAGE_ALGORITHM_V1 } from '../../../lib/crypto/SecretStorage';
import olmlib from '../../../lib/crypto/olmlib';
import TestClient from '../../TestClient';
import { makeTestClients } from './verification/util';
async function makeTestClient(userInfo, options) {
const client = (new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
)).client;
await client.initCrypto();
return client;
}
describe("Secrets", function() {
if (!global.Olm) {
console.warn('Not running megolm backup unit tests: libolm not present');
return;
}
beforeEach(async function() {
await global.Olm.init();
});
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 signing = new global.Olm.PkSigning();
const signingKey = signing.generate_seed();
const signingPubKey = signing.init_with_seed(signingKey);
const signingkeyInfo = {
user_id: "@alice:example.com",
usage: ['master'],
keys: {
['ed25519:' + signingPubKey]: signingPubKey,
},
};
const getKey = expect.createSpy().andCall(e => {
expect(Object.keys(e.keys)).toEqual(["abc"]);
return ['abc', privkey];
});
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
{
cryptoCallbacks: {
getCrossSigningKey: t => signingKey,
getSecretStorageKey: getKey,
},
},
);
alice._crypto._crossSigningInfo.setKeys({
master: signingkeyInfo,
});
const secretStorage = alice._crypto._secretStorage;
alice.setAccountData = async function(eventType, contents, callback) {
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: eventType,
content: contents,
}),
]);
if (callback) {
callback();
}
};
const keyAccountData = {
algorithm: SECRET_STORAGE_ALGORITHM_V1,
pubkey: pubkey,
};
await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master');
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: "m.secret_storage.key.abc",
content: keyAccountData,
}),
]);
expect(secretStorage.isStored("foo")).toBe(false);
await secretStorage.store("foo", "bar", ["abc"]);
expect(secretStorage.isStored("foo")).toBe(true);
expect(await secretStorage.get("foo")).toBe("bar");
expect(getKey).toHaveBeenCalled();
});
it("should throw if given a key that doesn't exist", async function() {
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
);
try {
await alice.storeSecret("foo", "bar", ["this secret does not exist"]);
// should be able to use expect(...).toThrow() but mocha still fails
// the test even when it throws for reasons I have no inclination to debug
expect(true).toBeFalsy();
} catch (e) {
}
});
it("should refuse to encrypt with zero keys", async function() {
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
);
try {
await alice.storeSecret("foo", "bar", []);
expect(true).toBeFalsy();
} catch (e) {
}
});
it("should encrypt with default key if keys is null", async function() {
let keys = {};
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
{
cryptoCallbacks: {
getCrossSigningKey: t => keys[t],
saveCrossSigningKeys: k => keys = k,
},
},
);
alice.setAccountData = async function(eventType, contents, callback) {
alice.store.storeAccountDataEvents([
new MatrixEvent({
type: eventType,
content: contents,
}),
]);
};
alice.resetCrossSigningKeys();
const newKeyId = await alice.addSecretKey(
SECRET_STORAGE_ALGORITHM_V1,
);
// 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
alice.setDefaultSecretStorageKeyId(newKeyId);
await alice.storeSecret("foo", "bar");
const accountData = alice.getAccountData('foo');
expect(accountData.getContent().encrypted).toBeTruthy();
});
it("should refuse to encrypt if no keys given and no default key", async function() {
const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"},
);
try {
await alice.storeSecret("foo", "bar");
expect(true).toBeFalsy();
} catch (e) {
}
});
it("should request secrets from other clients", async function() {
const [osborne2, vax] = await makeTestClients(
[
{userId: "@alice:example.com", deviceId: "Osborne2"},
{userId: "@alice:example.com", deviceId: "VAX"},
],
{
cryptoCallbacks: {
onSecretRequested: e => {
expect(e.name).toBe("foo");
return "bar";
},
},
},
);
const vaxDevice = vax.client._crypto._olmDevice;
const osborne2Device = osborne2.client._crypto._olmDevice;
const secretStorage = osborne2.client._crypto._secretStorage;
osborne2.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", {
"VAX": {
user_id: "@alice:example.com",
device_id: "VAX",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:VAX": vaxDevice.deviceEd25519Key,
"curve25519:VAX": vaxDevice.deviceCurve25519Key,
},
},
});
vax.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", {
"Osborne2": {
user_id: "@alice:example.com",
device_id: "Osborne2",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Osborne2": osborne2Device.deviceEd25519Key,
"curve25519:Osborne2": osborne2Device.deviceCurve25519Key,
},
},
});
await osborne2Device.generateOneTimeKeys(1);
const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
await osborne2Device.markKeysAsPublished();
await vax.client._crypto._olmDevice.createOutboundSession(
osborne2Device.deviceCurve25519Key,
Object.values(otks)[0],
);
const request = await secretStorage.request("foo", ["VAX"]);
const secret = await request.promise;
expect(secret).toBe("bar");
});
});
+11 -5
View File
@@ -51,7 +51,7 @@ describe("verification request", function() {
verificationMethods: [verificationMethods.SAS],
},
);
alice._crypto._deviceList.getRawStoredDevicesForUser = function() {
alice.client._crypto._deviceList.getRawStoredDevicesForUser = function() {
return {
Dynabook: {
keys: {
@@ -60,17 +60,23 @@ describe("verification request", function() {
},
};
};
alice.downloadKeys = () => {
alice.client.downloadKeys = () => {
return Promise.resolve();
};
bob.downloadKeys = () => {
bob.client.downloadKeys = () => {
return Promise.resolve();
};
bob.on("crypto.verification.request", (request) => {
bob.client.on("crypto.verification.request", (request) => {
const bobVerifier = request.beginKeyVerification(verificationMethods.SAS);
bobVerifier.verify();
// XXX: Private function access (but it's a test, so we're okay)
bobVerifier._endTimer();
});
const aliceVerifier = await alice.requestVerification("@bob:example.com");
const aliceVerifier = await alice.client.requestVerification("@bob:example.com");
expect(aliceVerifier).toBeAn(SAS);
// XXX: Private function access (but it's a test, so we're okay)
aliceVerifier._endTimer();
});
});
+288 -54
View File
@@ -22,6 +22,7 @@ try {
}
import expect from 'expect';
import olmlib from '../../../../lib/crypto/olmlib';
import sdk from '../../../..';
@@ -36,6 +37,9 @@ const MatrixEvent = sdk.MatrixEvent;
import {makeTestClients} from './util';
let ALICE_DEVICES;
let BOB_DEVICES;
describe("SAS verification", function() {
if (!global.Olm) {
logger.warn('Not running device verification unit tests: libolm not present');
@@ -57,6 +61,9 @@ describe("SAS verification", function() {
await sas.verify()
.catch(spy);
expect(spy).toHaveBeenCalled();
// Cancel the SAS for cleanup (we started a verification, so abort)
sas.cancel();
});
describe("verification", function() {
@@ -78,38 +85,43 @@ describe("SAS verification", function() {
},
);
alice.setDeviceVerified = expect.createSpy();
alice.getDeviceEd25519Key = () => {
return "alice+base64+ed25519+key";
};
alice.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Dynabook": "bob+base64+ed25519+key",
},
const aliceDevice = alice.client._crypto._olmDevice;
const bobDevice = bob.client._crypto._olmDevice;
ALICE_DEVICES = {
Osborne2: {
user_id: "@alice:example.com",
device_id: "Osborne2",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Osborne2": aliceDevice.deviceEd25519Key,
"curve25519:Osborne2": aliceDevice.deviceCurve25519Key,
},
"Dynabook",
);
},
};
BOB_DEVICES = {
Dynabook: {
user_id: "@bob:example.com",
device_id: "Dynabook",
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
keys: {
"ed25519:Dynabook": bobDevice.deviceEd25519Key,
"curve25519:Dynabook": bobDevice.deviceCurve25519Key,
},
},
};
alice.client._crypto._deviceList.storeDevicesForUser(
"@bob:example.com", BOB_DEVICES,
);
alice.downloadKeys = () => {
return Promise.resolve();
};
bob.setDeviceVerified = expect.createSpy();
bob.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Osborne2": "alice+base64+ed25519+key",
},
},
"Osborne2",
);
};
bob.getDeviceEd25519Key = () => {
return "bob+base64+ed25519+key";
};
bob.client._crypto._deviceList.storeDevicesForUser(
"@alice:example.com", ALICE_DEVICES,
);
bob.downloadKeys = () => {
return Promise.resolve();
};
@@ -118,7 +130,7 @@ describe("SAS verification", function() {
bobSasEvent = null;
bobPromise = new Promise((resolve, reject) => {
bob.on("crypto.verification.start", (verifier) => {
bob.client.on("crypto.verification.start", (verifier) => {
verifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
@@ -139,8 +151,8 @@ describe("SAS verification", function() {
});
});
aliceVerifier = alice.beginKeyVerification(
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
aliceVerifier = alice.client.beginKeyVerification(
verificationMethods.SAS, bob.client.getUserId(), bob.deviceId,
);
aliceVerifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
@@ -162,66 +174,165 @@ describe("SAS verification", function() {
it("should verify a key", async function() {
let macMethod;
const origSendToDevice = alice.sendToDevice;
bob.sendToDevice = function(type, map) {
const origSendToDevice = alice.client.sendToDevice;
bob.client.sendToDevice = function(type, map) {
if (type === "m.key.verification.accept") {
macMethod = map[alice.getUserId()][alice.deviceId]
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
.message_authentication_code;
}
return origSendToDevice.call(this, type, map);
};
alice.httpBackend.when('POST', '/keys/query').respond(200, {
failures: {},
device_keys: {
"@bob:example.com": BOB_DEVICES,
},
});
bob.httpBackend.when('POST', '/keys/query').respond(200, {
failures: {},
device_keys: {
"@alice:example.com": ALICE_DEVICES,
},
});
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
alice.httpBackend.flush(),
bob.httpBackend.flush(),
]);
// make sure that it uses the preferred method
expect(macMethod).toBe("hkdf-hmac-sha256");
// make sure Alice and Bob verified each other
expect(alice.setDeviceVerified)
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
expect(bob.setDeviceVerified)
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
const bobDevice
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice.isVerified()).toBeTruthy();
const aliceDevice
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice.isVerified()).toBeTruthy();
});
it("should be able to verify using the old MAC", async function() {
// pretend that Alice can only understand the old (incorrect) MAC,
// and make sure that she can still verify with Bob
let macMethod;
const origSendToDevice = alice.sendToDevice;
alice.sendToDevice = function(type, map) {
const origSendToDevice = alice.client.sendToDevice;
alice.client.sendToDevice = function(type, map) {
if (type === "m.key.verification.start") {
// Note: this modifies not only the message that Bob
// receives, but also the copy of the message that Alice
// has, since it is the same object. If this does not
// happen, the verification will fail due to a hash
// commitment mismatch.
map[bob.getUserId()][bob.deviceId]
map[bob.client.getUserId()][bob.client.deviceId]
.message_authentication_codes = ['hmac-sha256'];
}
return origSendToDevice.call(this, type, map);
};
bob.sendToDevice = function(type, map) {
bob.client.sendToDevice = function(type, map) {
if (type === "m.key.verification.accept") {
macMethod = map[alice.getUserId()][alice.deviceId]
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
.message_authentication_code;
}
return origSendToDevice.call(this, type, map);
};
alice.httpBackend.when('POST', '/keys/query').respond(200, {
failures: {},
device_keys: {
"@bob:example.com": BOB_DEVICES,
},
});
bob.httpBackend.when('POST', '/keys/query').respond(200, {
failures: {},
device_keys: {
"@alice:example.com": ALICE_DEVICES,
},
});
await Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => verifier.verify()),
alice.httpBackend.flush(),
bob.httpBackend.flush(),
]);
expect(macMethod).toBe("hmac-sha256");
expect(alice.setDeviceVerified)
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
expect(bob.setDeviceVerified)
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
const bobDevice
= await alice.client.getStoredDevice("@bob:example.com", "Dynabook");
expect(bobDevice.isVerified()).toBeTruthy();
const aliceDevice
= await bob.client.getStoredDevice("@alice:example.com", "Osborne2");
expect(aliceDevice.isVerified()).toBeTruthy();
});
it("should verify a cross-signing key", async function() {
alice.httpBackend.when('POST', '/keys/device_signing/upload').respond(
200, {},
);
alice.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
alice.httpBackend.flush(undefined, 2);
await alice.client.resetCrossSigningKeys();
bob.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {});
bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
bob.httpBackend.flush(undefined, 2);
await bob.client.resetCrossSigningKeys();
bob.client._crypto._deviceList.storeCrossSigningForUser(
"@alice:example.com", {
keys: alice.client._crypto._crossSigningInfo.keys,
},
);
alice.httpBackend.when('POST', '/keys/query').respond(200, {
failures: {},
device_keys: {
"@bob:example.com": BOB_DEVICES,
},
});
bob.httpBackend.when('POST', '/keys/query').respond(200, {
failures: {},
device_keys: {
"@alice:example.com": ALICE_DEVICES,
},
});
const verifyProm = Promise.all([
aliceVerifier.verify(),
bobPromise.then((verifier) => {
bob.httpBackend.when(
'POST', '/keys/signatures/upload',
).respond(200, {});
bob.httpBackend.flush(undefined, 2);
return verifier.verify();
}),
]);
await alice.httpBackend.flush(undefined, 1);
console.log("alice reqs flushed");
await verifyProm;
const bobDeviceTrust = alice.client.checkDeviceTrust(
"@bob:example.com", "Dynabook",
);
expect(bobDeviceTrust.isLocallyVerified()).toBeTruthy();
expect(bobDeviceTrust.isCrossSigningVerified()).toBeFalsy();
const aliceTrust = bob.client.checkUserTrust("@alice:example.com");
expect(aliceTrust.isCrossSigningVerified()).toBeTruthy();
expect(aliceTrust.isTofu()).toBeTruthy();
const aliceDeviceTrust = bob.client.checkDeviceTrust(
"@alice:example.com", "Osborne2",
);
expect(aliceDeviceTrust.isLocallyVerified()).toBeTruthy();
expect(aliceDeviceTrust.isCrossSigningVerified()).toBeFalsy();
});
});
@@ -235,17 +346,17 @@ describe("SAS verification", function() {
verificationMethods: [verificationMethods.SAS],
},
);
alice.setDeviceVerified = expect.createSpy();
alice.downloadKeys = () => {
alice.client.setDeviceVerified = expect.createSpy();
alice.client.downloadKeys = () => {
return Promise.resolve();
};
bob.setDeviceVerified = expect.createSpy();
bob.downloadKeys = () => {
bob.client.setDeviceVerified = expect.createSpy();
bob.client.downloadKeys = () => {
return Promise.resolve();
};
const bobPromise = new Promise((resolve, reject) => {
bob.on("crypto.verification.start", (verifier) => {
bob.client.on("crypto.verification.start", (verifier) => {
verifier.on("show_sas", (e) => {
e.mismatch();
});
@@ -253,8 +364,8 @@ describe("SAS verification", function() {
});
});
const aliceVerifier = alice.beginKeyVerification(
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
const aliceVerifier = alice.client.beginKeyVerification(
verificationMethods.SAS, bob.client.getUserId(), bob.client.deviceId,
);
const aliceSpy = expect.createSpy();
@@ -265,9 +376,132 @@ describe("SAS verification", function() {
]);
expect(aliceSpy).toHaveBeenCalled();
expect(bobSpy).toHaveBeenCalled();
expect(alice.setDeviceVerified)
expect(alice.client.setDeviceVerified)
.toNotHaveBeenCalled();
expect(bob.setDeviceVerified)
expect(bob.client.setDeviceVerified)
.toNotHaveBeenCalled();
});
describe("verification in DM", function() {
let alice;
let bob;
let aliceSasEvent;
let bobSasEvent;
let aliceVerifier;
let bobPromise;
beforeEach(async function() {
[alice, bob] = await makeTestClients(
[
{userId: "@alice:example.com", deviceId: "Osborne2"},
{userId: "@bob:example.com", deviceId: "Dynabook"},
],
{
verificationMethods: [verificationMethods.SAS],
},
);
alice.client.setDeviceVerified = expect.createSpy();
alice.client.getDeviceEd25519Key = () => {
return "alice+base64+ed25519+key";
};
alice.client.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Dynabook": "bob+base64+ed25519+key",
},
},
"Dynabook",
);
};
alice.client.downloadKeys = () => {
return Promise.resolve();
};
bob.client.setDeviceVerified = expect.createSpy();
bob.client.getStoredDevice = () => {
return DeviceInfo.fromStorage(
{
keys: {
"ed25519:Osborne2": "alice+base64+ed25519+key",
},
},
"Osborne2",
);
};
bob.client.getDeviceEd25519Key = () => {
return "bob+base64+ed25519+key";
};
bob.client.downloadKeys = () => {
return Promise.resolve();
};
aliceSasEvent = null;
bobSasEvent = null;
bobPromise = new Promise((resolve, reject) => {
bob.client.on("event", async (event) => {
const content = event.getContent();
if (event.getType() === "m.room.message"
&& content.msgtype === "m.key.verification.request") {
expect(content.methods).toInclude(SAS.NAME);
expect(content.to).toBe(bob.client.getUserId());
const verifier = bob.client.acceptVerificationDM(event, SAS.NAME);
verifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!aliceSasEvent) {
bobSasEvent = e;
} else {
try {
expect(e.sas).toEqual(aliceSasEvent.sas);
e.confirm();
aliceSasEvent.confirm();
} catch (error) {
e.mismatch();
aliceSasEvent.mismatch();
}
}
});
await verifier.verify();
resolve();
}
});
});
aliceVerifier = await alice.client.requestVerificationDM(
bob.client.getUserId(), "!room_id", [verificationMethods.SAS],
);
aliceVerifier.on("show_sas", (e) => {
if (!e.sas.emoji || !e.sas.decimal) {
e.cancel();
} else if (!bobSasEvent) {
aliceSasEvent = e;
} else {
try {
expect(e.sas).toEqual(bobSasEvent.sas);
e.confirm();
bobSasEvent.confirm();
} catch (error) {
e.mismatch();
bobSasEvent.mismatch();
}
}
});
});
it("should verify a key", async function() {
await Promise.all([
aliceVerifier.verify(),
bobPromise,
]);
// make sure Alice and Bob verified each other
expect(alice.client.setDeviceVerified)
.toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId);
expect(bob.client.setDeviceVerified)
.toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId);
});
});
});
+43 -11
View File
@@ -33,31 +33,63 @@ export async function makeTestClients(userInfos, options) {
type: type,
content: msg,
});
setTimeout(
() => clientMap[userId][deviceId]
.emit("toDeviceEvent", event),
0,
);
const client = clientMap[userId][deviceId];
if (event.isEncrypted()) {
event.attemptDecryption(client._crypto)
.then(() => client.emit("toDeviceEvent", event));
} else {
setTimeout(
() => client.emit("toDeviceEvent", event),
0,
);
}
}
}
}
}
};
const sendEvent = function(room, type, content) {
// make up a unique ID as the event ID
const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this
const event = new MatrixEvent({
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
type: type,
content: content,
room_id: room,
event_id: eventId,
});
for (const tc of clients) {
setTimeout(
() => tc.client.emit("event", event),
0,
);
}
return {event_id: eventId};
};
for (const userInfo of userInfos) {
const client = (new TestClient(
let keys = {};
if (!options) options = {};
if (!options.cryptoCallbacks) options.cryptoCallbacks = {};
if (!options.cryptoCallbacks.saveCrossSigningKeys) {
options.cryptoCallbacks.saveCrossSigningKeys = k => { keys = k; };
options.cryptoCallbacks.getCrossSigningKey = typ => keys[typ];
}
const testClient = new TestClient(
userInfo.userId, userInfo.deviceId, undefined, undefined,
options,
)).client;
);
if (!(userInfo.userId in clientMap)) {
clientMap[userInfo.userId] = {};
}
clientMap[userInfo.userId][userInfo.deviceId] = client;
client.sendToDevice = sendToDevice;
clients.push(client);
clientMap[userInfo.userId][userInfo.deviceId] = testClient.client;
testClient.client.sendToDevice = sendToDevice;
testClient.client.sendEvent = sendEvent;
clients.push(testClient);
}
await Promise.all(clients.map((client) => client.initCrypto()));
await Promise.all(clients.map((testClient) => testClient.client.initCrypto()));
return clients;
}
+1 -7
View File
@@ -154,12 +154,9 @@ describe("MatrixClient", function() {
});
// FIXME: We shouldn't be yanking _http like this.
client._http = [
"authedRequest", "authedRequestWithPrefix", "getContentUri",
"request", "requestWithPrefix", "uploadContent",
"authedRequest", "getContentUri", "request", "uploadContent",
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
client._http.authedRequest.andCall(httpReq);
client._http.authedRequestWithPrefix.andCall(httpReq);
client._http.requestWithPrefix.andCall(httpReq);
client._http.request.andCall(httpReq);
// set reasonable working defaults
@@ -181,9 +178,6 @@ describe("MatrixClient", function() {
client._http.authedRequest.andCall(function() {
return Promise.defer().promise;
});
client._http.authedRequestWithPrefix.andCall(function() {
return Promise.defer().promise;
});
});
it("should not POST /filter if a matching filter already exists", async function() {
+24 -13
View File
@@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -275,21 +276,11 @@ export class AutoDiscovery {
let isUrl = "";
if (wellknown["m.identity_server"]) {
// We prepare a failing identity server response to save lines later
// in this branch. Note that we also fail the homeserver check in the
// object because according to the spec we're supposed to FAIL_ERROR
// if *anything* goes wrong with the IS validation, including invalid
// format. This means we're supposed to stop discovery completely.
// in this branch.
const failingClientConfig = {
"m.homeserver": {
state: AutoDiscovery.FAIL_ERROR,
error: AutoDiscovery.ERROR_INVALID_IS,
// We'll provide the base_url that was previously valid for
// debugging purposes.
base_url: clientConfig["m.homeserver"].base_url,
},
"m.homeserver": clientConfig["m.homeserver"],
"m.identity_server": {
state: AutoDiscovery.FAIL_ERROR,
state: AutoDiscovery.FAIL_PROMPT,
error: AutoDiscovery.ERROR_INVALID_IS,
base_url: null,
},
@@ -429,6 +420,26 @@ export class AutoDiscovery {
return AutoDiscovery.fromDiscoveryConfig(wellknown.raw);
}
/**
* Gets the raw discovery client configuration for the given domain name.
* Should only be used if there's no validation to be done on the resulting
* object, otherwise use findClientConfig().
* @param {string} domain The domain to get the client config for.
* @returns {Promise<object>} Resolves to the domain's client config. Can
* be an empty object.
*/
static async getRawClientConfig(domain) {
if (!domain || typeof(domain) !== "string" || domain.length === 0) {
throw new Error("'domain' must be a string of non-zero length");
}
const response = await this._fetchWellKnownObject(
`https://${domain}/.well-known/matrix/client`,
);
if (!response) return {};
return response.raw || {};
}
/**
* Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and
* is suitable for the requirements laid out by .well-known auto discovery.
+624 -80
View File
@@ -1,6 +1,8 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -23,8 +25,23 @@ limitations under the License.
* @module base-apis
*/
import { SERVICE_TYPES } from './service-types';
import logger from './logger';
const httpApi = require("./http-api");
const utils = require("./utils");
const PushProcessor = require("./pushprocessor");
function termsUrlForService(serviceType, baseUrl) {
switch (serviceType) {
case SERVICE_TYPES.IS:
return baseUrl + httpApi.PREFIX_IDENTITY_V2 + '/terms';
case SERVICE_TYPES.IM:
return baseUrl + '/_matrix/integrations/v1/terms';
default:
throw new Error('Unsupported service type');
}
}
/**
* Low-level wrappers for the Matrix APIs
@@ -46,6 +63,15 @@ const utils = require("./utils");
*
* @param {string} opts.accessToken The access_token for this user.
*
* @param {IdentityServerProvider} [opts.identityServer]
* Optional. A provider object with one function `getAccessToken`, which is a
* callback that returns a Promise<String> of an identity access token to supply
* with identity requests. If the object is unset, no access token will be
* supplied.
* See also https://github.com/vector-im/riot-web/issues/10615 which seeks to
* replace the previous approach of manual access tokens params with this
* callback throughout the SDK.
*
* @param {Number=} opts.localTimeoutMs Optional. The default maximum amount of
* time to wait before timing out HTTP requests. If not specified, there is no
* timeout.
@@ -62,6 +88,7 @@ function MatrixBaseApis(opts) {
this.baseUrl = opts.baseUrl;
this.idBaseUrl = opts.idBaseUrl;
this.identityServer = opts.identityServer;
const httpOpts = {
baseUrl: opts.baseUrl,
@@ -100,6 +127,15 @@ MatrixBaseApis.prototype.getIdentityServerUrl = function(stripProto=false) {
return this.idBaseUrl;
};
/**
* Set the Identity Server URL of this client
* @param {string} url New Identity Server URL
*/
MatrixBaseApis.prototype.setIdentityServerUrl = function(url) {
this.idBaseUrl = utils.ensureNoTrailingSlash(url);
this._http.setIdBaseUrl(this.idBaseUrl);
};
/**
* Get the access token associated with this account.
* @return {?String} The access_token or null
@@ -391,9 +427,8 @@ MatrixBaseApis.prototype.deactivateAccount = function(auth, erase) {
body.erase = erase;
}
return this._http.authedRequestWithPrefix(
return this._http.authedRequest(
undefined, "POST", '/account/deactivate', undefined, body,
httpApi.PREFIX_R0,
);
};
@@ -462,8 +497,10 @@ MatrixBaseApis.prototype.fetchRelations =
$relationType: relationType,
$eventType: eventType,
});
const response = await this._http.authedRequestWithPrefix(
undefined, "GET", path, null, null, httpApi.PREFIX_UNSTABLE,
const response = await this._http.authedRequest(
undefined, "GET", path, null, null, {
prefix: httpApi.PREFIX_UNSTABLE,
},
);
return response;
};
@@ -956,10 +993,13 @@ MatrixBaseApis.prototype.roomInitialSync = function(roomId, limit, callback) {
* @param {string} rrEventId ID of the event tracked by the read receipt. This is here
* for convenience because the RR and the RM are commonly updated at the same time as
* each other. Optional.
* @param {object} opts Options for the read markers.
* @param {object} opts.hidden True to hide the read receipt from other users. <b>This
* property is currently unstable and may change in the future.</b>
* @return {module:client.Promise} Resolves: the empty object, {}.
*/
MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
function(roomId, rmEventId, rrEventId) {
function(roomId, rmEventId, rrEventId, opts) {
const path = utils.encodeUri("/rooms/$roomId/read_markers", {
$roomId: roomId,
});
@@ -967,6 +1007,7 @@ MatrixBaseApis.prototype.setRoomReadMarkersHttpRequest =
const content = {
"m.fully_read": rmEventId,
"m.read": rrEventId,
"m.hidden": Boolean(opts ? opts.hidden : false),
};
return this._http.authedRequest(
@@ -1298,10 +1339,16 @@ MatrixBaseApis.prototype.getThreePids = function(callback) {
};
/**
* Add a 3PID to your homeserver account and optionally bind it to an identity
* server as well. An identity server is required as part of the `creds` object.
*
* This API is deprecated, and you should instead use `addThreePidOnly`
* for homeservers that support it.
*
* @param {Object} creds
* @param {boolean} bind
* @param {module:client.callback} callback Optional.
* @return {module:client.Promise} Resolves: TODO
* @return {module:client.Promise} Resolves: on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
@@ -1315,6 +1362,75 @@ MatrixBaseApis.prototype.addThreePid = function(creds, bind, callback) {
);
};
/**
* Add a 3PID to your homeserver account. This API does not use an identity
* server, as the homeserver is expected to handle 3PID ownership validation.
*
* You can check whether a homeserver supports this API via
* `doesServerSupportSeparateAddAndBind`.
*
* @param {Object} data A object with 3PID validation data from having called
* `account/3pid/<medium>/requestToken` on the homeserver.
* @return {module:client.Promise} Resolves: on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.addThreePidOnly = async function(data) {
const path = "/account/3pid/add";
const prefix = await this.isVersionSupported("r0.6.0") ?
httpApi.PREFIX_R0 : httpApi.PREFIX_UNSTABLE;
return this._http.authedRequest(
undefined, "POST", path, null, data, { prefix },
);
};
/**
* Bind a 3PID for discovery onto an identity server via the homeserver. The
* identity server handles 3PID ownership validation and the homeserver records
* the new binding to track where all 3PIDs for the account are bound.
*
* You can check whether a homeserver supports this API via
* `doesServerSupportSeparateAddAndBind`.
*
* @param {Object} data A object with 3PID validation data from having called
* `validate/<medium>/requestToken` on the identity server. It should also
* contain `id_server` and `id_access_token` fields as well.
* @return {module:client.Promise} Resolves: on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.bindThreePid = async function(data) {
const path = "/account/3pid/bind";
const prefix = await this.isVersionSupported("r0.6.0") ?
httpApi.PREFIX_R0 : httpApi.PREFIX_UNSTABLE;
return this._http.authedRequest(
undefined, "POST", path, null, data, { prefix },
);
};
/**
* Unbind a 3PID for discovery on an identity server via the homeserver. The
* homeserver removes its record of the binding to keep an updated record of
* where all 3PIDs for the account are bound.
*
* @param {string} medium The threepid medium (eg. 'email')
* @param {string} address The threepid address (eg. 'bob@example.com')
* this must be as returned by getThreePids.
* @return {module:client.Promise} Resolves: on success
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.unbindThreePid = async function(medium, address) {
const path = "/account/3pid/unbind";
const data = {
medium,
address,
id_server: this.getIdentityServerUrl(true),
};
const prefix = await this.isVersionSupported("r0.6.0") ?
httpApi.PREFIX_R0 : httpApi.PREFIX_UNSTABLE;
return this._http.authedRequest(
undefined, "POST", path, null, data, { prefix },
);
};
/**
* @param {string} medium The threepid medium (eg. 'email')
* @param {string} address The threepid address (eg. 'bob@example.com')
@@ -1329,9 +1445,7 @@ MatrixBaseApis.prototype.deleteThreePid = function(medium, address) {
'medium': medium,
'address': address,
};
return this._http.authedRequestWithPrefix(
undefined, "POST", path, null, data, httpApi.PREFIX_UNSTABLE,
);
return this._http.authedRequest(undefined, "POST", path, null, data);
};
/**
@@ -1364,10 +1478,8 @@ MatrixBaseApis.prototype.setPassword = function(authDict, newPassword, callback)
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getDevices = function() {
const path = "/devices";
return this._http.authedRequestWithPrefix(
undefined, "GET", path, undefined, undefined,
httpApi.PREFIX_UNSTABLE,
return this._http.authedRequest(
undefined, 'GET', "/devices", undefined, undefined,
);
};
@@ -1384,11 +1496,7 @@ MatrixBaseApis.prototype.setDeviceDetails = function(device_id, body) {
$device_id: device_id,
});
return this._http.authedRequestWithPrefix(
undefined, "PUT", path, undefined, body,
httpApi.PREFIX_UNSTABLE,
);
return this._http.authedRequest(undefined, "PUT", path, undefined, body);
};
/**
@@ -1410,10 +1518,7 @@ MatrixBaseApis.prototype.deleteDevice = function(device_id, auth) {
body.auth = auth;
}
return this._http.authedRequestWithPrefix(
undefined, "DELETE", path, undefined, body,
httpApi.PREFIX_UNSTABLE,
);
return this._http.authedRequest(undefined, "DELETE", path, undefined, body);
};
/**
@@ -1431,10 +1536,8 @@ MatrixBaseApis.prototype.deleteMultipleDevices = function(devices, auth) {
body.auth = auth;
}
return this._http.authedRequestWithPrefix(
undefined, "POST", "/delete_devices", undefined, body,
httpApi.PREFIX_UNSTABLE,
);
const path = "/delete_devices";
return this._http.authedRequest(undefined, "POST", path, undefined, body);
};
@@ -1476,7 +1579,9 @@ MatrixBaseApis.prototype.setPusher = function(pusher, callback) {
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getPushRules = function(callback) {
return this._http.authedRequest(callback, "GET", "/pushrules/");
return this._http.authedRequest(callback, "GET", "/pushrules/").then(rules => {
return PushProcessor.rewriteDefaultRules(rules);
});
};
/**
@@ -1610,8 +1715,15 @@ MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
} else {
path = "/keys/upload";
}
return this._http.authedRequestWithPrefix(
callback, "POST", path, undefined, content, httpApi.PREFIX_UNSTABLE,
return this._http.authedRequest(callback, "POST", path, undefined, content);
};
MatrixBaseApis.prototype.uploadKeySignatures = function(content) {
return this._http.authedRequest(
undefined, "POST", '/keys/signatures/upload', undefined,
content, {
prefix: httpApi.PREFIX_UNSTABLE,
},
);
};
@@ -1647,10 +1759,7 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
content.device_keys[u] = {};
});
return this._http.authedRequestWithPrefix(
undefined, "POST", "/keys/query", undefined, content,
httpApi.PREFIX_UNSTABLE,
);
return this._http.authedRequest(undefined, "POST", "/keys/query", undefined, content);
};
/**
@@ -1678,10 +1787,8 @@ MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
query[deviceId] = key_algorithm;
}
const content = {one_time_keys: queries};
return this._http.authedRequestWithPrefix(
undefined, "POST", "/keys/claim", undefined, content,
httpApi.PREFIX_UNSTABLE,
);
const path = "/keys/claim";
return this._http.authedRequest(undefined, "POST", path, undefined, content);
};
/**
@@ -1700,21 +1807,53 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
to: newToken,
};
return this._http.authedRequestWithPrefix(
undefined, "GET", "/keys/changes", qps, undefined,
httpApi.PREFIX_UNSTABLE,
);
const path = "/keys/changes";
return this._http.authedRequest(undefined, "GET", path, qps, undefined);
};
MatrixBaseApis.prototype.uploadDeviceSigningKeys = function(auth, keys) {
const data = Object.assign({}, keys, {auth});
return this._http.authedRequest(
undefined, "POST", "/keys/device_signing/upload", undefined, data, {
prefix: httpApi.PREFIX_UNSTABLE,
},
);
};
// Identity Server Operations
// ==========================
/**
* Requests an email verification token directly from an Identity Server.
* Register with an Identity Server using the OpenID token from the user's
* Homeserver, which can be retrieved via
* {@link module:client~MatrixClient#getOpenIdToken}.
*
* Note that the Home Server offers APIs to proxy this API for specific
* situations, allowing for better feedback to the user.
* Note that the `/account/register` endpoint (as well as IS authentication in
* general) was added as part of the v2 API version.
*
* @param {object} hsOpenIdToken
* @return {module:client.Promise} Resolves: with object containing an Identity
* Server access token.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.registerWithIdentityServer = function(hsOpenIdToken) {
if (!this.idBaseUrl) {
throw new Error("No Identity Server base URL set");
}
const uri = this.idBaseUrl + httpApi.PREFIX_IDENTITY_V2 + "/account/register";
return this._http.requestOtherUrl(
undefined, "POST", uri,
null, hsOpenIdToken,
);
};
/**
* Requests an email verification token directly from an identity server.
*
* This API is used as part of binding an email for discovery on an identity
* server. The validation data that results should be passed to the
* `bindThreePid` method to complete the binding process.
*
* @param {string} email The email address to request a token for
* @param {string} clientSecret A secret binary string generated by the client.
@@ -1726,26 +1865,122 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
* @param {string} nextLink Optional If specified, the client will be redirected
* to this link after validation.
* @param {module:client.callback} callback Optional.
* @param {string} identityAccessToken The `access_token` field of the identity
* server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if No ID server is set
* @throws Error if no identity server is set
*/
MatrixBaseApis.prototype.requestEmailToken = function(email, clientSecret,
sendAttempt, nextLink, callback) {
MatrixBaseApis.prototype.requestEmailToken = async function(
email,
clientSecret,
sendAttempt,
nextLink,
callback,
identityAccessToken,
) {
const params = {
client_secret: clientSecret,
email: email,
send_attempt: sendAttempt,
next_link: nextLink,
};
return this._http.idServerRequest(
callback, "POST", "/validate/email/requestToken",
params, httpApi.PREFIX_IDENTITY_V1,
);
try {
const response = await this._http.idServerRequest(
undefined, "POST", "/validate/email/requestToken",
params, httpApi.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, httpApi.PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err);
throw err;
}
};
/**
* Submits an MSISDN token to the identity server
* Requests a MSISDN verification token directly from an identity server.
*
* This API is used as part of binding a MSISDN for discovery on an identity
* server. The validation data that results should be passed to the
* `bindThreePid` method to complete the binding process.
*
* @param {string} phoneCountry The ISO 3166-1 alpha-2 code for the country in
* which phoneNumber should be parsed relative to.
* @param {string} phoneNumber The phone number, in national or international
* format
* @param {string} clientSecret A secret binary string generated by the client.
* It is recommended this be around 16 ASCII characters.
* @param {number} sendAttempt If an identity server sees a duplicate request
* with the same sendAttempt, it will not send another SMS.
* To request another SMS to be sent, use a larger value for
* the sendAttempt param as was used in the previous request.
* @param {string} nextLink Optional If specified, the client will be redirected
* to this link after validation.
* @param {module:client.callback} callback Optional.
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: TODO
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if no identity server is set
*/
MatrixBaseApis.prototype.requestMsisdnToken = async function(
phoneCountry,
phoneNumber,
clientSecret,
sendAttempt,
nextLink,
callback,
identityAccessToken,
) {
const params = {
client_secret: clientSecret,
country: phoneCountry,
phone_number: phoneNumber,
send_attempt: sendAttempt,
next_link: nextLink,
};
try {
const response = await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/requestToken",
params, httpApi.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, httpApi.PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err);
throw err;
}
};
/**
* Submits a MSISDN token to the identity server
*
* This is used when submitting the code sent by SMS to a phone number.
* The ID server has an equivalent API for email but the js-sdk does
@@ -1755,46 +1990,323 @@ MatrixBaseApis.prototype.requestEmailToken = function(email, clientSecret,
* @param {string} sid The sid given in the response to requestToken
* @param {string} clientSecret A secret binary string generated by the client.
* This must be the same value submitted in the requestToken call.
* @param {string} token The token, as enetered by the user.
* @param {string} msisdnToken The MSISDN token, as enetered by the user.
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: Object, currently with no parameters.
* @return {module:http-api.MatrixError} Rejects: with an error response.
* @throws Error if No ID server is set
*/
MatrixBaseApis.prototype.submitMsisdnToken = function(sid, clientSecret, token) {
MatrixBaseApis.prototype.submitMsisdnToken = async function(
sid,
clientSecret,
msisdnToken,
identityAccessToken,
) {
const params = {
sid: sid,
client_secret: clientSecret,
token: token,
token: msisdnToken,
};
return this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, httpApi.PREFIX_IDENTITY_V1,
try {
return await this._http.idServerRequest(
undefined, "POST", "/validate/msisdn/submitToken",
params, httpApi.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, httpApi.PREFIX_IDENTITY_V1,
);
}
throw err;
}
};
/**
* Submits a MSISDN token to an arbitrary URL.
*
* This is used when submitting the code sent by SMS to a phone number in the
* newer 3PID flow where the homeserver validates 3PID ownership (as part of
* `requestAdd3pidMsisdnToken`). The homeserver response may include a
* `submit_url` to specify where the token should be sent, and this helper can
* be used to pass the token to this URL.
*
* @param {string} url The URL to submit the token to
* @param {string} sid The sid given in the response to requestToken
* @param {string} clientSecret A secret binary string generated by the client.
* This must be the same value submitted in the requestToken call.
* @param {string} msisdnToken The MSISDN token, as enetered by the user.
*
* @return {module:client.Promise} Resolves: Object, currently with no parameters.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.submitMsisdnTokenOtherUrl = function(
url,
sid,
clientSecret,
msisdnToken,
) {
const params = {
sid: sid,
client_secret: clientSecret,
token: msisdnToken,
};
return this._http.requestOtherUrl(
undefined, "POST", url, undefined, params,
);
};
/**
* Gets the V2 hashing information from the identity server. Primarily useful for
* lookups.
* @param {string} identityAccessToken The access token for the identity server.
* @returns {Promise<object>} The hashing information for the identity server.
*/
MatrixBaseApis.prototype.getIdentityHashDetails = function(identityAccessToken) {
return this._http.idServerRequest(
undefined, "GET", "/hash_details",
null, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
};
/**
* Performs a hashed lookup of addresses against the identity server. This is
* only supported on identity servers which have at least the version 2 API.
* @param {Array<Array<string,string>>} addressPairs An array of 2 element arrays.
* The first element of each pair is the address, the second is the 3PID medium.
* Eg: ["email@example.org", "email"]
* @param {string} identityAccessToken The access token for the identity server.
* @returns {Promise<Array<{address, mxid}>>} A collection of address mappings to
* found MXIDs. Results where no user could be found will not be listed.
*/
MatrixBaseApis.prototype.identityHashedLookup = async function(
addressPairs, // [["email@example.org", "email"], ["10005550000", "msisdn"]]
identityAccessToken,
) {
const params = {
// addresses: ["email@example.org", "10005550000"],
// algorithm: "sha256",
// pepper: "abc123"
};
// Get hash information first before trying to do a lookup
const hashes = await this.getIdentityHashDetails(identityAccessToken);
if (!hashes || !hashes['lookup_pepper'] || !hashes['algorithms']) {
throw new Error("Unsupported identity server: bad response");
}
params['pepper'] = hashes['lookup_pepper'];
const localMapping = {
// hashed identifier => plain text address
// For use in this function's return format
};
// When picking an algorithm, we pick the hashed over no hashes
if (hashes['algorithms'].includes('sha256')) {
// Abuse the olm hashing
const olmutil = new global.Olm.Utility();
params["addresses"] = addressPairs.map(p => {
const addr = p[0].toLowerCase(); // lowercase to get consistent hashes
const med = p[1].toLowerCase();
const hashed = olmutil.sha256(`${addr} ${med} ${params['pepper']}`)
.replace(/\+/g, '-').replace(/\//g, '_'); // URL-safe base64
// Map the hash to a known (case-sensitive) address. We use the case
// sensitive version because the caller might be expecting that.
localMapping[hashed] = p[0];
return hashed;
});
params["algorithm"] = "sha256";
} else if (hashes['algorithms'].includes('none')) {
params["addresses"] = addressPairs.map(p => {
const addr = p[0].toLowerCase(); // lowercase to get consistent hashes
const med = p[1].toLowerCase();
const unhashed = `${addr} ${med}`;
// Map the unhashed values to a known (case-sensitive) address. We use
// the case sensitive version because the caller might be expecting that.
localMapping[unhashed] = p[0];
return unhashed;
});
params["algorithm"] = "none";
} else {
throw new Error("Unsupported identity server: unknown hash algorithm");
}
const response = await this._http.idServerRequest(
undefined, "POST", "/lookup",
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
if (!response || !response['mappings']) return []; // no results
const foundAddresses = [/* {address: "plain@example.org", mxid} */];
for (const hashed of Object.keys(response['mappings'])) {
const mxid = response['mappings'][hashed];
const plainAddress = localMapping[hashed];
if (!plainAddress) {
throw new Error("Identity server returned more results than expected");
}
foundAddresses.push({address: plainAddress, mxid});
}
return foundAddresses;
};
/**
* Looks up the public Matrix ID mapping for a given 3rd party
* identifier from the Identity Server
*
* @param {string} medium The medium of the threepid, eg. 'email'
* @param {string} address The textual address of the threepid
* @param {module:client.callback} callback Optional.
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: A threepid mapping
* object or the empty object if no mapping
* exists
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.lookupThreePid = function(medium, address, callback) {
const params = {
medium: medium,
address: address,
};
return this._http.idServerRequest(
callback, "GET", "/lookup",
params, httpApi.PREFIX_IDENTITY_V1,
);
MatrixBaseApis.prototype.lookupThreePid = async function(
medium,
address,
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, httpApi.PREFIX_IDENTITY_V1,
);
}
if (callback) callback(err, undefined);
throw err;
}
};
/**
* Looks up the public Matrix ID mappings for multiple 3PIDs.
*
* @param {Array.<Array.<string>>} query Array of arrays containing
* [medium, address]
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: Lookup results from IS.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
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,
);
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,
]);
}
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,
httpApi.PREFIX_IDENTITY_V1, identityAccessToken,
);
}
throw err;
}
};
/**
* Get account info from the Identity Server. This is useful as a neutral check
* to verify that other APIs are likely to approve access by testing that the
* token is valid, terms have been agreed, etc.
*
* @param {string} identityAccessToken The `access_token` field of the Identity
* Server `/account/register` response (see {@link registerWithIdentityServer}).
*
* @return {module:client.Promise} Resolves: an object with account info.
* @return {module:http-api.MatrixError} Rejects: with an error response.
*/
MatrixBaseApis.prototype.getIdentityAccount = function(
identityAccessToken,
) {
return this._http.idServerRequest(
undefined, "GET", "/account",
undefined, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
);
};
// Direct-to-device messaging
// ==========================
@@ -1821,10 +2333,13 @@ MatrixBaseApis.prototype.sendToDevice = function(
messages: contentMap,
};
return this._http.authedRequestWithPrefix(
undefined, "PUT", path, undefined, body,
httpApi.PREFIX_UNSTABLE,
);
const targets = Object.keys(contentMap).reduce((obj, key) => {
obj[key] = Object.keys(contentMap[key]);
return obj;
}, {});
logger.log(`PUT ${path}`, targets);
return this._http.authedRequest(undefined, "PUT", path, undefined, body);
};
// Third party Lookup API
@@ -1836,9 +2351,8 @@ MatrixBaseApis.prototype.sendToDevice = function(
* @return {module:client.Promise} Resolves to the result object
*/
MatrixBaseApis.prototype.getThirdpartyProtocols = function() {
return this._http.authedRequestWithPrefix(
return this._http.authedRequest(
undefined, "GET", "/thirdparty/protocols", undefined, undefined,
httpApi.PREFIX_UNSTABLE,
).then((response) => {
// sanity check
if (!response || typeof(response) !== 'object') {
@@ -1863,10 +2377,7 @@ MatrixBaseApis.prototype.getThirdpartyLocation = function(protocol, params) {
$protocol: protocol,
});
return this._http.authedRequestWithPrefix(
undefined, "GET", path, params, undefined,
httpApi.PREFIX_UNSTABLE,
);
return this._http.authedRequest(undefined, "GET", path, params, undefined);
};
/**
@@ -1882,12 +2393,45 @@ MatrixBaseApis.prototype.getThirdpartyUser = function(protocol, params) {
$protocol: protocol,
});
return this._http.authedRequestWithPrefix(
undefined, "GET", path, params, undefined,
httpApi.PREFIX_UNSTABLE,
return this._http.authedRequest(undefined, "GET", path, params, undefined);
};
MatrixBaseApis.prototype.getTerms = function(serviceType, baseUrl) {
const url = termsUrlForService(serviceType, baseUrl);
return this._http.requestOtherUrl(
undefined, 'GET', url,
);
};
MatrixBaseApis.prototype.agreeToTerms = function(
serviceType, baseUrl, accessToken, termsUrls,
) {
const url = termsUrlForService(serviceType, baseUrl);
const headers = {
Authorization: "Bearer " + accessToken,
};
return this._http.requestOtherUrl(
undefined, 'POST', url, null, { user_accepts: termsUrls }, { headers },
);
};
/**
* Reports an event as inappropriate to the server, which may then notify the appropriate people.
* @param {string} roomId The room in which the event being reported is located.
* @param {string} eventId The event to report.
* @param {number} score The score to rate this content as where -100 is most offensive and 0 is inoffensive.
* @param {string} reason The reason the content is being reported. May be blank.
* @returns {module:client.Promise} Resolves to an empty object if successful
*/
MatrixBaseApis.prototype.reportEvent = function(roomId, eventId, score, reason) {
const path = utils.encodeUri("/rooms/$roomId/report/$eventId", {
$roomId: roomId,
$eventId: eventId,
});
return this._http.authedRequest(undefined, "POST", path, null, {score, reason});
};
/**
* MatrixBaseApis object
*/
+703 -105
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -47,7 +47,7 @@ module.exports = {
}
}
let serverAndMediaId = mxc.slice(6); // strips mxc://
let prefix = "/_matrix/media/v1/download/";
let prefix = "/_matrix/media/r0/download/";
const params = {};
if (width) {
@@ -62,7 +62,7 @@ module.exports = {
if (utils.keys(params).length > 0) {
// these are thumbnailing params so they probably want the
// thumbnailing API...
prefix = "/_matrix/media/v1/thumbnail/";
prefix = "/_matrix/media/r0/thumbnail/";
}
const fragmentOffset = serverAndMediaId.indexOf("#");
@@ -83,6 +83,7 @@ module.exports = {
* @param {Number} width The desired width of the image in pixels. Default: 96.
* @param {Number} height The desired height of the image in pixels. Default: 96.
* @return {string} The complete URL to the identicon.
* @deprecated This is no longer in the specification.
*/
getIdenticonUri: function(baseUrl, identiconString, width, height) {
if (!identiconString) {
@@ -99,7 +100,7 @@ module.exports = {
height: height,
};
const path = utils.encodeUri("/_matrix/media/v1/identicon/$ident", {
const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
$ident: identiconString,
});
return baseUrl + path +
+487
View File
@@ -0,0 +1,487 @@
/*
Copyright 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
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.
*/
/**
* Cross signing methods
* @module crypto/CrossSigning
*/
import {pkSign, pkVerify} from './olmlib';
import {EventEmitter} from 'events';
import logger from '../logger';
function publicKeyFromKeyInfo(keyInfo) {
return Object.entries(keyInfo.keys)[0];
}
export class CrossSigningInfo extends EventEmitter {
/**
* Information about a user's cross-signing keys
*
* @class
*
* @param {string} userId the user that the information is about
* @param {object} callbacks Callbacks used to interact with the app
* Requires getCrossSigningKey and saveCrossSigningKeys
*/
constructor(userId, callbacks) {
super();
// you can't change the userId
Object.defineProperty(this, 'userId', {
enumerable: true,
value: userId,
});
this._callbacks = callbacks || {};
this.keys = {};
this.firstUse = true;
}
/**
* Calls the app callback to ask for a private key
* @param {string} type The key type ("master", "self_signing", or "user_signing")
* @param {Uint8Array} expectedPubkey The matching public key or undefined to use
* the stored public key for the given key type.
*/
async getCrossSigningKey(type, expectedPubkey) {
if (!this._callbacks.getCrossSigningKey) {
throw new Error("No getCrossSigningKey callback supplied");
}
if (expectedPubkey === undefined) {
expectedPubkey = this.getId(type);
}
const privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey);
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];
}
}
static fromStorage(obj, userId) {
const res = new CrossSigningInfo(userId);
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
res[prop] = obj[prop];
}
}
return res;
}
toStorage() {
return {
keys: this.keys,
firstUse: this.firstUse,
};
}
hasKeys() {
return Object.keys(this.keys).length > 0;
}
/**
* Get the ID used to identify the user
*
* @param {string} type The type of key to get the ID of. One of "master",
* "self_signing", or "user_signing". Defaults to "master".
*
* @return {string} the ID
*/
getId(type) {
type = type || "master";
if (!this.keys[type]) return null;
const keyInfo = this.keys[type];
return publicKeyFromKeyInfo(keyInfo)[1];
}
async resetKeys(level) {
if (!this._callbacks.saveCrossSigningKeys) {
throw new Error("No saveCrossSigningKeys callback supplied");
}
// If we're resetting the master key, we reset all keys
if (
level === undefined ||
level & CrossSigningLevel.MASTER ||
!this.keys.master
) {
level = (
CrossSigningLevel.MASTER |
CrossSigningLevel.USER_SIGNING |
CrossSigningLevel.SELF_SIGNING
);
} else if (level === 0) {
return;
}
const privateKeys = {};
const keys = {};
let masterSigning;
let masterPub;
try {
if (level & CrossSigningLevel.MASTER) {
masterSigning = new global.Olm.PkSigning();
privateKeys.master = masterSigning.generate_seed();
masterPub = masterSigning.init_with_seed(privateKeys.master);
keys.master = {
user_id: this.userId,
usage: ['master'],
keys: {
['ed25519:' + masterPub]: masterPub,
},
};
} else {
[masterPub, masterSigning] = await this.getCrossSigningyKey("master");
}
if (level & CrossSigningLevel.SELF_SIGNING) {
const sskSigning = new global.Olm.PkSigning();
try {
privateKeys.self_signing = sskSigning.generate_seed();
const sskPub = sskSigning.init_with_seed(privateKeys.self_signing);
keys.self_signing = {
user_id: this.userId,
usage: ['self_signing'],
keys: {
['ed25519:' + sskPub]: sskPub,
},
};
pkSign(keys.self_signing, masterSigning, this.userId, masterPub);
} finally {
sskSigning.free();
}
}
if (level & CrossSigningLevel.USER_SIGNING) {
const uskSigning = new global.Olm.PkSigning();
try {
privateKeys.user_signing = uskSigning.generate_seed();
const uskPub = uskSigning.init_with_seed(privateKeys.user_signing);
keys.user_signing = {
user_id: this.userId,
usage: ['user_signing'],
keys: {
['ed25519:' + uskPub]: uskPub,
},
};
pkSign(keys.user_signing, masterSigning, this.userId, masterPub);
} finally {
uskSigning.free();
}
}
Object.assign(this.keys, keys);
this._callbacks.saveCrossSigningKeys(privateKeys);
} finally {
if (masterSigning) {
masterSigning.free();
}
}
}
setKeys(keys) {
const signingKeys = {};
if (keys.master) {
if (keys.master.user_id !== this.userId) {
const error = "Mismatched user ID " + keys.master.user_id +
" in master key from " + this.userId;
logger.error(error);
throw new Error(error);
}
if (!this.keys.master) {
// this is the first key we've seen, so first-use is true
this.firstUse = true;
} else if (publicKeyFromKeyInfo(keys.master)[1] !== this.getId()) {
// this is a different key, so first-use is false
this.firstUse = false;
} // otherwise, same key, so no change
signingKeys.master = keys.master;
} else if (this.keys.master) {
signingKeys.master = this.keys.master;
} else {
throw new Error("Tried to set cross-signing keys without a master key");
}
const masterKey = publicKeyFromKeyInfo(signingKeys.master)[1];
// verify signatures
if (keys.user_signing) {
if (keys.user_signing.user_id !== this.userId) {
const error = "Mismatched user ID " + keys.master.user_id +
" in user_signing key from " + this.userId;
logger.error(error);
throw new Error(error);
}
try {
pkVerify(keys.user_signing, masterKey, this.userId);
} catch (e) {
logger.error("invalid signature on user-signing key");
// FIXME: what do we want to do here?
throw e;
}
}
if (keys.self_signing) {
if (keys.self_signing.user_id !== this.userId) {
const error = "Mismatched user ID " + keys.master.user_id +
" in self_signing key from " + this.userId;
logger.error(error);
throw new Error(error);
}
try {
pkVerify(keys.self_signing, masterKey, this.userId);
} catch (e) {
logger.error("invalid signature on self-signing key");
// FIXME: what do we want to do here?
throw e;
}
}
// if everything checks out, then save the keys
if (keys.master) {
this.keys.master = keys.master;
// if the master key is set, then the old self-signing and
// user-signing keys are obsolete
delete this.keys.self_signing;
delete this.keys.user_signing;
}
if (keys.self_signing) {
this.keys.self_signing = keys.self_signing;
}
if (keys.user_signing) {
this.keys.user_signing = keys.user_signing;
}
}
async signObject(data, type) {
if (!this.keys[type]) {
throw new Error(
"Attempted to sign with " + type + " key but no such key present",
);
}
const [pubkey, signing] = await this.getCrossSigningKey(type);
try {
pkSign(data, signing, this.userId, pubkey);
return data;
} finally {
signing.free();
}
}
async signUser(key) {
if (!this.keys.user_signing) {
return;
}
return this.signObject(key.keys.master, "user_signing");
}
async signDevice(userId, device) {
if (userId !== this.userId) {
throw new Error(
`Trying to sign ${userId}'s device; can only sign our own device`,
);
}
if (!this.keys.self_signing) {
return;
}
return this.signObject(
{
algorithms: device.algorithms,
keys: device.keys,
device_id: device.deviceId,
user_id: userId,
}, "self_signing",
);
}
/**
* Check whether a given user is trusted.
*
* @param {CrossSigningInfo} userCrossSigning Cross signing info for user
*
* @returns {UserTrustLevel}
*/
checkUserTrust(userCrossSigning) {
// if we're checking our own key, then it's trusted if the master key
// and self-signing key match
if (this.userId === userCrossSigning.userId
&& this.getId() && this.getId() === userCrossSigning.getId()
&& this.getId("self_signing")
&& this.getId("self_signing") === userCrossSigning.getId("self_signing")
) {
return new UserTrustLevel(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);
}
let userTrusted;
const userMaster = userCrossSigning.keys.master;
const uskId = this.getId('user_signing');
try {
pkVerify(userMaster, uskId, this.userId);
userTrusted = true;
} catch (e) {
userTrusted = false;
}
return new UserTrustLevel(userTrusted, userCrossSigning.firstUse);
}
/**
* Check whether a given device is trusted.
*
* @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
*
* @returns {DeviceTrustLevel}
*/
checkDeviceTrust(userCrossSigning, device, localTrust) {
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);
}
const deviceObj = deviceToObject(device, userCrossSigning.userId);
try {
// if we can verify the user's SSK from their master key...
pkVerify(userSSK, userCrossSigning.getId(), userCrossSigning.userId);
// ...and this device's key from their SSK...
pkVerify(
deviceObj, publicKeyFromKeyInfo(userSSK)[1], userCrossSigning.userId,
);
// ...then we trust this device as much as far as we trust the user
return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust);
} catch (e) {
return new DeviceTrustLevel(false, false, localTrust);
}
}
}
function deviceToObject(device, userId) {
return {
algorithms: device.algorithms,
keys: device.keys,
device_id: device.deviceId,
user_id: userId,
signatures: device.signatures,
};
}
export const CrossSigningLevel = {
MASTER: 4,
USER_SIGNING: 2,
SELF_SIGNING: 1,
};
/**
* Represents the ways in which we trust a user
*/
export class UserTrustLevel {
constructor(crossSigningVerified, tofu) {
this._crossSigningVerified = crossSigningVerified;
this._tofu = tofu;
}
/**
* @returns {bool} true if this user is verified via any means
*/
isVerified() {
return this.isCrossSigningVerified();
}
/**
* @returns {bool} true if this user is verified via cross signing
*/
isCrossSigningVerified() {
return this._crossSigningVerified;
}
/**
* @returns {bool} true if this user's key is trusted on first use
*/
isTofu() {
return this._tofu;
}
}
/**
* Represents the ways in which we trust a device
*/
export class DeviceTrustLevel {
constructor(crossSigningVerified, tofu, localVerified) {
this._crossSigningVerified = crossSigningVerified;
this._tofu = tofu;
this._localVerified = localVerified;
}
static fromUserTrustLevel(userTrustLevel, localVerified) {
return new DeviceTrustLevel(
userTrustLevel._crossSigningVerified,
userTrustLevel._tofu,
localVerified,
);
}
/**
* @returns {bool} true if this device is verified via any means
*/
isVerified() {
return this.isCrossSigningVerified() || this.isLocallyVerified();
}
/**
* @returns {bool} true if this device is verified via cross signing
*/
isCrossSigningVerified() {
return this._crossSigningVerified;
}
/**
* @returns {bool} true if this device is verified locally
*/
isLocallyVerified() {
return this._localVerified;
}
/**
* @returns {bool} true if this device is trusted from a user's key
* that is trusted on first use
*/
isTofu() {
return this._tofu;
}
}
+87 -20
View File
@@ -1,6 +1,7 @@
/*
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -23,9 +24,11 @@ limitations under the License.
*/
import Promise from 'bluebird';
import {EventEmitter} from 'events';
import logger from '../logger';
import DeviceInfo from './deviceinfo';
import {CrossSigningInfo} from './CrossSigning';
import olmlib from './olmlib';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
@@ -60,8 +63,10 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
/**
* @alias module:crypto/DeviceList
*/
export default class DeviceList {
export default class DeviceList extends EventEmitter {
constructor(baseApis, cryptoStore, olmDevice) {
super();
this._cryptoStore = cryptoStore;
// userId -> {
@@ -71,6 +76,11 @@ export default class DeviceList {
// }
this._devices = {};
// userId -> {
// [key info]
// }
this._crossSigningInfo = {};
// map of identity keys to the user who owns it
this._userByIdentityKey = {};
@@ -111,6 +121,7 @@ export default class DeviceList {
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
this._devices = deviceData ? deviceData.devices : {},
this._ssks = deviceData ? deviceData.self_signing_keys || {} : {};
this._deviceTrackingStatus = deviceData ?
deviceData.trackingStatus : {};
this._syncToken = deviceData ? deviceData.syncToken : null;
@@ -201,6 +212,7 @@ export default class DeviceList {
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
this._cryptoStore.storeEndToEndDeviceData({
devices: this._devices,
self_signing_keys: this._ssks,
trackingStatus: this._deviceTrackingStatus,
syncToken: this._syncToken,
}, txn);
@@ -334,6 +346,17 @@ export default class DeviceList {
return this._devices[userId];
}
getStoredCrossSigningForUser(userId) {
if (!this._crossSigningInfo[userId]) return null;
return CrossSigningInfo.fromStorage(this._crossSigningInfo[userId], userId);
}
storeCrossSigningForUser(userId, info) {
this._crossSigningInfo[userId] = info;
this._dirty = true;
}
/**
* Get the stored keys for a single device
*
@@ -561,6 +584,10 @@ export default class DeviceList {
}
}
setRawStoredCrossSigningForUser(userId, info) {
this._crossSigningInfo[userId] = info;
}
/**
* Fire off download update requests for the given users, and update the
* device list tracking status for them, and the
@@ -624,6 +651,7 @@ export default class DeviceList {
}
});
this.saveIfDirty();
this.emit("crypto.devicesUpdated", users);
};
return prom;
@@ -724,6 +752,9 @@ class DeviceListUpdateSerialiser {
downloadUsers, opts,
).then((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
// (https://github.com/vector-im/riot-web/issues/3158)
@@ -733,7 +764,13 @@ class DeviceListUpdateSerialiser {
let prom = Promise.resolve();
for (const userId of downloadUsers) {
prom = prom.delay(5).then(() => {
return this._processQueryResponseForUser(userId, dk[userId]);
return this._processQueryResponseForUser(
userId, dk[userId], {
master: masterKeys[userId],
self_signing: ssks[userId],
user_signing: usks[userId],
},
);
});
}
@@ -757,30 +794,58 @@ class DeviceListUpdateSerialiser {
return deferred.promise;
}
async _processQueryResponseForUser(userId, response) {
logger.log('got keys for ' + userId + ':', response);
async _processQueryResponseForUser(
userId, dkResponse, crossSigningResponse, sskResponse,
) {
logger.log('got device keys for ' + userId + ':', dkResponse);
logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse);
// map from deviceid -> deviceinfo for this user
const userStore = {};
const devs = this._deviceList.getRawStoredDevicesForUser(userId);
if (devs) {
Object.keys(devs).forEach((deviceId) => {
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
userStore[deviceId] = d;
{
// map from deviceid -> deviceinfo for this user
const userStore = {};
const devs = this._deviceList.getRawStoredDevicesForUser(userId);
if (devs) {
Object.keys(devs).forEach((deviceId) => {
const d = DeviceInfo.fromStorage(devs[deviceId], deviceId);
userStore[deviceId] = d;
});
}
await _updateStoredDeviceKeysForUser(
this._olmDevice, userId, userStore, dkResponse || {},
);
// put the updates into the object that will be returned as our results
const storage = {};
Object.keys(userStore).forEach((deviceId) => {
storage[deviceId] = userStore[deviceId].toStorage();
});
this._deviceList._setRawStoredDevicesForUser(userId, storage);
}
await _updateStoredDeviceKeysForUser(
this._olmDevice, userId, userStore, response || {},
);
// now do the same for the cross-signing keys
{
// FIXME: should we be ignoring empty cross-signing responses, or
// should we be dropping the keys?
if (crossSigningResponse
&& (crossSigningResponse.master || crossSigningResponse.self_signing
|| crossSigningResponse.user_signing)) {
const crossSigning
= this._deviceList.getStoredCrossSigningForUser(userId)
|| new CrossSigningInfo(userId);
// put the updates into thr object that will be returned as our results
const storage = {};
Object.keys(userStore).forEach((deviceId) => {
storage[deviceId] = userStore[deviceId].toStorage();
});
crossSigning.setKeys(crossSigningResponse);
this._deviceList._setRawStoredDevicesForUser(userId, storage);
this._deviceList.setRawStoredCrossSigningForUser(
userId, crossSigning.toStorage(),
);
// NB. Unlike most events in the js-sdk, this one is internal to the
// js-sdk and is not re-emitted
this._deviceList.emit('userCrossSigningUpdated', userId);
}
}
}
}
@@ -854,6 +919,7 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
}
const unsigned = deviceResult.unsigned || {};
const signatures = deviceResult.signatures || {};
try {
await olmlib.verifySignature(_olmDevice, deviceResult, userId, deviceId, signKey);
@@ -886,5 +952,6 @@ async function _storeDeviceKeys(_olmDevice, userStore, deviceResult) {
deviceStore.keys = deviceResult.keys || {};
deviceStore.algorithms = deviceResult.algorithms || [];
deviceStore.unsigned = unsigned;
deviceStore.signatures = signatures;
return true;
}
+17 -3
View File
@@ -462,7 +462,7 @@ OlmDevice.prototype.createInboundSession = async function(
*/
OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityKey) {
if (this._sessionsInProgress[theirDeviceIdentityKey]) {
logger.log("waiting for session to be created");
logger.log("waiting for olm session to be created");
try {
await this._sessionsInProgress[theirDeviceIdentityKey];
} catch (e) {
@@ -543,7 +543,7 @@ OlmDevice.prototype.getSessionIdForDevice = async function(
*/
OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey, nowait) {
if (this._sessionsInProgress[deviceIdentityKey] && !nowait) {
logger.log("waiting for session to be created");
logger.log("waiting for olm session to be created");
try {
await this._sessionsInProgress[deviceIdentityKey];
} catch (e) {
@@ -594,6 +594,11 @@ OlmDevice.prototype.encryptMessage = async function(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
const sessionDesc = sessionInfo.session.describe();
logger.log(
"encryptMessage: Olm Session ID " + sessionId + " to " +
theirDeviceIdentityKey + ": " + sessionDesc,
);
res = sessionInfo.session.encrypt(payloadString);
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
});
@@ -621,6 +626,11 @@ OlmDevice.prototype.decryptMessage = async function(
'readwrite', [IndexedDBCryptoStore.STORE_SESSIONS],
(txn) => {
this._getSession(theirDeviceIdentityKey, sessionId, txn, (sessionInfo) => {
const sessionDesc = sessionInfo.session.describe();
logger.log(
"decryptMessage: Olm Session ID " + sessionId + " from " +
theirDeviceIdentityKey + ": " + sessionDesc,
);
payloadString = sessionInfo.session.decrypt(messageType, ciphertext);
sessionInfo.lastReceivedMessageTs = Date.now();
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
@@ -730,6 +740,8 @@ OlmDevice.prototype.createOutboundGroupSession = function() {
OlmDevice.prototype.encryptGroupMessage = function(sessionId, payloadString) {
const self = this;
logger.log(`encrypting msg with megolm session ${sessionId}`);
checkPayloadLength(payloadString);
return this._getOutboundGroupSession(sessionId, function(session) {
@@ -876,7 +888,9 @@ OlmDevice.prototype.addInboundGroupSession = async function(
<= session.first_known_index()) {
// existing session has lower index (i.e. can
// decrypt more), so keep it
logger.log("Keeping existing session");
logger.log(
`Keeping existing megolm session ${sessionId}`,
);
return;
}
}
+529
View File
@@ -0,0 +1,529 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import {EventEmitter} from 'events';
import logger from '../logger';
import olmlib from './olmlib';
import { randomString } from '../randomstring';
import { keyFromPassphrase } from './key_passphrase';
import { encodeRecoveryKey } from './recoverykey';
import { pkVerify } from './olmlib';
export const SECRET_STORAGE_ALGORITHM_V1 = "m.secret_storage.v1.curve25519-aes-sha2";
/**
* Implements Secure Secret Storage and Sharing (MSC1946)
* @module crypto/SecretStorage
*/
export default class SecretStorage extends EventEmitter {
constructor(baseApis, cryptoCallbacks, crossSigningInfo) {
super();
this._baseApis = baseApis;
this._cryptoCallbacks = cryptoCallbacks;
this._crossSigningInfo = crossSigningInfo;
this._requests = {};
this._incomingRequests = {};
}
getDefaultKeyId() {
const defaultKeyEvent = this._baseApis.getAccountData(
'm.secret_storage.default_key',
);
if (!defaultKeyEvent) return null;
return defaultKeyEvent.getContent().key;
}
setDefaultKeyId(keyId) {
return new Promise((resolve) => {
const listener = (ev) => {
if (
ev.getType() === 'm.secret_storage.default_key' &&
ev.getContent().key === keyId
) {
this._baseApis.removeListener('accountData', listener);
resolve();
}
};
this._baseApis.on('accountData', listener);
this._baseApis.setAccountData(
'm.secret_storage.default_key',
{ key: keyId },
);
});
}
/**
* Add a key for encrypting secrets.
*
* @param {string} algorithm the algorithm used by the key.
* @param {object} opts the options for the algorithm. The properties used
* depend on the algorithm given. This object may be modified to pass
* information back about the key.
* @param {string} [keyId] the ID of the key. If not given, a random
* ID will be generated.
*
* @return {string} the ID of the key
*/
async addKey(algorithm, opts, keyId) {
const keyData = {algorithm};
if (!opts) opts = {};
if (opts.name) {
keyData.name = opts.name;
}
switch (algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
{
const decryption = new global.Olm.PkDecryption();
try {
if (opts.passphrase) {
const key = await keyFromPassphrase(opts.passphrase);
keyData.passphrase = {
algorithm: "m.pbkdf2",
iterations: key.iterations,
salt: key.salt,
};
opts.encodedkey = encodeRecoveryKey(key.key);
keyData.pubkey = decryption.init_with_private_key(key.key);
} else if (opts.privkey) {
keyData.pubkey = decryption.init_with_private_key(opts.privkey);
opts.encodedkey = encodeRecoveryKey(opts.privkey);
} else {
keyData.pubkey = decryption.generate_key();
opts.encodedkey = encodeRecoveryKey(decryption.get_private_key());
}
} finally {
decryption.free();
}
break;
}
default:
throw new Error(`Unknown key algorithm ${opts.algorithm}`);
}
if (!keyId) {
do {
keyId = randomString(32);
} while (this._baseApis.getAccountData(`m.secret_storage.key.${keyId}`));
}
await this._crossSigningInfo.signObject(keyData, 'master');
await this._baseApis.setAccountData(
`m.secret_storage.key.${keyId}`, keyData,
);
return keyId;
}
// TODO: need a function to get all the secret keys
/**
* Store an encrypted secret on the server
*
* @param {string} name The name of the secret
* @param {string} secret The secret contents.
* @param {Array} keys The IDs of the keys to use to encrypt the secret
* or null/undefined to use the default key.
*/
async store(name, secret, keys) {
const encrypted = {};
if (!keys) {
const defaultKeyId = this.getDefaultKeyId();
if (!defaultKeyId) {
throw new Error("No keys specified and no default key present");
}
keys = [defaultKeyId];
}
if (keys.length === 0) {
throw new Error("Zero keys given to encrypt with!");
}
for (const keyId of keys) {
// get key information from key storage
const keyInfo = this._baseApis.getAccountData(
"m.secret_storage.key." + keyId,
);
if (!keyInfo) {
throw new Error("Unknown key: " + keyId);
}
const keyInfoContent = keyInfo.getContent();
// check signature of key info
pkVerify(
keyInfoContent,
this._crossSigningInfo.getId('master'),
this._crossSigningInfo.userId,
);
// encrypt secret, based on the algorithm
switch (keyInfoContent.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
{
const encryption = new global.Olm.PkEncryption();
try {
encryption.set_recipient_key(keyInfoContent.pubkey);
encrypted[keyId] = encryption.encrypt(secret);
} finally {
encryption.free();
}
break;
}
default:
logger.warn("unknown algorithm for secret storage key " + keyId
+ ": " + keyInfoContent.algorithm);
// do nothing if we don't understand the encryption algorithm
}
}
// save encrypted secret
await this._baseApis.setAccountData(name, {encrypted});
}
/**
* Get a secret from storage.
*
* @param {string} name the name of the secret
*
* @return {string} the contents of the secret
*/
async get(name) {
const secretInfo = this._baseApis.getAccountData(name);
if (!secretInfo) {
return;
}
const secretContent = secretInfo.getContent();
if (!secretContent.encrypted) {
throw new Error("Content is not encrypted!");
}
// get possible keys to decrypt
const keys = {};
for (const keyId of Object.keys(secretContent.encrypted)) {
// get key information from key storage
const keyInfo = this._baseApis.getAccountData(
"m.secret_storage.key." + keyId,
).getContent();
const encInfo = secretContent.encrypted[keyId];
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
&& encInfo.ephemeral) {
keys[keyId] = keyInfo;
}
break;
default:
// do nothing if we don't understand the encryption algorithm
}
}
let keyId;
let decryption;
try {
// fetch private key from app
[keyId, decryption] = await this._getSecretStorageKey(keys);
// decrypt secret
const encInfo = secretContent.encrypted[keyId];
switch (keys[keyId].algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
return decryption.decrypt(
encInfo.ephemeral, encInfo.mac, encInfo.ciphertext,
);
}
} finally {
if (decryption) decryption.free();
}
}
/**
* Check if a secret is stored on the server.
*
* @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
*/
isStored(name, checkKey) {
// check if secret exists
const secretInfo = this._baseApis.getAccountData(name);
if (!secretInfo) {
return false;
}
if (checkKey === undefined) checkKey = true;
const secretContent = secretInfo.getContent();
if (!secretContent.encrypted) {
return false;
}
// check if secret is encrypted by a known/trusted secret and
// encryption looks sane
for (const keyId of Object.keys(secretContent.encrypted)) {
// get key information from key storage
const keyInfo = this._baseApis.getAccountData(
"m.secret_storage.key." + keyId,
).getContent();
const encInfo = secretContent.encrypted[keyId];
if (checkKey) {
pkVerify(
keyInfo,
this._crossSigningInfo.getId('master'),
this._crossSigningInfo.userId,
);
}
switch (keyInfo.algorithm) {
case SECRET_STORAGE_ALGORITHM_V1:
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
&& encInfo.ephemeral) {
return true;
}
break;
default:
// do nothing if we don't understand the encryption algorithm
}
}
return false;
}
/**
* Request a secret from another device
*
* @param {string} name the name of the secret to request
* @param {string[]} devices the devices to request the secret from
*
* @return {string} the contents of the secret
*/
request(name, devices) {
const requestId = this._baseApis.makeTxnId();
const requestControl = this._requests[requestId] = {
devices,
};
const promise = new Promise((resolve, reject) => {
requestControl.resolve = resolve;
requestControl.reject = reject;
});
const cancel = (reason) => {
// send cancellation event
const cancelData = {
action: "request_cancellation",
requesting_device_id: this._baseApis.deviceId,
request_id: requestId,
};
const toDevice = {};
for (const device of devices) {
toDevice[device] = cancelData;
}
this._baseApis.sendToDevice("m.secret.request", {
[this._baseApis.getUserId()]: toDevice,
});
// and reject the promise so that anyone waiting on it will be
// notified
requestControl.reject(new Error(reason || "Cancelled"));
};
// send request to devices
const requestData = {
name,
action: "request",
requesting_device_id: this._baseApis.deviceId,
request_id: requestId,
};
const toDevice = {};
for (const device of devices) {
toDevice[device] = requestData;
}
this._baseApis.sendToDevice("m.secret.request", {
[this._baseApis.getUserId()]: toDevice,
});
return {
request_id: requestId,
promise,
cancel,
};
}
async _onRequestReceived(event) {
const sender = event.getSender();
const content = event.getContent();
if (sender !== this._baseApis.getUserId()
|| !(content.name && content.action
&& content.requesting_device_id && content.request_id)) {
// ignore requests from anyone else, for now
return;
}
const deviceId = content.requesting_device_id;
// check if it's a cancel
if (content.action === "request_cancellation") {
if (this._incomingRequests[deviceId]
&& this._incomingRequests[deviceId][content.request_id]) {
logger.info("received request cancellation for secret (" + sender
+ ", " + deviceId + ", " + content.request_id + ")");
this.baseApis.emit("crypto.secrets.requestCancelled", {
user_id: sender,
device_id: deviceId,
request_id: content.request_id,
});
}
} else if (content.action === "request") {
if (deviceId === this._baseApis.deviceId) {
// no point in trying to send ourself the secret
return;
}
// check if we have the secret
logger.info("received request for secret (" + sender
+ ", " + deviceId + ", " + content.request_id + ")");
if (!this._cryptoCallbacks.onSecretRequested) {
return;
}
const secret = await this._cryptoCallbacks.onSecretRequested({
user_id: sender,
device_id: deviceId,
request_id: content.request_id,
name: content.name,
device_trust: this._baseApis.checkDeviceTrust(sender, deviceId),
});
if (secret) {
const payload = {
type: "m.secret.send",
content: {
request_id: content.request_id,
secret: secret,
},
};
const encryptedContent = {
algorithm: olmlib.OLM_ALGORITHM,
sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key,
ciphertext: {},
};
await olmlib.ensureOlmSessionsForDevices(
this._baseApis._crypto._olmDevice,
this._baseApis,
{
[sender]: [
await this._baseApis.getStoredDevice(sender, deviceId),
],
},
);
await olmlib.encryptMessageForDevice(
encryptedContent.ciphertext,
this._baseApis.getUserId(),
this._baseApis.deviceId,
this._baseApis._crypto._olmDevice,
sender,
this._baseApis._crypto.getStoredDevice(sender, deviceId),
payload,
);
const contentMap = {
[sender]: {
[deviceId]: encryptedContent,
},
};
this._baseApis.sendToDevice("m.room.encrypted", contentMap);
}
}
}
_onSecretReceived(event) {
if (event.getSender() !== this._baseApis.getUserId()) {
// we shouldn't be receiving secrets from anyone else, so ignore
// because someone could be trying to send us bogus data
return;
}
const content = event.getContent();
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
// we requested from
const deviceInfo = this._baseApis._crypto._deviceList.getDeviceByIdentityKey(
olmlib.OLM_ALGORITHM,
event.getSenderKey(),
);
if (!deviceInfo) {
logger.log(
"secret share from unknown device with key", event.getSenderKey(),
);
return;
}
if (!requestControl.devices.includes(deviceInfo.deviceId)) {
logger.log("unsolicited secret share from device", deviceInfo.deviceId);
return;
}
requestControl.resolve(content.secret);
}
}
async _getSecretStorageKey(keys) {
if (!this._cryptoCallbacks.getSecretStorageKey) {
throw new Error("No getSecretStorageKey callback supplied");
}
const returned = await Promise.resolve(
this._cryptoCallbacks.getSecretStorageKey({keys}),
);
if (!returned) {
throw new Error("getSecretStorageKey callback returned falsey");
}
if (returned.length < 2) {
throw new Error("getSecretStorageKey callback returned invalid data");
}
const [keyId, privateKey] = returned;
if (!keys[keyId]) {
throw new Error("App returned unknown key from getSecretStorageKey!");
}
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];
}
default:
throw new Error("Unknown key type: " + keys[keyId].algorithm);
}
}
}
+18 -17
View File
@@ -23,7 +23,7 @@ limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../../../src/logger';
import logger from '../../logger';
const utils = require("../../utils");
const olmlib = require("../olmlib");
@@ -104,7 +104,7 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
}
if (!devicesInRoom.hasOwnProperty(userId)) {
logger.log("Starting new session because we shared with " + userId);
logger.log("Starting new megolm session because we shared with " + userId);
return true;
}
@@ -115,7 +115,7 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
if (!devicesInRoom[userId].hasOwnProperty(deviceId)) {
logger.log(
"Starting new session because we shared with " +
"Starting new megolm session because we shared with " +
userId + ":" + deviceId,
);
return true;
@@ -200,6 +200,8 @@ MegolmEncryption.prototype._ensureOutboundSession = function(devicesInRoom) {
if (!session) {
logger.log(`Starting new megolm session for room ${self._roomId}`);
session = await self._prepareNewSession();
logger.log(`Started new megolm session ${session.sessionId} ` +
`for room ${self._roomId}`);
self._outboundSessions[session.sessionId] = session;
}
@@ -278,7 +280,7 @@ MegolmEncryption.prototype._prepareNewSession = async function() {
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
logger.log("Failed to back up group session", e);
logger.log("Failed to back up megolm session", e);
});
}
@@ -440,19 +442,19 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
) {
const obSessionInfo = this._outboundSessions[sessionId];
if (!obSessionInfo) {
logger.debug("Session ID " + sessionId + " not found: not re-sharing keys");
logger.debug(`megolm session ${sessionId} not found: not re-sharing keys`);
return;
}
// The chain index of the key we previously sent this device
if (obSessionInfo.sharedWithDevices[userId] === undefined) {
logger.debug("Session ID " + sessionId + " never shared with user " + userId);
logger.debug(`megolm session ${sessionId} never shared with user ${userId}`);
return;
}
const sentChainIndex = obSessionInfo.sharedWithDevices[userId][device.deviceId];
if (sentChainIndex === undefined) {
logger.debug(
"Session ID " + sessionId + " never shared with device " +
"megolm session ID " + sessionId + " never shared with device " +
userId + ":" + device.deviceId,
);
return;
@@ -466,7 +468,7 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
if (!key) {
logger.warn(
"No outbound session key found for " + sessionId + ": not re-sharing keys",
`No inbound session key found for megolm ${sessionId}: not re-sharing keys`,
);
return;
}
@@ -513,9 +515,8 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
[device.deviceId]: encryptedContent,
},
});
logger.debug(
`Re-shared key for session ${sessionId} with ${userId}:${device.deviceId}`,
);
logger.debug(`Re-shared key for megolm session ${sessionId} ` +
`with ${userId}:${device.deviceId}`);
};
/**
@@ -550,10 +551,10 @@ MegolmEncryption.prototype._shareKeyWithDevices = async function(session, device
await this._encryptAndSendKeysToDevices(
session, key.chain_index, userDeviceMaps[i], payload,
);
logger.log(`Completed megolm keyshare in ${this._roomId} `
+ `(slice ${i + 1}/${userDeviceMaps.length})`);
logger.log(`Completed megolm keyshare for ${session.sessionId} `
+ `in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`);
} catch (e) {
logger.log(`megolm keyshare in ${this._roomId} `
logger.log(`megolm keyshare for ${session.sessionId} in ${this._roomId} `
+ `(slice ${i + 1}/${userDeviceMaps.length}) failed`);
throw e;
@@ -922,7 +923,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
keysClaimed = event.getKeysClaimed();
}
logger.log(`Adding key for megolm session ${senderKey}|${sessionId}`);
logger.log(`Received and adding key for megolm session ${senderKey}|${sessionId}`);
return this._olmDevice.addInboundGroupSession(
content.room_id, senderKey, forwardingKeyChain, sessionId,
content.session_key, keysClaimed,
@@ -955,7 +956,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
logger.log("Failed to back up group session", e);
logger.log("Failed to back up megolm session", e);
});
}
}).catch((e) => {
@@ -1088,7 +1089,7 @@ MegolmDecryption.prototype.importRoomKey = function(session) {
).catch((e) => {
// This throws if the upload failed, but this is fine
// since it will have written it to the db and will retry.
logger.log("Failed to back up group session", e);
logger.log("Failed to back up megolm session", e);
});
}
// have another go at decrypting events sent with this session.
+2
View File
@@ -56,6 +56,7 @@ function DeviceInfo(deviceId) {
this.verified = DeviceVerification.UNVERIFIED;
this.known = false;
this.unsigned = {};
this.signatures = {};
}
/**
@@ -88,6 +89,7 @@ DeviceInfo.prototype.toStorage = function() {
verified: this.verified,
known: this.known,
unsigned: this.unsigned,
signatures: this.signatures,
};
};
+723 -44
View File
@@ -2,6 +2,7 @@
Copyright 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018-2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -24,6 +25,7 @@ limitations under the License.
const anotherjson = require('another-json');
import Promise from 'bluebird';
import {EventEmitter} from 'events';
import ReEmitter from '../ReEmitter';
import logger from '../logger';
const utils = require("../utils");
@@ -34,6 +36,8 @@ const DeviceInfo = require("./deviceinfo");
const DeviceVerification = DeviceInfo.DeviceVerification;
const DeviceList = require('./DeviceList').default;
import { randomString } from '../randomstring';
import { CrossSigningInfo, UserTrustLevel, DeviceTrustLevel } from './CrossSigning';
import SecretStorage from './SecretStorage';
import OutgoingRoomKeyRequestManager from './OutgoingRoomKeyRequestManager';
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
@@ -65,6 +69,30 @@ export function isCryptoAvailable() {
return Boolean(global.Olm);
}
/* subscribes to timeline events / to_device events for SAS verification */
function listenForEvents(client, roomId, listener) {
let isEncrypted = false;
if (roomId) {
isEncrypted = client.isRoomEncrypted(roomId);
}
if (isEncrypted) {
client.on("Event.decrypted", listener);
}
client.on("event", listener);
let subscribed = true;
return function() {
if (subscribed) {
if (isEncrypted) {
client.off("Event.decrypted", listener);
}
client.off("event", listener);
subscribed = false;
}
return null;
};
}
const MIN_FORCE_SESSION_INTERVAL_MS = 60 * 60 * 1000;
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
@@ -100,6 +128,10 @@ const KEY_BACKUP_KEYS_PER_REQUEST = 200;
*/
export default function Crypto(baseApis, sessionStore, userId, deviceId,
clientStore, cryptoStore, roomList, verificationMethods) {
this._onDeviceListUserCrossSigningUpdated =
this._onDeviceListUserCrossSigningUpdated.bind(this);
this._reEmitter = new ReEmitter(this);
this._baseApis = baseApis;
this._sessionStore = sessionStore;
this._userId = userId;
@@ -138,6 +170,12 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
this._deviceList = new DeviceList(
baseApis, cryptoStore, this._olmDevice,
);
// XXX: This isn't removed at any point, but then none of the event listeners
// this class sets seem to be removed at any point... :/
this._deviceList.on(
'userCrossSigningUpdated', this._onDeviceListUserCrossSigningUpdated,
);
this._reEmitter.reEmit(this._deviceList, ["crypto.devicesUpdated"]);
// the last time we did a check for the number of one-time-keys on the
// server.
@@ -188,6 +226,14 @@ export default function Crypto(baseApis, sessionStore, userId, deviceId,
this._lastNewSessionForced = {};
this._verificationTransactions = new Map();
this._crossSigningInfo = new CrossSigningInfo(
userId, this._baseApis._cryptoCallbacks,
);
this._secretStorage = new SecretStorage(
baseApis, this._baseApis._cryptoCallbacks, this._crossSigningInfo,
);
}
utils.inherits(Crypto, EventEmitter);
@@ -236,10 +282,422 @@ Crypto.prototype.init = async function() {
this._deviceList.saveIfDirty();
}
await this._cryptoStore.doTxn(
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._cryptoStore.getCrossSigningKeys(txn, (keys) => {
if (keys) {
this._crossSigningInfo.setKeys(keys);
}
});
},
);
// make sure we are keeping track of our own devices
// (this is important for key backups & things)
this._deviceList.startTrackingDeviceList(this._userId);
logger.log("Crypto: checking for key backup...");
this._checkAndStartKeyBackup();
};
Crypto.prototype.addSecretKey = function(algorithm, opts, keyID) {
return this._secretStorage.addKey(algorithm, opts, keyID);
};
Crypto.prototype.storeSecret = function(name, secret, keys) {
return this._secretStorage.store(name, secret, keys);
};
Crypto.prototype.getSecret = function(name) {
return this._secretStorage.get(name);
};
Crypto.prototype.isSecretStored = function(name, checkKey) {
return this._secretStorage.isStored(name, checkKey);
};
Crypto.prototype.requestSecret = function(name, devices) {
if (!devices) {
devices = Object.keys(this._deviceList.getRawStoredDevicesForUser(this._userId));
}
return this._secretStorage.request(name, devices);
};
Crypto.prototype.getDefaultSecretStorageKeyId = function() {
return this._secretStorage.getDefaultKeyId();
};
Crypto.prototype.setDefaultSecretStorageKeyId = function(k) {
return this._secretStorage.setDefaultKeyId(k);
};
/**
* Checks that a given private key matches a given public key
* This can be used by the getCrossSigningKey callback to verify that the
* private key it is about to supply is the one that was requested.
*
* @param {Uint8Array} privateKey The private key
* @param {Uint8Array} expectedPublicKey The public key supplied by the getCrossSigningKey callback
* @returns {boolean} true if the key matches, otherwise false
*/
Crypto.prototype.checkPrivateKey = function(privateKey, expectedPublicKey) {
let signing = null;
try {
signing = new global.Olm.PkSigning();
const gotPubkey = signing.init_with_seed(privateKey);
// make sure it agrees with the given pubkey
return gotPubkey === expectedPublicKey;
} finally {
if (signing) signing.free();
}
};
/**
* Check whether we already have cross-signing keys for the current user.
*
* @return {boolean} Whether we have keys.
*/
Crypto.prototype.doesCrossSigningHaveKeys = function() {
return this._crossSigningInfo.hasKeys();
};
/**
* Generate new cross-signing keys.
*
* @param {object} authDict Auth data to supply for User-Interactive auth.
* @param {CrossSigningLevel} [level] the level of cross-signing to reset. New
* keys will be created for the given level and below. Defaults to
* regenerating all keys.
*/
Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) {
await this._crossSigningInfo.resetKeys(level);
await this._signObject(this._crossSigningInfo.keys.master);
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys);
},
);
// send keys to server
const keys = {};
for (const [name, key] of Object.entries(this._crossSigningInfo.keys)) {
keys[name + "_key"] = key;
}
await this._baseApis.uploadDeviceSigningKeys(authDict || {}, keys);
this._baseApis.emit("crossSigning.keysChanged", {});
// sign the current device with the new key, and upload to the server
const device = this._deviceList.getStoredDevice(this._userId, this._deviceId);
const signedDevice = await this._crossSigningInfo.signDevice(this._userId, device);
await this._baseApis.uploadKeySignatures({
[this._userId]: {
[this._deviceId]: signedDevice,
},
});
// check all users for signatures
// FIXME: do this in batches
const users = {};
for (const [userId, crossSigningInfo]
of Object.entries(this._deviceList._crossSigningInfo)) {
const upgradeInfo = await this._checkForDeviceVerificationUpgrade(
userId, CrossSigningInfo.fromStorage(crossSigningInfo, userId),
);
if (upgradeInfo) {
users[userId] = upgradeInfo;
}
}
const shouldUpgradeCb = (
this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications
);
if (Object.keys(users).length > 0 && shouldUpgradeCb) {
try {
const usersToUpgrade = await shouldUpgradeCb({users: users});
if (usersToUpgrade) {
for (const userId of usersToUpgrade) {
if (userId in users) {
await this._baseApis.setDeviceVerified(
userId, users[userId].crossSigningInfo.getId(),
);
}
}
}
} catch (e) {
logger.log(
"shouldUpgradeDeviceVerifications threw an error: not upgrading", e,
);
}
}
};
/**
* Check if a user's cross-signing key is a candidate for upgrading from device
* verification.
*
* @param {string} userId the user whose cross-signing information is to be checked
* @param {object} crossSigningInfo the cross-signing information to check
*/
Crypto.prototype._checkForDeviceVerificationUpgrade = async function(
userId, crossSigningInfo,
) {
// only upgrade if this is the first cross-signing key that we've seen for
// them, and if their cross-signing key isn't already verified
const trustLevel = this._crossSigningInfo.checkUserTrust(crossSigningInfo);
if (crossSigningInfo.firstUse && !trustLevel.verified) {
const devices = this._deviceList.getRawStoredDevicesForUser(userId);
const deviceIds = await this._checkForValidDeviceSignature(
userId, crossSigningInfo.keys.master, devices,
);
if (deviceIds.length) {
return {
devices: deviceIds.map(
deviceId => DeviceInfo.fromStorage(devices[deviceId], deviceId),
),
crossSigningInfo,
};
}
}
};
/**
* Check if the cross-signing key is signed by a verified device.
*
* @param {string} userId the user ID whose key is being checked
* @param {object} key the key that is being checked
* @param {object} devices the user's devices. Should be a map from device ID
* to device info
*/
Crypto.prototype._checkForValidDeviceSignature = async function(userId, key, devices) {
const deviceIds = [];
if (devices && key.signatures && key.signatures[userId]) {
for (const signame of Object.keys(key.signatures[userId])) {
const [, deviceId] = signame.split(':', 2);
if (deviceId in devices
&& devices[deviceId].verified === DeviceVerification.VERIFIED) {
try {
await olmlib.verifySignature(
this._olmDevice,
key,
userId,
deviceId,
devices[deviceId].keys[signame],
);
deviceIds.push(deviceId);
} catch (e) {}
}
}
}
return deviceIds;
};
/**
* Get the user's cross-signing key ID.
*
* @param {string} [type=master] The type of key to get the ID of. One of
* "master", "self_signing", or "user_signing". Defaults to "master".
*
* @returns {string} the key ID
*/
Crypto.prototype.getCrossSigningId = function(type) {
return this._crossSigningInfo.getId(type);
};
/**
* Get the cross signing information for a given user.
*
* @param {string} userId the user ID to get the cross-signing info for.
*
* @returns {CrossSigningInfo} the cross signing informmation for the user.
*/
Crypto.prototype.getStoredCrossSigningForUser = function(userId) {
return this._deviceList.getStoredCrossSigningForUser(userId);
};
/**
* Check whether a given user is trusted.
*
* @param {string} userId The ID of the user to check.
*
* @returns {UserTrustLevel}
*/
Crypto.prototype.checkUserTrust = function(userId) {
const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId);
if (!userCrossSigning) {
return new UserTrustLevel(false, false);
}
return this._crossSigningInfo.checkUserTrust(userCrossSigning);
};
/**
* Check whether a given device is trusted.
*
* @param {string} userId The ID of the user whose devices is to be checked.
* @param {string} deviceId The ID of the device to check
*
* @returns {DeviceTrustLevel}
*/
Crypto.prototype.checkDeviceTrust = function(userId, deviceId) {
const device = this._deviceList.getStoredDevice(userId, deviceId);
const trustedLocally = device && device.isVerified();
const userCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId);
if (device && userCrossSigning) {
return this._crossSigningInfo.checkDeviceTrust(
userCrossSigning, device, trustedLocally,
);
} else {
return new DeviceTrustLevel(false, false, trustedLocally);
}
};
/*
* Event handler for DeviceList's userNewDevices event
*/
Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) {
if (userId === this._userId) {
// An update to our own cross-signing key.
// Get the new key first:
const newCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId);
const seenPubkey = newCrossSigning ? newCrossSigning.getId() : null;
const currentPubkey = this._crossSigningInfo.getId();
const changed = currentPubkey !== seenPubkey;
if (currentPubkey && seenPubkey && !changed) {
// If it's not changed, just make sure everything is up to date
await this.checkOwnCrossSigningTrust();
} else {
this.emit("crossSigning.keysChanged", {});
// We'll now be in a state where cross-signing on the account is not trusted
// because our locally stored cross-signing keys will not match the ones
// on the server for our account. The app must call checkOwnCrossSigningTrust()
// to fix this.
// XXX: Do we need to do something to emit events saying every device has become
// untrusted?
}
} else {
await this._checkDeviceVerifications(userId);
this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId));
}
};
/*
* Check the copy of our cross-signing key that we have in the device list and
* see if we can get the private key. If so, mark it as trusted.
*/
Crypto.prototype.checkOwnCrossSigningTrust = async function() {
const userId = this._userId;
// If we see an update to our own master key, check it against the master
// key we have and, if it matches, mark it as verified
// First, get the new cross-signing info
const newCrossSigning = this._deviceList.getStoredCrossSigningForUser(userId);
if (!newCrossSigning) {
logger.error(
"Got cross-signing update event for user " + userId +
" but no new cross-signing information found!",
);
return;
}
const seenPubkey = newCrossSigning.getId();
const changed = this._crossSigningInfo.getId() !== seenPubkey;
if (changed) {
// try to get the private key if the master key changed
logger.info("Got new master key", seenPubkey);
let signing = null;
try {
const ret = await this._crossSigningInfo.getCrossSigningKey(
'master', seenPubkey,
);
signing = ret[1];
} finally {
signing.free();
}
logger.info("Got matching private key from callback for new public master key");
}
const oldSelfSigningId = this._crossSigningInfo.getId("self_signing");
const oldUserSigningId = this._crossSigningInfo.getId("user_signing");
// Update the version of our keys in our cross-signing object and the local store
this._crossSigningInfo.setKeys(newCrossSigning.keys);
await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => {
this._cryptoStore.storeCrossSigningKeys(txn, this._crossSigningInfo.keys);
},
);
const keySignatures = {};
if (oldSelfSigningId !== newCrossSigning.getId("self_signing")) {
logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
const device = this._deviceList.getStoredDevice(this._userId, this._deviceId);
const signedDevice = await this._crossSigningInfo.signDevice(
this._userId, device,
);
keySignatures[this._deviceId] = signedDevice;
}
if (oldUserSigningId !== newCrossSigning.getId("user_signing")) {
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
}
if (changed) {
await this._signObject(this._crossSigningInfo.keys.master);
keySignatures[this._crossSigningInfo.getId()]
= this._crossSigningInfo.keys.master;
}
if (Object.keys(keySignatures).length) {
await this._baseApis.uploadKeySignatures({[this._userId]: keySignatures});
}
this.emit("userTrustStatusChanged", userId, this.checkUserTrust(userId));
// Now we may be able to trust our key backup
await this.checkKeyBackup();
// FIXME: if we previously trusted the backup, should we automatically sign
// the backup with the new key (if not already signed)?
};
/**
* Check if the master key is signed by a verified device, and if so, prompt
* the application to mark it as verified.
*
* @param {string} userId the user ID whose key should be checked
*/
Crypto.prototype._checkDeviceVerifications = async function(userId) {
if (this._crossSigningInfo.keys.user_signing) {
const crossSigningInfo = this._deviceList.getStoredCrossSigningForUser(userId);
if (crossSigningInfo) {
const upgradeInfo = await this._checkForDeviceVerificationUpgrade(
userId, crossSigningInfo,
);
const shouldUpgradeCb = (
this._baseApis._cryptoCallbacks.shouldUpgradeDeviceVerifications
);
if (upgradeInfo && shouldUpgradeCb) {
const usersToUpgrade = await shouldUpgradeCb({
users: {
[userId]: upgradeInfo,
},
});
if (usersToUpgrade.includes(userId)) {
await this._baseApis.setDeviceVerified(
userId, crossSigningInfo.getId(),
);
}
}
}
}
};
/**
* Check the server for an active key backup and
* if one is present and has a valid signature from
@@ -362,7 +820,35 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
logger.log("Ignoring unknown signature type: " + keyIdParts[0]);
continue;
}
// Could be an SSK but just say this is the device ID for backwards compat
const sigInfo = { deviceId: keyIdParts[1] }; // XXX: is this how we're supposed to get the device ID?
// first check to see if it's from our cross-signing key
const crossSigningId = this._crossSigningInfo.getId();
if (crossSigningId === keyId) {
sigInfo.cross_signing_key = crossSigningId;
try {
await olmlib.verifySignature(
this._olmDevice,
backupInfo.auth_data,
this._userId,
sigInfo.deviceId,
crossSigningId,
);
sigInfo.valid = true;
} catch (e) {
logger.warning(
"Bad signature from cross signing key " + crossSigningId, e,
);
sigInfo.valid = false;
}
ret.sigs.push(sigInfo);
continue;
}
// Now look for a sig from a device
// At some point this can probably go away and we'll just support
// it being signed by the SSK
const device = this._deviceList.getStoredDevice(
this._userId, sigInfo.deviceId,
);
@@ -394,10 +880,15 @@ Crypto.prototype.isKeyBackupTrusted = async function(backupInfo) {
ret.sigs.push(sigInfo);
}
ret.usable = (
ret.sigs.some((s) => s.valid && s.device.isVerified()) ||
ret.trusted_locally
);
ret.usable = ret.sigs.some((s) => {
return (
s.valid && (
(s.device && s.device.isVerified()) ||
(s.cross_signing_key)
)
);
});
ret.usable |= ret.trusted_locally;
return ret;
};
@@ -493,7 +984,7 @@ Crypto.prototype.uploadDeviceKeys = function() {
};
return crypto._signObject(deviceKeys).then(() => {
crypto._baseApis.uploadKeysRequest({
return crypto._baseApis.uploadKeysRequest({
device_keys: deviceKeys,
}, {
// for now, we set the device id explicitly, as we may not be using the
@@ -721,6 +1212,36 @@ Crypto.prototype.saveDeviceList = function(delay) {
Crypto.prototype.setDeviceVerification = async function(
userId, deviceId, verified, blocked, known,
) {
// get rid of any `undefined`s here so we can just check
// for null rather than null or undefined
if (verified === undefined) verified = null;
if (blocked === undefined) blocked = null;
if (known === undefined) known = null;
// Check if the 'device' is actually a cross signing key
// The js-sdk's verification treats cross-signing keys as devices
// and so uses this method to mark them verified.
const xsk = this._deviceList.getStoredCrossSigningForUser(userId);
if (xsk && xsk.getId() === deviceId) {
if (blocked !== null || known !== null) {
throw new Error("Cannot set blocked or known for a cross-signing key");
}
if (!verified) {
throw new Error("Cannot set a cross-signing key as unverified");
}
const device = await this._crossSigningInfo.signUser(xsk);
if (device) {
await this._baseApis.uploadKeySignatures({
[userId]: {
[deviceId]: device,
},
});
// This will emit events when it comes back down the sync
// (we could do local echo to speed things up)
}
return device;
}
const devices = this._deviceList.getRawStoredDevicesForUser(userId);
if (!devices || !devices[deviceId]) {
throw new Error("Unknown device " + userId + ":" + deviceId);
@@ -742,7 +1263,7 @@ Crypto.prototype.setDeviceVerification = async function(
}
let knownStatus = dev.known;
if (known !== null && known !== undefined) {
if (known !== null) {
knownStatus = known;
}
@@ -752,10 +1273,165 @@ Crypto.prototype.setDeviceVerification = async function(
this._deviceList.storeDevicesForUser(userId, devices);
this._deviceList.saveIfDirty();
}
return DeviceInfo.fromStorage(dev, deviceId);
// do cross-signing
if (verified && userId === this._userId) {
const device = await this._crossSigningInfo.signDevice(
userId, DeviceInfo.fromStorage(dev, deviceId),
);
if (device) {
await this._baseApis.uploadKeySignatures({
[userId]: {
[deviceId]: device,
},
});
// XXX: we'll need to wait for the device list to be updated
}
}
const deviceObj = DeviceInfo.fromStorage(dev, deviceId);
this.emit("deviceVerificationChanged", userId, deviceId, deviceObj);
return deviceObj;
};
function verificationEventHandler(target, userId, roomId, eventId) {
return function(event) {
// listen for events related to this verification
if (event.getRoomId() !== roomId
|| event.getSender() !== userId) {
return;
}
// ignore events that haven't been decrypted yet.
// we also listen for undecrypted events, just in case
// the other side would be sending unencrypted events in an e2ee room
if (event.getType() === "m.room.encrypted") {
return;
}
const relatesTo = event.getRelation();
if (!relatesTo
|| !relatesTo.rel_type
|| relatesTo.rel_type !== "m.reference"
|| !relatesTo.event_id
|| relatesTo.event_id !== eventId) {
return;
}
// the event seems to be related to this verification, so pass it on to
// the verification handler
target.handleEvent(event);
};
}
Crypto.prototype.requestVerificationDM = async function(userId, roomId, methods) {
let methodMap;
if (methods) {
methodMap = new Map();
for (const method of methods) {
if (typeof method === "string") {
methodMap.set(method, defaultVerificationMethods[method]);
} else if (method.NAME) {
methodMap.set(method.NAME, method);
}
}
} else {
methodMap = this._baseApis._crypto._verificationMethods;
}
let eventId = undefined;
const listenPromise = new Promise((_resolve, _reject) => {
const listener = (event) => {
// listen for events related to this verification
if (event.getRoomId() !== roomId
|| event.getSender() !== userId) {
return;
}
const relatesTo = event.getRelation();
if (!relatesTo || !relatesTo.rel_type
|| relatesTo.rel_type !== "m.reference"
|| !relatesTo.event_id
|| relatesTo.event_id !== eventId) {
return;
}
const content = event.getContent();
// the event seems to be related to this verification
switch (event.getType()) {
case "m.key.verification.start": {
const verifier = new (methodMap.get(content.method))(
this._baseApis, userId, content.from_device, eventId,
roomId, event,
);
verifier.handler = verificationEventHandler(
verifier, userId, roomId, eventId,
);
// this handler gets removed when the verification finishes
// (see the verify method of crypto/verification/Base.js)
const subscription =
listenForEvents(this._baseApis, roomId, verifier.handler);
verifier.setEventsSubscription(subscription);
resolve(verifier);
break;
}
case "m.key.verification.cancel": {
reject(event);
break;
}
}
};
let initialResponseSubscription =
listenForEvents(this._baseApis, roomId, listener);
const resolve = (...args) => {
if (initialResponseSubscription) {
initialResponseSubscription = initialResponseSubscription();
}
_resolve(...args);
};
const reject = (...args) => {
if (initialResponseSubscription) {
initialResponseSubscription = initialResponseSubscription();
}
_reject(...args);
};
});
const res = await this._baseApis.sendEvent(
roomId, "m.room.message",
{
body: this._baseApis.getUserId() + " is requesting to verify " +
"your key, but your client does not support in-chat key " +
"verification. You will need to use legacy key " +
"verification to verify keys.",
msgtype: "m.key.verification.request",
to: userId,
from_device: this._baseApis.getDeviceId(),
methods: [...methodMap.keys()],
},
);
eventId = res.event_id;
return listenPromise;
};
Crypto.prototype.acceptVerificationDM = function(event, Method) {
if (typeof(Method) === "string") {
Method = defaultVerificationMethods[Method];
}
const content = event.getContent();
const verifier = new Method(
this._baseApis, event.getSender(), content.from_device, event.getId(),
event.getRoomId(),
);
verifier.handler = verificationEventHandler(
verifier, event.getSender(), event.getRoomId(), event.getId(),
);
const subscription = listenForEvents(
this._baseApis, event.getRoomId(), verifier.handler);
verifier.setEventsSubscription(subscription);
return verifier;
};
Crypto.prototype.requestVerification = function(userId, methods, devices) {
if (!methods) {
// .keys() returns an iterator, so we need to explicitly turn it into an array
@@ -803,20 +1479,7 @@ Crypto.prototype.beginKeyVerification = function(
this._verificationTransactions.set(userId, new Map());
}
transactionId = transactionId || randomString(32);
if (method instanceof Array) {
if (method.length !== 2
|| !this._verificationMethods.has(method[0])
|| !this._verificationMethods.has(method[1])) {
throw newUnknownMethodError();
}
/*
return new TwoPartVerification(
this._verificationMethods[method[0]],
this._verificationMethods[method[1]],
userId, deviceId, transactionId,
);
*/
} else if (this._verificationMethods.has(method)) {
if (this._verificationMethods.has(method)) {
const verifier = new (this._verificationMethods.get(method))(
this._baseApis, userId, deviceId, transactionId,
);
@@ -951,6 +1614,15 @@ Crypto.prototype.forceDiscardSession = function(roomId) {
* the device query is always inhibited as the members are not tracked.
*/
Crypto.prototype.setRoomEncryption = async function(roomId, config, inhibitDeviceQuery) {
// ignore crypto events with no algorithm defined
// This will happen if a crypto event is redacted before we fetch the room state
// It would otherwise just throw later as an unknown algorithm would, but we may
// as well catch this here
if (!config.algorithm) {
logger.log("Ignoring setRoomEncryption with no algorithm");
return;
}
// if state is being replayed from storage, we might already have a configuration
// for this room as they are persisted as well.
// We just need to make sure the algorithm is initialized in this case.
@@ -1288,6 +1960,18 @@ Crypto.prototype.backupGroupSession = async function(
* upload in the background as soon as possible.
*/
Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() {
await this.flagAllGroupSessionsForBackup();
// Schedule keys to upload in the background as soon as possible.
this.scheduleKeyBackupSend(0 /* maxDelay */);
};
/**
* Marks all group sessions as needing to be backed up without scheduling
* them to upload in the background.
* @returns {Promise<int>} Resolves to the number of sessions requiring a backup.
*/
Crypto.prototype.flagAllGroupSessionsForBackup = async function() {
await this._cryptoStore.doTxn(
'readwrite',
[
@@ -1305,9 +1989,7 @@ Crypto.prototype.scheduleAllGroupSessionsForBackup = async function() {
const remaining = await this._cryptoStore.countSessionsNeedingBackup();
this.emit("crypto.keyBackupSessionsRemaining", remaining);
// Schedule keys to upload in the background as soon as possible.
this.scheduleKeyBackupSend(0 /* maxDelay */);
return remaining;
};
/* eslint-disable valid-jsdoc */ //https://github.com/eslint/eslint/issues/7307
@@ -1485,6 +2167,8 @@ Crypto.prototype.onSyncWillProcess = async function(syncData) {
// at which point we'll start tracking all the users of that room.
logger.log("Initial sync performed - resetting device tracking state");
this._deviceList.stopTrackingAllDeviceLists();
// we always track our own device list (for key backups etc)
this._deviceList.startTrackingDeviceList(this._userId);
this._roomDeviceTrackingState = {};
}
};
@@ -1593,11 +2277,18 @@ Crypto.prototype._getTrackedE2eRooms = function() {
Crypto.prototype._onToDeviceEvent = function(event) {
try {
logger.log(`received to_device ${event.getType()} from: ` +
`${event.getSender()} id: ${event.getId()}`);
if (event.getType() == "m.room_key"
|| event.getType() == "m.forwarded_room_key") {
this._onRoomKeyEvent(event);
} else if (event.getType() == "m.room_key_request") {
this._onRoomKeyRequestEvent(event);
} else if (event.getType() === "m.secret.request") {
this._secretStorage._onRequestReceived(event);
} else if (event.getType() === "m.secret.send") {
this._secretStorage._onSecretReceived(event);
} else if (event.getType() === "m.key.verification.request") {
this._onKeyVerificationRequest(event);
} else if (event.getType() === "m.key.verification.start") {
@@ -1807,22 +2498,6 @@ Crypto.prototype._onKeyVerificationStart = function(event) {
transaction_id: content.transactionId,
}));
return;
} else if (content.next_method) {
if (!this._verificationMethods.has(content.next_method)) {
cancel(newUnknownMethodError({
transaction_id: content.transactionId,
}));
return;
} else {
/* TODO:
const verification = new TwoPartVerification(
this._verificationMethods[content.method],
this._verificationMethods[content.next_method],
userId, deviceId,
);
this.emit(verification.event_type, verification);
this.emit(verification.first.event_type, verification);*/
}
} else {
const verifier = new (this._verificationMethods.get(content.method))(
this._baseApis, sender, deviceId, content.transaction_id,
@@ -1877,8 +2552,6 @@ Crypto.prototype._onKeyVerificationStart = function(event) {
handler.request.resolve(verifier);
}
} else {
// FIXME: make sure we're in a two-part verification, and the start matches the second part
}
}
this._baseApis.emit("crypto.verification.start", verifier);
@@ -2274,11 +2947,17 @@ Crypto.prototype._getRoomDecryptor = function(roomId, algorithm) {
* @param {Object} obj Object to which we will add a 'signatures' property
*/
Crypto.prototype._signObject = async function(obj) {
const sigs = {};
sigs[this._userId] = {};
const sigs = obj.signatures || {};
const unsigned = obj.unsigned;
delete obj.signatures;
delete obj.unsigned;
sigs[this._userId] = sigs[this._userId] || {};
sigs[this._userId]["ed25519:" + this._deviceId] =
await this._olmDevice.sign(anotherjson.stringify(obj));
obj.signatures = sigs;
if (unsigned !== undefined) obj.unsigned = unsigned;
};
@@ -1,5 +1,6 @@
/*
Copyright 2018 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -18,13 +19,11 @@ import { randomString } from '../randomstring';
const DEFAULT_ITERATIONS = 500000;
export async function keyForExistingBackup(backupData, password) {
export async function keyFromAuthData(authData, password) {
if (!global.Olm) {
throw new Error("Olm is not available");
}
const authData = backupData.auth_data;
if (!authData.private_key_salt || !authData.private_key_iterations) {
throw new Error(
"Salt and/or iterations not found: " +
@@ -33,12 +32,12 @@ export async function keyForExistingBackup(backupData, password) {
}
return await deriveKey(
password, backupData.auth_data.private_key_salt,
backupData.auth_data.private_key_iterations,
password, authData.private_key_salt,
authData.private_key_iterations,
);
}
export async function keyForNewBackup(password) {
export async function keyFromPassphrase(password) {
if (!global.Olm) {
throw new Error("Olm is not available");
}
+68 -5
View File
@@ -287,12 +287,12 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
);
} catch (e) {
// possibly a bad key
logger.error("Error starting session with device " +
logger.error("Error starting olm session with device " +
userId + ":" + deviceId + ": " + e);
return null;
}
logger.log("Started new sessionid " + sid +
logger.log("Started new olm sessionid " + sid +
" for device " + userId + ":" + deviceId);
return sid;
}
@@ -328,11 +328,74 @@ const _verifySignature = module.exports.verifySignature = async function(
// prepare the canonical json: remove unsigned and signatures, and stringify with
// anotherjson
delete obj.unsigned;
delete obj.signatures;
const json = anotherjson.stringify(obj);
const mangledObj = Object.assign({}, obj);
delete mangledObj.unsigned;
delete mangledObj.signatures;
const json = anotherjson.stringify(mangledObj);
olmDevice.verifySignature(
signingKey, json, signature,
);
};
/**
* Sign a JSON object using public key cryptography
* @param {Object} obj Object to sign. The object will be modified to include
* the new signature
* @param {Olm.PkSigning|Uint8Array} key the signing object or the private key
* seed
* @param {string} userId The user ID who owns the signing key
* @param {string} pubkey The public key (ignored if key is a seed)
* @returns {string} the signature for the object
*/
module.exports.pkSign = function(obj, key, userId, pubkey) {
let createdKey = false;
if (key instanceof Uint8Array) {
const keyObj = new global.Olm.PkSigning();
pubkey = keyObj.init_with_seed(key);
key = keyObj;
createdKey = true;
}
const sigs = obj.signatures || {};
delete obj.signatures;
const unsigned = obj.unsigned;
if (obj.unsigned) delete obj.unsigned;
try {
const mysigs = sigs[userId] || {};
sigs[userId] = mysigs;
return mysigs['ed25519:' + pubkey] = key.sign(anotherjson.stringify(obj));
} finally {
obj.signatures = sigs;
if (unsigned) obj.unsigned = unsigned;
if (createdKey) {
key.free();
}
}
};
/**
* Verify a signed JSON object
* @param {Object} obj Object to verify
* @param {string} pubkey The public key to use to verify
* @param {string} userId The user ID who signed the object
*/
module.exports.pkVerify = function(obj, pubkey, userId) {
const keyId = "ed25519:" + pubkey;
if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) {
throw new Error("No signature");
}
const signature = obj.signatures[userId][keyId];
const util = new global.Olm.Utility();
const sigs = obj.signatures;
delete obj.signatures;
const unsigned = obj.unsigned;
if (obj.unsigned) delete obj.unsigned;
try {
util.ed25519_verify(pubkey, anotherjson.stringify(obj), signature);
} finally {
obj.signatures = sigs;
if (unsigned) obj.unsigned = unsigned;
util.free();
}
};
@@ -332,6 +332,23 @@ export class Backend {
objectStore.put(newData, "-");
}
getCrossSigningKeys(txn, func) {
const objectStore = txn.objectStore("account");
const getReq = objectStore.get("crossSigningKeys");
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");
}
// Olm Sessions
countEndToEndSessions(txn, func) {
+24 -2
View File
@@ -290,7 +290,7 @@ export default class IndexedDBCryptoStore {
this._backendPromise.value().getAccount(txn, func);
}
/*
/**
* Write the account pickle to the store.
* This requires an active transaction. See doTxn().
*
@@ -301,6 +301,28 @@ export default class IndexedDBCryptoStore {
this._backendPromise.value().storeAccount(txn, newData);
}
/**
* Get the public part of the cross-signing keys (eg. self-signing key,
* user signing key).
*
* @param {*} txn An active transaction. See doTxn().
* @param {function(string)} func Called with the account keys object:
* { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed
*/
getCrossSigningKeys(txn, func) {
this._backendPromise.value().getCrossSigningKeys(txn, func);
}
/**
* Write the cross-signing keys back to the store
*
* @param {*} txn An active transaction. See doTxn().
* @param {string} keys keys object as getCrossSigningKeys()
*/
storeCrossSigningKeys(txn, keys) {
this._backendPromise.value().storeCrossSigningKeys(txn, keys);
}
// Olm sessions
/**
@@ -367,7 +389,7 @@ export default class IndexedDBCryptoStore {
);
}
// Inbound group saessions
// Inbound group sessions
/**
* Retrieve the end-to-end inbound group session for a given
+13 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
import Promise from 'bluebird';
import logger from '../../logger';
import MemoryCryptoStore from './memory-crypto-store.js';
import MemoryCryptoStore from './memory-crypto-store';
/**
* Internal module. Partial localStorage backed storage for e2e.
@@ -31,6 +31,7 @@ import MemoryCryptoStore from './memory-crypto-store.js';
const E2E_PREFIX = "crypto.";
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys";
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
@@ -284,6 +285,17 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
);
}
getCrossSigningKeys(txn, func) {
const keys = getJsonItem(this.store, KEY_CROSS_SIGNING_KEYS);
func(keys);
}
storeCrossSigningKeys(txn, keys) {
setJsonItem(
this.store, KEY_CROSS_SIGNING_KEYS, keys,
);
}
doTxn(mode, stores, func) {
return Promise.resolve(func(null));
}
+9
View File
@@ -33,6 +33,7 @@ export default class MemoryCryptoStore {
constructor() {
this._outgoingRoomKeyRequests = [];
this._account = null;
this._crossSigningKeys = null;
// Map of {devicekey -> {sessionId -> session pickle}}
this._sessions = {};
@@ -234,6 +235,14 @@ export default class MemoryCryptoStore {
this._account = newData;
}
getCrossSigningKeys(txn, func) {
func(this._crossSigningKeys);
}
storeCrossSigningKeys(txn, keys) {
this._crossSigningKeys = keys;
}
// Olm Sessions
countEndToEndSessions(txn, func) {
+132 -23
View File
@@ -22,6 +22,7 @@ limitations under the License.
import {MatrixEvent} from '../../models/event';
import {EventEmitter} from 'events';
import logger from '../../logger';
import DeviceInfo from '../deviceinfo';
import {newTimeoutError} from "./Error";
const timeoutException = new Error("Verification timed out");
@@ -47,55 +48,116 @@ export default class VerificationBase extends EventEmitter {
*
* @param {string} transactionId the transaction ID to be used when sending events
*
* @param {object} startEvent the m.key.verification.start event that
* @param {string} [roomId] the room to use for verification
*
* @param {object} [startEvent] the m.key.verification.start event that
* initiated this verification, if any
*
* @param {object} request the key verification request object related to
* @param {object} [request] the key verification request object related to
* this verification, if any
*
* @param {object} parent parent verification for this verification, if any
*/
constructor(baseApis, userId, deviceId, transactionId, startEvent, request, parent) {
constructor(baseApis, userId, deviceId, transactionId, roomId, startEvent, request) {
super();
this._baseApis = baseApis;
this.userId = userId;
this.deviceId = deviceId;
this.transactionId = transactionId;
this.startEvent = startEvent;
this.request = request;
if (typeof(roomId) === "string" || roomId instanceof String) {
this.roomId = roomId;
this.startEvent = startEvent;
this.request = request;
} else {
// if room ID was omitted, but start event and request were not
this.startEvent= roomId;
this.request = startEvent;
}
this.cancelled = false;
this._parent = parent;
this._done = false;
this._promise = null;
this._transactionTimeoutTimer = null;
this._eventsSubscription = null;
// At this point, the verification request was received so start the timeout timer.
this._resetTimer();
if (this.roomId) {
this._sendWithTxnId = this._sendMessage;
} else {
this._sendWithTxnId = this._sendToDevice;
}
}
_resetTimer() {
console.log("Refreshing/starting the verification transaction timeout timer");
logger.info("Refreshing/starting the verification transaction timeout timer");
if (this._transactionTimeoutTimer !== null) {
clearTimeout(this._transactionTimeoutTimer);
}
setTimeout(() => {
if (!this._done && !this.cancelled) {
console.log("Triggering verification timeout");
this.cancel(timeoutException);
}
this._transactionTimeoutTimer = setTimeout(() => {
if (!this._done && !this.cancelled) {
logger.info("Triggering verification timeout");
this.cancel(timeoutException);
}
}, 10 * 60 * 1000); // 10 minutes
}
_endTimer() {
if (this._transactionTimeoutTimer !== null) {
clearTimeout(this._transactionTimeoutTimer);
this._transactionTimeoutTimer = null;
}
}
_contentFromEventWithTxnId(event) {
if (this.roomId) { // verification as timeline event
// ensure m.related_to is included in e2ee rooms
// as the field is excluded from encryption
const content = Object.assign({}, event.getContent());
content["m.relates_to"] = event.getRelation();
return content;
} else { // verification as to_device event
return event.getContent();
}
}
/* creates a content object with the transaction id added to it */
_contentWithTxnId(content) {
const copy = Object.assign({}, content);
if (this.roomId) { // verification as timeline event
copy["m.relates_to"] = {
rel_type: "m.reference",
event_id: this.transactionId,
};
} else { // verification as to_device event
copy.transaction_id = this.transactionId;
}
return copy;
}
_send(type, contentWithoutTxnId) {
const content = this._contentWithTxnId(contentWithoutTxnId);
return this._sendWithTxnId(type, content);
}
/* send a message to the other participant, using to-device messages
*/
_sendToDevice(type, content) {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
}
content.transaction_id = this.transactionId;
return this._baseApis.sendToDevice(type, {
[this.userId]: { [this.deviceId]: content },
});
}
/* send a message to the other participant, using in-roomm messages
*/
_sendMessage(type, content) {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
}
return this._baseApis.sendEvent(this.roomId, type, content);
}
_waitForEvent(type) {
if (this._done) {
return Promise.reject(new Error("Verification is already done"));
@@ -115,12 +177,16 @@ export default class VerificationBase extends EventEmitter {
this._rejectEvent = undefined;
this._resetTimer();
this._resolveEvent(e);
} else if (e.getType() === "m.key.verification.cancel") {
const reject = this._reject;
this._reject = undefined;
reject(new Error("Other side cancelled verification"));
} else {
this._expectedEvent = undefined;
const exception = new Error(
"Unexpected message: expecting " + this._expectedEvent
+ " but got " + e.getType(),
);
this._expectedEvent = undefined;
if (this._rejectEvent) {
const reject = this._rejectEvent;
this._rejectEvent = undefined;
@@ -131,12 +197,18 @@ export default class VerificationBase extends EventEmitter {
}
done() {
this._endTimer(); // always kill the activity timer
if (!this._done) {
if (this.roomId) {
// verification in DM requires a done message
this._send("m.key.verification.done", {});
}
this._resolve();
}
}
cancel(e) {
this._endTimer(); // always kill the activity timer
if (!this._done) {
this.cancelled = true;
if (this.userId && this.deviceId && this.transactionId) {
@@ -144,7 +216,7 @@ export default class VerificationBase extends EventEmitter {
// cancelled by the other user)
if (e === timeoutException) {
const timeoutEvent = newTimeoutError();
this._sendToDevice(timeoutEvent.getType(), timeoutEvent.getContent());
this._send(timeoutEvent.getType(), timeoutEvent.getContent());
} else if (e instanceof MatrixEvent) {
const sender = e.getSender();
if (sender !== this.userId) {
@@ -154,9 +226,9 @@ export default class VerificationBase extends EventEmitter {
content.reason = content.reason || content.body
|| "Unknown reason";
content.transaction_id = this.transactionId;
this._sendToDevice("m.key.verification.cancel", content);
this._send("m.key.verification.cancel", content);
} else {
this._sendToDevice("m.key.verification.cancel", {
this._send("m.key.verification.cancel", {
code: "m.unknown",
reason: content.body || "Unknown reason",
transaction_id: this.transactionId,
@@ -164,7 +236,7 @@ export default class VerificationBase extends EventEmitter {
}
}
} else {
this._sendToDevice("m.key.verification.cancel", {
this._send("m.key.verification.cancel", {
code: "m.unknown",
reason: e.toString(),
transaction_id: this.transactionId,
@@ -176,6 +248,12 @@ export default class VerificationBase extends EventEmitter {
// but no reject function. If cancel is called again, we'd error.
if (this._reject) this._reject(e);
} else {
// unsubscribe from events, this happens in _reject usually but we don't have one here
if (this._eventsSubscription) {
this._eventsSubscription = this._eventsSubscription();
}
// FIXME: this causes an "Uncaught promise" console message
// if nothing ends up chaining this promise.
this._promise = Promise.reject(e);
}
// Also emit a 'cancel' event that the app can listen for to detect cancellation
@@ -196,10 +274,24 @@ export default class VerificationBase extends EventEmitter {
this._promise = new Promise((resolve, reject) => {
this._resolve = (...args) => {
this._done = true;
this._endTimer();
if (this.handler) {
// these listeners are attached in Crypto.acceptVerificationDM
if (this._eventsSubscription) {
this._eventsSubscription = this._eventsSubscription();
}
}
resolve(...args);
};
this._reject = (...args) => {
this._done = true;
this._endTimer();
if (this.handler) {
// these listeners are attached in Crypto.acceptVerificationDM
if (this._eventsSubscription) {
this._eventsSubscription = this._eventsSubscription();
}
}
reject(...args);
};
});
@@ -221,11 +313,24 @@ export default class VerificationBase extends EventEmitter {
for (const [keyId, keyInfo] of Object.entries(keys)) {
const deviceId = keyId.split(':', 2)[1];
const device = await this._baseApis.getStoredDevice(userId, deviceId);
if (!device) {
logger.warn(`verification: Could not find device ${deviceId} to verify`);
} else {
if (device) {
await verifier(keyId, device, keyInfo);
verifiedDevices.push(deviceId);
} else {
const crossSigningInfo = this._baseApis._crypto._deviceList
.getStoredCrossSigningForUser(userId);
if (crossSigningInfo && crossSigningInfo.getId() === deviceId) {
await verifier(keyId, DeviceInfo.fromStorage({
keys: {
[keyId]: deviceId,
},
}, deviceId), keyInfo);
verifiedDevices.push(deviceId);
} else {
logger.warn(
`verification: Could not find device ${deviceId} to verify`,
);
}
}
}
@@ -239,4 +344,8 @@ export default class VerificationBase extends EventEmitter {
await this._baseApis.setDeviceVerified(userId, deviceId);
}
}
setEventsSubscription(subscription) {
this._eventsSubscription = subscription;
}
}
+30 -13
View File
@@ -205,7 +205,7 @@ export default class SAS extends Base {
}
async _doSendVerification() {
const initialMessage = {
const initialMessage = this._contentWithTxnId({
method: SAS.NAME,
from_device: this._baseApis.deviceId,
key_agreement_protocols: KEY_AGREEMENT_LIST,
@@ -213,9 +213,10 @@ export default class SAS extends Base {
message_authentication_codes: MAC_LIST,
// FIXME: allow app to specify what SAS methods can be used
short_authentication_string: SAS_LIST,
transaction_id: this.transactionId,
};
this._sendToDevice("m.key.verification.start", initialMessage);
});
// add the transaction id to the message beforehand because
// it needs to be included in the commitment hash later on
this._sendWithTxnId("m.key.verification.start", initialMessage);
let e = await this._waitForEvent("m.key.verification.accept");
@@ -235,7 +236,7 @@ export default class SAS extends Base {
const hashCommitment = content.commitment;
const olmSAS = new global.Olm.SAS();
try {
this._sendToDevice("m.key.verification.key", {
this._send("m.key.verification.key", {
key: olmSAS.get_pubkey(),
});
@@ -280,7 +281,10 @@ export default class SAS extends Base {
}
async _doRespondVerification() {
let content = this.startEvent.getContent();
// as m.related_to is not included in the encrypted content in e2e rooms,
// we need to make sure it is added
let content = this._contentFromEventWithTxnId(this.startEvent);
// Note: we intersect using our pre-made lists, rather than the sets,
// so that the result will be in our order of preference. Then
// fetching the first element from the array will give our preferred
@@ -306,7 +310,7 @@ export default class SAS extends Base {
const olmSAS = new global.Olm.SAS();
try {
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
this._sendToDevice("m.key.verification.accept", {
this._send("m.key.verification.accept", {
key_agreement_protocol: keyAgreement,
hash: hashMethod,
message_authentication_code: macMethod,
@@ -320,7 +324,7 @@ export default class SAS extends Base {
// FIXME: make sure event is properly formed
content = e.getContent();
olmSAS.set_their_key(content.key);
this._sendToDevice("m.key.verification.key", {
this._send("m.key.verification.key", {
key: olmSAS.get_pubkey(),
});
@@ -354,22 +358,35 @@ export default class SAS extends Base {
}
_sendMAC(olmSAS, method) {
const keyId = `ed25519:${this._baseApis.deviceId}`;
const mac = {};
const keyList = [];
const baseInfo = "MATRIX_KEY_VERIFICATION_MAC"
+ this._baseApis.getUserId() + this._baseApis.deviceId
+ this.userId + this.deviceId
+ this.transactionId;
mac[keyId] = olmSAS[macMethods[method]](
const deviceKeyId = `ed25519:${this._baseApis.deviceId}`;
mac[deviceKeyId] = olmSAS[macMethods[method]](
this._baseApis.getDeviceEd25519Key(),
baseInfo + keyId,
baseInfo + deviceKeyId,
);
keyList.push(deviceKeyId);
const crossSigningId = this._baseApis.getCrossSigningId();
if (crossSigningId) {
const crossSigningKeyId = `ed25519:${crossSigningId}`;
mac[crossSigningKeyId] = olmSAS[macMethods[method]](
crossSigningId,
baseInfo + crossSigningKeyId,
);
keyList.push(crossSigningKeyId);
}
const keys = olmSAS[macMethods[method]](
keyId,
keyList.sort().join(","),
baseInfo + "KEY_IDS",
);
this._sendToDevice("m.key.verification.mac", { mac, keys });
this._send("m.key.verification.mac", { mac, keys });
}
async _checkMAC(olmSAS, content, method) {
+42 -88
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -22,7 +23,7 @@ import Promise from 'bluebird';
const parseContentType = require('content-type').parse;
const utils = require("./utils");
import logger from '../src/logger';
import logger from './logger';
// we use our own implementation of setTimeout, so that if we get suspended in
// the middle of a /sync, we cancel the sync as soon as we awake, rather than
@@ -46,10 +47,15 @@ module.exports.PREFIX_R0 = "/_matrix/client/r0";
module.exports.PREFIX_UNSTABLE = "/_matrix/client/unstable";
/**
* URI path for the identity API
* URI path for v1 of the the identity API
*/
module.exports.PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
/**
* URI path for the v2 identity API
*/
module.exports.PREFIX_IDENTITY_V2 = "/_matrix/identity/v2";
/**
* URI path for the media repo API
*/
@@ -90,6 +96,13 @@ module.exports.MatrixHttpApi = function MatrixHttpApi(event_emitter, opts) {
};
module.exports.MatrixHttpApi.prototype = {
/**
* Sets the baase URL for the identity server
* @param {string} url The new base url
*/
setIdBaseUrl: function(url) {
this.opts.idBaseUrl = url;
},
/**
* Get the content repository url with query parameters.
@@ -102,7 +115,7 @@ module.exports.MatrixHttpApi.prototype = {
};
return {
base: this.opts.baseUrl,
path: "/_matrix/media/v1/upload",
path: "/_matrix/media/r0/upload",
params: params,
};
},
@@ -291,7 +304,7 @@ module.exports.MatrixHttpApi.prototype = {
});
}
});
let url = this.opts.baseUrl + "/_matrix/media/v1/upload";
let url = this.opts.baseUrl + "/_matrix/media/r0/upload";
const queryArgs = [];
@@ -327,7 +340,7 @@ module.exports.MatrixHttpApi.prototype = {
promise = this.authedRequest(
opts.callback, "POST", "/upload", queryParams, body, {
prefix: "/_matrix/media/v1",
prefix: "/_matrix/media/r0",
headers: {"Content-Type": contentType},
json: false,
bodyParser: bodyParser,
@@ -368,7 +381,18 @@ module.exports.MatrixHttpApi.prototype = {
return this.uploads;
},
idServerRequest: function(callback, method, path, params, prefix) {
idServerRequest: function(
callback,
method,
path,
params,
prefix,
accessToken,
) {
if (!this.opts.idBaseUrl) {
throw new Error("No Identity Server base URL set");
}
const fullUri = this.opts.idBaseUrl + prefix + path;
if (callback !== undefined && !utils.isFunction(callback)) {
@@ -381,13 +405,17 @@ module.exports.MatrixHttpApi.prototype = {
uri: fullUri,
method: method,
withCredentials: false,
json: false,
json: true, // we want a JSON response if we can
_matrix_opts: this.opts,
headers: {},
};
if (method == 'GET') {
if (method === 'GET') {
opts.qs = params;
} else {
opts.form = params;
} else if (typeof params === "object") {
opts.json = params;
}
if (accessToken) {
opts.headers['Authorization'] = `Bearer ${accessToken}`;
}
const defer = Promise.defer();
@@ -395,12 +423,7 @@ module.exports.MatrixHttpApi.prototype = {
opts,
requestCallback(defer, callback, this.opts.onlyData),
);
// ID server does not always take JSON, so we can't use requests' 'json'
// option as we do with the home server, but it does return JSON, so
// parse it manually
return defer.promise.then(function(response) {
return JSON.parse(response);
});
return defer.promise;
},
/**
@@ -470,7 +493,7 @@ module.exports.MatrixHttpApi.prototype = {
const self = this;
requestPromise.catch(function(err) {
if (err.errcode == 'M_UNKNOWN_TOKEN') {
self.event_emitter.emit("Session.logged_out");
self.event_emitter.emit("Session.logged_out", err);
} else if (err.errcode == 'M_CONSENT_NOT_GIVEN') {
self.event_emitter.emit(
"no_consent",
@@ -525,76 +548,6 @@ module.exports.MatrixHttpApi.prototype = {
);
},
/**
* Perform an authorised request to the homeserver with a specific path
* prefix which overrides the default for this call only. Useful for hitting
* different Matrix Client-Server versions.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*
* @deprecated prefer authedRequest with opts.prefix
*/
authedRequestWithPrefix: function(callback, method, path, queryParams, data,
prefix, localTimeoutMs) {
return this.authedRequest(
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
},
);
},
/**
* Perform a request to the homeserver without any credentials but with a
* specific path prefix which overrides the default for this call only.
* Useful for hitting different Matrix Client-Server versions.
* @param {Function} callback Optional. The callback to invoke on
* success/failure. See the promise return values for more information.
* @param {string} method The HTTP method e.g. "GET".
* @param {string} path The HTTP path <b>after</b> the supplied prefix e.g.
* "/createRoom".
* @param {Object} queryParams A dict of query params (these will NOT be
* urlencoded).
* @param {Object} data The HTTP JSON body.
* @param {string} prefix The full prefix to use e.g.
* "/_matrix/client/v2_alpha".
* @param {Number=} localTimeoutMs The maximum amount of time to wait before
* timing out the request. If not specified, there is no timeout.
* @return {module:client.Promise} Resolves to <code>{data: {Object},
* headers: {Object}, code: {Number}}</code>.
* If <code>onlyData</code> is set, this will resolve to the <code>data</code>
* object only.
* @return {module:http-api.MatrixError} Rejects with an error if a problem
* occurred. This includes network problems and Matrix-specific error JSON.
*
* @deprecated prefer request with opts.prefix
*/
requestWithPrefix: function(callback, method, path, queryParams, data, prefix,
localTimeoutMs) {
return this.request(
callback, method, path, queryParams, data, {
localTimeoutMs: localTimeoutMs,
prefix: prefix,
},
);
},
/**
* Perform a request to an arbitrary URL.
* @param {Function} callback Optional. The callback to invoke on
@@ -883,7 +836,8 @@ function parseErrorResponse(response, body) {
let err;
if (contentType) {
if (contentType.type === 'application/json') {
err = new module.exports.MatrixError(JSON.parse(body));
const jsonBody = typeof(body) === 'object' ? body : JSON.parse(body);
err = new module.exports.MatrixError(jsonBody);
} else if (contentType.type === 'text/plain') {
err = new Error(`Server returned ${httpStatus} error: ${body}`);
}
+12 -9
View File
@@ -22,7 +22,7 @@ import Promise from 'bluebird';
const url = require("url");
const utils = require("./utils");
import logger from '../src/logger';
import logger from './logger';
const EMAIL_STAGE_TYPE = "m.login.email.identity";
const MSISDN_STAGE_TYPE = "m.login.msisdn";
@@ -174,16 +174,19 @@ InteractiveAuth.prototype = {
// The email can be validated out-of-band, but we need to provide the
// creds so the HS can go & check it.
if (this._emailSid) {
const idServerParsedUrl = url.parse(
this._matrixClient.getIdentityServerUrl(),
);
const creds = {
sid: this._emailSid,
client_secret: this._clientSecret,
};
if (await this._matrixClient.doesServerRequireIdServerParam()) {
const idServerParsedUrl = url.parse(
this._matrixClient.getIdentityServerUrl(),
);
creds.id_server = idServerParsedUrl.host;
}
authDict = {
type: EMAIL_STAGE_TYPE,
threepid_creds: {
sid: this._emailSid,
client_secret: this._clientSecret,
id_server: idServerParsedUrl.host,
},
threepid_creds: creds,
};
}
}
+2
View File
@@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -76,6 +77,7 @@ module.exports.InteractiveAuth = require("./interactive-auth");
/** The {@link module:auto-discovery|AutoDiscovery} class. */
module.exports.AutoDiscovery = require("./autodiscovery").AutoDiscovery;
module.exports.SERVICE_TYPES = require('./service-types').SERVICE_TYPES;
module.exports.MemoryCryptoStore =
require("./crypto/store/memory-crypto-store").default;
+22 -5
View File
@@ -21,7 +21,7 @@ const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
const EventTimeline = require("./event-timeline");
import {EventStatus} from "./event";
import logger from '../../src/logger';
import logger from '../logger';
import Relations from './relations';
// var DEBUG = false;
@@ -92,6 +92,13 @@ function EventTimelineSet(room, opts) {
}
utils.inherits(EventTimelineSet, EventEmitter);
/**
* Get all the timelines in this set
* @return {module:models/event-timeline~EventTimeline[]} the timelines in this set
*/
EventTimelineSet.prototype.getTimelines = function() {
return this._timelines;
};
/**
* Get the filter object this timeline set is filtered on, if any
* @return {?Filter} the optional filter for this timelineSet
@@ -688,9 +695,12 @@ EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
* The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc.
* @param {String} eventType
* The relation event's type, such as "m.reaction", etc.
* @throws If <code>eventId</code>, <code>relationType</code> or <code>eventType</code>
* are not valid.
*
* @returns {Relations}
* A container for relation events.
* @returns {?Relations}
* A container for relation events or undefined if there are no relation events for
* the relationType.
*/
EventTimelineSet.prototype.getRelationsForEvent = function(
eventId, relationType, eventType,
@@ -782,20 +792,27 @@ EventTimelineSet.prototype.aggregateRelations = function(event) {
}
let relationsWithEventType = relationsWithRelType[eventType];
let isNewRelations = false;
let relatesToEvent;
if (!relationsWithEventType) {
relationsWithEventType = relationsWithRelType[eventType] = new Relations(
relationType,
eventType,
this.room,
);
const relatesToEvent = this.findEventById(relatesToEventId);
isNewRelations = true;
relatesToEvent = this.findEventById(relatesToEventId);
if (relatesToEvent) {
relationsWithEventType.setTargetEvent(relatesToEvent);
relatesToEvent.emit("Event.relationsCreated", relationType, eventType);
}
}
relationsWithEventType.addEvent(event);
// only emit once event has been added to relations
if (isNewRelations && relatesToEvent) {
relatesToEvent.emit("Event.relationsCreated", relationType, eventType);
}
};
/**
+32 -3
View File
@@ -24,7 +24,7 @@ limitations under the License.
import Promise from 'bluebird';
import {EventEmitter} from 'events';
import utils from '../utils.js';
import logger from '../../src/logger';
import logger from '../logger';
/**
* Enum for event statuses.
@@ -898,13 +898,25 @@ utils.extend(module.exports.MatrixEvent.prototype, {
return this.status;
},
getServerAggregatedRelation(relType) {
const relations = this.getUnsigned()["m.relations"];
if (relations) {
return relations[relType];
}
},
/**
* Returns the event ID of the event replacing the content of this event, if any.
*
* @return {string?}
*/
replacingEventId() {
return this._replacingEvent && this._replacingEvent.getId();
const replaceRelation = this.getServerAggregatedRelation("m.replace");
if (replaceRelation) {
return replaceRelation.event_id;
} else if (this._replacingEvent) {
return this._replacingEvent.getId();
}
},
/**
@@ -918,6 +930,23 @@ utils.extend(module.exports.MatrixEvent.prototype, {
return this._replacingEvent;
},
/**
* Returns the origin_server_ts of the event replacing the content of this event, if any.
*
* @return {Date?}
*/
replacingEventDate() {
const replaceRelation = this.getServerAggregatedRelation("m.replace");
if (replaceRelation) {
const ts = replaceRelation.origin_server_ts;
if (Number.isFinite(ts)) {
return new Date(ts);
}
} else if (this._replacingEvent) {
return this._replacingEvent.getDate();
}
},
/**
* Returns the event that wants to redact this event, but hasn't been sent yet.
* @return {MatrixEvent} the event
@@ -1024,7 +1053,7 @@ utils.extend(module.exports.MatrixEvent.prototype, {
/* _REDACT_KEEP_KEY_MAP gives the keys we keep when an event is redacted
*
* This is specified here:
* http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#redactions
* http://matrix.org/speculator/spec/HEAD/client_server/latest.html#redactions
*
* Also:
* - We keep 'unsigned' since that is created by the local server
+11 -1
View File
@@ -242,7 +242,7 @@ export default class Relations extends EventEmitter {
redactedEvent.removeListener("Event.beforeRedaction", this._onBeforeRedaction);
this.emit("Relations.redaction");
this.emit("Relations.redaction", redactedEvent);
}
/**
@@ -301,10 +301,20 @@ export default class Relations extends EventEmitter {
// event is known anyway.
return null;
}
// the all-knowning server tells us that the event at some point had
// this timestamp for its replacement, so any following replacement should definitely not be less
const replaceRelation =
this._targetEvent.getServerAggregatedRelation("m.replace");
const minTs = replaceRelation && replaceRelation.origin_server_ts;
return this.getRelations().reduce((last, event) => {
if (event.getSender() !== this._targetEvent.getSender()) {
return last;
}
if (minTs && minTs > event.getTs()) {
return last;
}
if (last && last.getTs() > event.getTs()) {
return last;
}
+1 -1
View File
@@ -249,7 +249,7 @@ RoomMember.prototype.getDMInviter = function() {
* "crop" or "scale".
* @param {Boolean} allowDefault (optional) Passing false causes this method to
* return null if the user has no avatar image. Otherwise, a default image URL
* will be returned. Default: true.
* will be returned. Default: true. (Deprecated)
* @param {Boolean} allowDirectLinks (optional) If true, the avatar URL will be
* returned even if it is a direct hyperlink rather than a matrix content URL.
* If false, any non-matrix content URLs will be ignored. Setting this option to
+1 -1
View File
@@ -21,7 +21,7 @@ const EventEmitter = require("events").EventEmitter;
const utils = require("../utils");
const RoomMember = require("./room-member");
import logger from '../../src/logger';
import logger from '../logger';
// possible statuses for out-of-band member loading
const OOB_STATUS_NOTSTARTED = 1;
+52 -5
View File
@@ -1,6 +1,7 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018, 2019 New Vector Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -29,7 +30,7 @@ const ContentRepo = require("../content-repo");
const EventTimeline = require("./event-timeline");
const EventTimelineSet = require("./event-timeline-set");
import logger from '../../src/logger';
import logger from '../logger';
import ReEmitter from '../ReEmitter';
// These constants are used as sane defaults when the homeserver doesn't support
@@ -346,13 +347,30 @@ Room.prototype.userMayUpgradeRoom = function(userId) {
Room.prototype.getPendingEvents = function() {
if (this._opts.pendingEventOrdering !== "detached") {
throw new Error(
"Cannot call getPendingEventList with pendingEventOrdering == " +
"Cannot call getPendingEvents with pendingEventOrdering == " +
this._opts.pendingEventOrdering);
}
return this._pendingEventList;
};
/**
* Check whether the pending event list contains a given event by ID.
*
* @param {string} eventId The event ID to check for.
* @return {boolean}
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
*/
Room.prototype.hasPendingEvent = function(eventId) {
if (this._opts.pendingEventOrdering !== "detached") {
throw new Error(
"Cannot call hasPendingEvent with pendingEventOrdering == " +
this._opts.pendingEventOrdering);
}
return this._pendingEventList.some(event => event.getId() === eventId);
};
/**
* Get the live unfiltered timeline for this room.
*
@@ -362,6 +380,23 @@ Room.prototype.getLiveTimeline = function() {
return this.getUnfilteredTimelineSet().getLiveTimeline();
};
/**
* Get the timestamp of the last message in the room
*
* @return {number} the timestamp of the last message in the room
*/
Room.prototype.getLastActiveTimestamp = function() {
const timeline = this.getLiveTimeline();
const events = timeline.getEvents();
if (events.length) {
const lastEvent = events[events.length - 1];
return lastEvent.getTs();
} else {
return Number.MIN_SAFE_INTEGER;
}
};
/**
* @param {string} myUserId the user id for the logged in member
* @return {string} the membership type (join | leave | invite) for the logged in user
@@ -763,7 +798,7 @@ Room.prototype.getBlacklistUnverifiedDevices = function() {
* @param {string} resizeMethod The thumbnail resize method to use, either
* "crop" or "scale".
* @param {boolean} allowDefault True to allow an identicon for this room if an
* avatar URL wasn't explicitly set. Default: true.
* avatar URL wasn't explicitly set. Default: true. (Deprecated)
* @return {?string} the avatar URL or null.
*/
Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
@@ -1039,6 +1074,18 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy) {
const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
if (redactedEvent) {
redactedEvent.makeRedacted(event);
// If this is in the current state, replace it with the redacted version
if (redactedEvent.getStateKey()) {
const currentStateEvent = this.currentState.getStateEvents(
redactedEvent.getType(),
redactedEvent.getStateKey(),
);
if (currentStateEvent.getId() === redactedEvent.getId()) {
this.currentState.setStateEvents([redactedEvent]);
}
}
this.emit("Room.redaction", event, this);
// TODO: we stash user displaynames (among other things) in
@@ -1157,7 +1204,7 @@ Room.prototype.addPendingEvent = function(event, txnId) {
for (let i = 0; i < this._timelineSets.length; i++) {
const timelineSet = this._timelineSets[i];
if (timelineSet.getFilter()) {
if (this._filter.filterRoomTimeline([event]).length) {
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
timelineSet.addEventToTimeline(event,
timelineSet.getLiveTimeline(), false);
}
@@ -1186,7 +1233,7 @@ Room.prototype._aggregateNonLiveRelation = function(event) {
for (let i = 0; i < this._timelineSets.length; i++) {
const timelineSet = this._timelineSets[i];
if (timelineSet.getFilter()) {
if (this._filter.filterRoomTimeline([event]).length) {
if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
timelineSet.aggregateRelations(event);
}
} else {
+53
View File
@@ -43,6 +43,11 @@ const DEFAULT_OVERRIDE_RULES = [
key: "type",
pattern: "m.room.tombstone",
},
{
kind: "event_match",
key: "state_key",
pattern: "",
},
],
actions: [
"notify",
@@ -52,6 +57,22 @@ const DEFAULT_OVERRIDE_RULES = [
},
],
},
{
// For homeservers which don't support MSC2153 yet
rule_id: ".m.rule.reaction",
default: true,
enabled: true,
conditions: [
{
kind: "event_match",
key: "type",
pattern: "m.reaction",
},
],
actions: [
"dont_notify",
],
},
];
/**
@@ -439,6 +460,38 @@ PushProcessor.actionListToActionsObject = function(actionlist) {
return actionobj;
};
/**
* Rewrites conditions on a client's push rules to match the defaults
* where applicable. Useful for upgrading push rules to more strict
* conditions when the server is falling behind on defaults.
* @param {object} incomingRules The client's existing push rules
* @returns {object} The rewritten rules
*/
PushProcessor.rewriteDefaultRules = function(incomingRules) {
let newRules = JSON.parse(JSON.stringify(incomingRules)); // deep clone
// These lines are mostly to make the tests happy. We shouldn't run into these
// properties missing in practice.
if (!newRules) newRules = {};
if (!newRules.global) newRules.global = {};
if (!newRules.global.override) newRules.global.override = [];
// Fix default override rules
newRules.global.override = newRules.global.override.map(r => {
const defaultRule = DEFAULT_OVERRIDE_RULES.find(d => d.rule_id === r.rule_id);
if (!defaultRule) return r;
// Copy over the actions, default, and conditions. Don't touch the user's
// preference.
r.default = defaultRule.default;
r.conditions = defaultRule.conditions;
r.actions = defaultRule.actions;
return r;
});
return newRules;
};
/**
* @typedef {Object} PushAction
* @type {Object}
+1 -1
View File
@@ -24,7 +24,7 @@ limitations under the License.
*/
"use strict";
import logger from '../src/logger';
import logger from './logger';
// we schedule a callback at least this often, to check if we've missed out on
// some wall-clock time due to being suspended.
+1 -1
View File
@@ -21,7 +21,7 @@ limitations under the License.
*/
const utils = require("./utils");
import Promise from 'bluebird';
import logger from '../src/logger';
import logger from './logger';
const DEBUG = false; // set true to enable console logging.
+20
View File
@@ -0,0 +1,20 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
export const SERVICE_TYPES = Object.freeze({
IS: 'SERVICE_TYPE_IS', // An Identity Service
IM: 'SERVICE_TYPE_IM', // An Integration Manager
});
+1 -1
View File
@@ -19,7 +19,7 @@ import Promise from 'bluebird';
import SyncAccumulator from "../sync-accumulator";
import utils from "../utils";
import * as IndexedDBHelpers from "../indexeddb-helpers";
import logger from '../../src/logger';
import logger from '../logger';
const VERSION = 3;
+1 -1
View File
@@ -16,7 +16,7 @@ limitations under the License.
*/
import Promise from 'bluebird';
import logger from '../../src/logger';
import logger from '../logger';
/**
* An IndexedDB store backend where the actual backend sits in a web
+1 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
import Promise from 'bluebird';
import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
import logger from '../../src/logger';
import logger from '../logger';
/**
* This class lives in the webworker and drives a LocalIndexedDBStoreBackend
+1 -1
View File
@@ -25,7 +25,7 @@ import LocalIndexedDBStoreBackend from "./indexeddb-local-backend.js";
import RemoteIndexedDBStoreBackend from "./indexeddb-remote-backend.js";
import User from "../models/user";
import {MatrixEvent} from "../models/event";
import logger from '../../src/logger';
import logger from '../logger';
/**
* This is an internal module. See {@link IndexedDBStore} for the public class.
+1 -1
View File
@@ -21,7 +21,7 @@ limitations under the License.
*/
import utils from "./utils";
import logger from '../src/logger';
import logger from './logger';
/**
+5 -3
View File
@@ -32,7 +32,8 @@ const Group = require('./models/group');
const utils = require("./utils");
const Filter = require("./filter");
const EventTimeline = require("./models/event-timeline");
import logger from '../src/logger';
const PushProcessor = require("./pushprocessor");
import logger from './logger';
import {InvalidStoreError} from './errors';
@@ -1030,8 +1031,9 @@ SyncApi.prototype._processSyncResponse = async function(
// honour push rules that were previously cached. Base rules
// will be updated when we recieve push rules via getPushRules
// (see SyncApi.prototype.sync) before syncing over the network.
if (accountDataEvent.getType() == 'm.push_rules') {
client.pushRules = accountDataEvent.getContent();
if (accountDataEvent.getType() === 'm.push_rules') {
const rules = accountDataEvent.getContent();
client.pushRules = PushProcessor.rewriteDefaultRules(rules);
}
client.emit("accountData", accountDataEvent);
return accountDataEvent;
+1 -1
View File
@@ -19,7 +19,7 @@ limitations under the License.
import Promise from 'bluebird';
const EventTimeline = require("./models/event-timeline");
import logger from '../src/logger';
import logger from './logger';
/**
* @private
+9
View File
@@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -699,3 +700,11 @@ module.exports.globToRegexp = function(glob, extended) {
}
return pat;
};
module.exports.ensureNoTrailingSlash = function(url) {
if (url && url.endsWith("/")) {
return url.substr(0, url.length - 1);
} else {
return url;
}
};
+40 -41
View File
@@ -21,7 +21,7 @@ limitations under the License.
*/
const utils = require("../utils");
const EventEmitter = require("events").EventEmitter;
import logger from '../../src/logger';
import logger from '../logger';
const DEBUG = true; // set true to enable console logging.
// events: hangup, error(err), replaced(call), state(state, oldState)
@@ -61,9 +61,9 @@ function MatrixCall(opts) {
this.URL = opts.URL;
// Array of Objects with urls, username, credential keys
this.turnServers = opts.turnServers || [];
if (this.turnServers.length === 0) {
if (this.turnServers.length === 0 && this.client.isFallbackICEServerAllowed()) {
this.turnServers.push({
urls: [MatrixCall.FALLBACK_STUN_SERVER],
urls: [MatrixCall.FALLBACK_ICE_SERVER],
});
}
utils.forEach(this.turnServers, function(server) {
@@ -92,8 +92,8 @@ function MatrixCall(opts) {
}
/** The length of time a call can be ringing for. */
MatrixCall.CALL_TIMEOUT_MS = 60000;
/** The fallback server to use for STUN. */
MatrixCall.FALLBACK_STUN_SERVER = 'stun:stun.l.google.com:19302';
/** The fallback ICE server to use for STUN or TURN protocols. */
MatrixCall.FALLBACK_ICE_SERVER = 'stun:turn.matrix.org';
/** An error code when the local client failed to create an offer. */
MatrixCall.ERR_LOCAL_OFFER_FAILED = "local_offer_failed";
/**
@@ -665,7 +665,7 @@ MatrixCall.prototype._maybeGotUserMediaForAnswer = function(stream) {
},
};
self.peerConn.createAnswer(function(description) {
debuglog("Created answer: " + description);
debuglog("Created answer: ", description);
self.peerConn.setLocalDescription(description, function() {
self._answerContent = {
version: 0,
@@ -754,7 +754,7 @@ MatrixCall.prototype._receivedAnswer = function(msg) {
*/
MatrixCall.prototype._gotLocalOffer = function(description) {
const self = this;
debuglog("Created offer: " + description);
debuglog("Created offer: ", description);
if (self.state == 'ended') {
debuglog("Ignoring newly created offer on call ID " + self.callId +
@@ -1217,24 +1217,9 @@ const _placeCallWithConstraints = function(self, constraints) {
};
const _createPeerConnection = function(self) {
let servers = self.turnServers;
if (self.webRtc.vendor === "mozilla") {
// modify turnServers struct to match what mozilla expects.
servers = [];
for (let i = 0; i < self.turnServers.length; i++) {
for (let j = 0; j < self.turnServers[i].urls.length; j++) {
servers.push({
url: self.turnServers[i].urls[j],
username: self.turnServers[i].username,
credential: self.turnServers[i].credential,
});
}
}
}
const pc = new self.webRtc.RtcPeerConnection({
iceTransportPolicy: self.forceTURN ? 'relay' : undefined,
iceServers: servers,
iceServers: self.turnServers,
});
pc.oniceconnectionstatechange = hookCallback(self, self._onIceConnectionStateChanged);
pc.onsignalingstatechange = hookCallback(self, self._onSignallingStateChanged);
@@ -1352,7 +1337,9 @@ module.exports.setVideoInput = function(deviceId) { videoInput = deviceId; };
* @param {MatrixClient} client The client instance to use.
* @param {string} roomId The room the call is in.
* @param {Object?} options DEPRECATED optional options map.
* @param {boolean} options.forceTURN DEPRECATED whether relay through TURN should be forced. This option is deprecated - use opts.forceTURN when creating the matrix client since it's only possible to set this option on outbound calls.
* @param {boolean} options.forceTURN DEPRECATED whether relay through TURN should be
* forced. This option is deprecated - use opts.forceTURN when creating the matrix client
* since it's only possible to set this option on outbound calls.
* @return {MatrixCall} the call or null if the browser doesn't support calling.
*/
module.exports.createNewMatrixCall = function(client, roomId, options) {
@@ -1383,24 +1370,36 @@ module.exports.createNewMatrixCall = function(client, roomId, options) {
return getUserMedia.apply(w.navigator, arguments);
};
}
webRtc.RtcPeerConnection = (
w.RTCPeerConnection || w.webkitRTCPeerConnection || w.mozRTCPeerConnection
);
webRtc.RtcSessionDescription = (
w.RTCSessionDescription || w.webkitRTCSessionDescription ||
w.mozRTCSessionDescription
);
webRtc.RtcIceCandidate = (
w.RTCIceCandidate || w.webkitRTCIceCandidate || w.mozRTCIceCandidate
);
webRtc.vendor = null;
if (w.mozRTCPeerConnection) {
webRtc.vendor = "mozilla";
} else if (w.webkitRTCPeerConnection) {
webRtc.vendor = "webkit";
} else if (w.RTCPeerConnection) {
webRtc.vendor = "generic";
// 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
// client creation process.
try {
webRtc.RtcPeerConnection = (
w.RTCPeerConnection || w.webkitRTCPeerConnection || w.mozRTCPeerConnection
);
webRtc.RtcSessionDescription = (
w.RTCSessionDescription || w.webkitRTCSessionDescription ||
w.mozRTCSessionDescription
);
webRtc.RtcIceCandidate = (
w.RTCIceCandidate || w.webkitRTCIceCandidate || w.mozRTCIceCandidate
);
webRtc.vendor = null;
if (w.mozRTCPeerConnection) {
webRtc.vendor = "mozilla";
} else if (w.webkitRTCPeerConnection) {
webRtc.vendor = "webkit";
} else if (w.RTCPeerConnection) {
webRtc.vendor = "generic";
}
} catch (e) {
logger.error("Failed to set up WebRTC object: possible browser interference?");
logger.error(e);
return null;
}
if (!webRtc.RtcIceCandidate || !webRtc.RtcSessionDescription ||
!webRtc.RtcPeerConnection || !webRtc.getUserMedia) {
return null; // WebRTC is not supported.
+680 -445
View File
File diff suppressed because it is too large Load Diff