Compare commits
366 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0d59963b53 | |||
| c669aafedb | |||
| 358f13500b | |||
| 016f16954a | |||
| c1543545d2 | |||
| 5da936d96a | |||
| 0dead73837 | |||
| 92ca2386ea | |||
| 59b25d6837 | |||
| a6f7936311 | |||
| 7392b4de17 | |||
| 79b0a5fada | |||
| aee9442e52 | |||
| 569d5d1fce | |||
| 6ea8003df2 | |||
| c8ab82010a | |||
| bf1bec9c6c | |||
| e0c90ec9e3 | |||
| 7ad5021147 | |||
| fd73c3fb3a | |||
| e3dbf7cc41 | |||
| 18749c580e | |||
| 396db30fbf | |||
| 6b38868de6 | |||
| 01a46ad880 | |||
| 46f8251e94 | |||
| 77f882f45a | |||
| 8c72fd104e | |||
| 549656884b | |||
| 5b8b0a8aa3 | |||
| b1924d4db6 | |||
| 1b877118ef | |||
| 682a5daf1c | |||
| fcbfaac1fd | |||
| 3787b6f1c7 | |||
| 6e08835496 | |||
| 191695da5a | |||
| 2215087f96 | |||
| 32234ee7fc | |||
| aa37f697bf | |||
| 057303d57c | |||
| ccc85d98e2 | |||
| c30a8b5a29 | |||
| 295010893d | |||
| 7fb807919c | |||
| bd8f8ef28d | |||
| 3901a381cc | |||
| 12f6e51ef6 | |||
| aa8454e30a | |||
| 6b70230e0d | |||
| 5e0ba9971c | |||
| fa577c9475 | |||
| 11a958b8ca | |||
| 6952db6762 | |||
| 51898cffe8 | |||
| d8337d703d | |||
| adac0c353c | |||
| 04fca16420 | |||
| ca89b6e7a8 | |||
| ac1173c628 | |||
| 0a0ae111f6 | |||
| 71a6e015f4 | |||
| e8bbb8a1cc | |||
| 04764998cb | |||
| 5262d716e4 | |||
| 7addacba38 | |||
| 8f8c9c8ec0 | |||
| 3a9832a8c6 | |||
| 4a40c10d4c | |||
| 58f8ca7d66 | |||
| 4d950fec66 | |||
| b4f68f4fc6 | |||
| ac742aad70 | |||
| 53d225a1d1 | |||
| 549b0f9313 | |||
| 2ce106382a | |||
| b44f43e5db | |||
| 2321b9a04e | |||
| 3bd518cf7f | |||
| c57109c2f3 | |||
| 522640edd9 | |||
| 5fc0629201 | |||
| 26edd7431a | |||
| fd58957b06 | |||
| 12bb0b86dd | |||
| 165c1fc0b6 | |||
| c785b10603 | |||
| 90512bdd5f | |||
| 10751e9a6d | |||
| d2ebc58c3c | |||
| d51c5a2d68 | |||
| 1f24845431 | |||
| 3b02b62ba5 | |||
| 24ae787736 | |||
| cd735ef459 | |||
| 180fea8ace | |||
| 5f02c4b5ad | |||
| 41680f6089 | |||
| 730f7d3dff | |||
| d32033f105 | |||
| 440274d639 | |||
| f93130a8a7 | |||
| 3d9bddfb9f | |||
| 439abbcce9 | |||
| ac91367801 | |||
| 2a63cc474c | |||
| 56261263f5 | |||
| 04b57bbe9d | |||
| c550f83a04 | |||
| 5224ef4b1f | |||
| 2ab033e76e | |||
| fa2e669eda | |||
| f0ba1f2ac0 | |||
| 6d0237ec71 | |||
| 97dff4640a | |||
| 00b571a429 | |||
| 86e0f49231 | |||
| f2f205f9bd | |||
| f84ec090cb | |||
| d37ed9ff6f | |||
| f5a5f5e51a | |||
| fe010242d9 | |||
| 545ebf81bf | |||
| 408934932a | |||
| 6f42824c35 | |||
| c3215d51bd | |||
| e541b96a71 | |||
| 904a2f466e | |||
| bad48da11a | |||
| ce2d1d6e2b | |||
| 2820071db1 | |||
| 5937185ce9 | |||
| be9b7a0d24 | |||
| 7ca09ad749 | |||
| 686a7a40f9 | |||
| 2a7b2835b6 | |||
| 69ecf3b145 | |||
| 2cd748b50c | |||
| 291133beb9 | |||
| e10c17c866 | |||
| 0048cbef08 | |||
| d9d65309b3 | |||
| d5d8032b5b | |||
| 693c749da0 | |||
| 7218e31a9c | |||
| 1798f3921f | |||
| d12c56a623 | |||
| 26aa3d3ce7 | |||
| c97a87d1f6 | |||
| 9bc185d459 | |||
| 4c651c15ea | |||
| a98e6964ef | |||
| 6f8d9c4693 | |||
| fbc4bd0c96 | |||
| 03c9241783 | |||
| 3a983271d6 | |||
| 03fe4afe32 | |||
| 12627022d1 | |||
| fabfe16d45 | |||
| a34758f938 | |||
| 20f5c3ea28 | |||
| 62e490cfe4 | |||
| a9dba39623 | |||
| f1d417597c | |||
| 549f679bf1 | |||
| 6ba052dcc4 | |||
| de873b84f5 | |||
| 37558ac1b4 | |||
| 9140d5a091 | |||
| 7827af0d90 | |||
| 1af8d20adf | |||
| 91df096698 | |||
| e8fd0498a7 | |||
| f3073e120d | |||
| a571624e13 | |||
| 74b649c04c | |||
| 7973b99f50 | |||
| e8f5a8b89d | |||
| 2d0bda933c | |||
| 49588da73d | |||
| 3e2d845342 | |||
| e92d2bd70a | |||
| de1b545df1 | |||
| 3bec28b2ff | |||
| 8cad116dd7 | |||
| 35adb75d80 | |||
| e9908b1d97 | |||
| fffd2eb70a | |||
| 136b9c0f50 | |||
| 0f1206b4ee | |||
| 46d7e4c707 | |||
| c874783742 | |||
| bb296f50d9 | |||
| da68b53ff9 | |||
| bbe141d44e | |||
| 8a03e41a7c | |||
| a79e1bc976 | |||
| 056bfbf7a3 | |||
| e0b64a487d | |||
| d47d1d8f26 | |||
| 42a07de9a7 | |||
| aead855470 | |||
| 335b2314f1 | |||
| 89bab24c14 | |||
| 3a439dcdad | |||
| 20d82eb92f | |||
| 319e1d1191 | |||
| 5f3492dbf8 | |||
| 107c8c0b1f | |||
| 8c6d9586bf | |||
| 1271fc6bf3 | |||
| c9df03c40c | |||
| d8e8dddd25 | |||
| 27f6745123 | |||
| 964f448334 | |||
| 20ee03bb44 | |||
| 77bd677182 | |||
| e024d047e3 | |||
| 40943edc06 | |||
| e6699c5424 | |||
| bd8a307e50 | |||
| f71301cafc | |||
| 562bf9331b | |||
| 11e6eb94b5 | |||
| cee3aa2a7a | |||
| 81e3783488 | |||
| fc7f9786f8 | |||
| 0808c0edf1 | |||
| 8de6746efd | |||
| eb9b8ef7c6 | |||
| b09621b915 | |||
| 8d667f9367 | |||
| 56dfe6630f | |||
| 8b3b181a48 | |||
| c952768542 | |||
| 1a368aa996 | |||
| 61449458cf | |||
| 4eb547e535 | |||
| b54acffaef | |||
| 65a1833e1f | |||
| 1ce4f25811 | |||
| 3127105516 | |||
| d59ea4be78 | |||
| f256f04440 | |||
| b444aaa67e | |||
| 745185e689 | |||
| 2bfa891f0a | |||
| 147167bed3 | |||
| 565e18e8a3 | |||
| 55b4595bbf | |||
| eeb2c463dc | |||
| d9bb0e9a52 | |||
| 8cae00407a | |||
| aaabebe7f5 | |||
| 80a92dcdc2 | |||
| dc9081e9d4 | |||
| 3c299637b6 | |||
| 07af333943 | |||
| 0bbc781d0c | |||
| 79bf64f079 | |||
| ed67d39456 | |||
| 2f8cc75432 | |||
| 03cccef805 | |||
| 6d5a0c2718 | |||
| 42b359eb5c | |||
| 3071587f11 | |||
| f3ec9768bc | |||
| 23159807b0 | |||
| b1ba9f76b8 | |||
| 0e51dfed46 | |||
| 09b00335f8 | |||
| 3d274815d9 | |||
| 70d60b905d | |||
| 3e2ffb25a6 | |||
| 8b9bef5cb3 | |||
| 31e72efc91 | |||
| 60b7252597 | |||
| 3980b62df2 | |||
| b306df726a | |||
| 3d5a79be3b | |||
| ba78d1a9ae | |||
| 241811298f | |||
| 8a0ddc43ab | |||
| 898fa0e41b | |||
| 081ff4dec0 | |||
| 3c69b8511d | |||
| 6843d86ecf | |||
| 2e91200136 | |||
| 852304c417 | |||
| ee752e3885 | |||
| b9480e4302 | |||
| 2ae4d07971 | |||
| 90cac8a118 | |||
| db18274f6e | |||
| 172bad8b55 | |||
| dfe454e18f | |||
| 3d8dd29b4c | |||
| c3ff213ec9 | |||
| e80e5e1f8c | |||
| bba249d5ce | |||
| f57df2bee5 | |||
| b930638156 | |||
| 39c1de19fc | |||
| 17724fc8d3 | |||
| 4c6d11d9ed | |||
| 05d77a85c9 | |||
| e95a133cdd | |||
| c21382d721 | |||
| 8c15125e23 | |||
| 64ddbd97dd | |||
| 9c24bcb7a9 | |||
| 8f016726f0 | |||
| 649fe7a490 | |||
| 35f1cdf89c | |||
| f05bf3f845 | |||
| a40d691159 | |||
| 4ebe60b2ad | |||
| 5a70859593 | |||
| c7be810e65 | |||
| 101217cfb6 | |||
| 5c2aa4677f | |||
| ab9bfa68ae | |||
| b004d1602d | |||
| 7f8b9de560 | |||
| 761f22b63d | |||
| b00804102d | |||
| 8d1d657c44 | |||
| 6cd09c6af2 | |||
| 46a8486245 | |||
| c5caf8f8f4 | |||
| 4356603665 | |||
| 1cae5e8b97 | |||
| 07c2e34d87 | |||
| 5bcbe76f2c | |||
| 4c6fa89053 | |||
| 98815ffdf6 | |||
| 6f6e7ea921 | |||
| 0c714ba4a1 | |||
| 5f539aacd9 | |||
| 6a77df7b41 | |||
| 4a9a1b40e9 | |||
| dc971b9a59 | |||
| 95131c7658 | |||
| 936eef194a | |||
| 941d871daf | |||
| 609ee663fa | |||
| 53804cac5c | |||
| 193ad9e09d | |||
| 405451d783 | |||
| b0275afac2 | |||
| ae71f41138 | |||
| ec2f07e1aa | |||
| 32814d1833 | |||
| e54f71718f | |||
| 7f5584e4f5 | |||
| b3513dc8f8 | |||
| 1b82dffcb4 | |||
| 5500f0d794 | |||
| c8082535de | |||
| 7dedcb82b2 | |||
| 7195365188 | |||
| 910d0ec9c1 | |||
| 1d58a64ee1 | |||
| 1f77cc6d1a | |||
| 02d4dcb128 | |||
| 2b54f442d1 |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"presets": ["es2015"],
|
||||
"presets": ["es2015", "es2016"],
|
||||
"plugins": [
|
||||
"transform-class-properties",
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ steps:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
|
||||
- label: ":karma: Tests"
|
||||
- label: ":jest: Tests"
|
||||
command:
|
||||
- "yarn install"
|
||||
- "yarn test"
|
||||
|
||||
@@ -12,10 +12,12 @@ module.exports = {
|
||||
// babel's transform-runtime converts references to ES6 globals such as
|
||||
// Promise and Map to core-js polyfills, so we can use ES6 globals.
|
||||
es6: true,
|
||||
jest: true,
|
||||
},
|
||||
extends: ["eslint:recommended", "google"],
|
||||
plugins: [
|
||||
"babel",
|
||||
"jest",
|
||||
],
|
||||
rules: {
|
||||
// rules we've always adhered to or now do
|
||||
|
||||
@@ -10,7 +10,6 @@ build/Release
|
||||
coverage
|
||||
lib-cov
|
||||
out
|
||||
reports
|
||||
/dist
|
||||
/lib
|
||||
/specbuild
|
||||
|
||||
+202
@@ -1,3 +1,205 @@
|
||||
Changes in [2.4.6](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.6) (2019-12-09)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.6-rc.1...v2.4.6)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [2.4.6-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v2.4.6-rc.1) (2019-12-04)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v2.4.5...v2.4.6-rc.1)
|
||||
|
||||
* Update alias handling
|
||||
[\#1102](https://github.com/matrix-org/matrix-js-sdk/pull/1102)
|
||||
* increase timeout on flush to fix failing unit test
|
||||
[\#1096](https://github.com/matrix-org/matrix-js-sdk/pull/1096)
|
||||
* Disable broken cross-signing test
|
||||
[\#1095](https://github.com/matrix-org/matrix-js-sdk/pull/1095)
|
||||
* Fix a couple SAS tests
|
||||
[\#1094](https://github.com/matrix-org/matrix-js-sdk/pull/1094)
|
||||
* Fix Olm unwedging test
|
||||
[\#1093](https://github.com/matrix-org/matrix-js-sdk/pull/1093)
|
||||
* Fix empty string handling in push notifications
|
||||
[\#1089](https://github.com/matrix-org/matrix-js-sdk/pull/1089)
|
||||
* expand e2ee logging to better debug UISIs
|
||||
[\#1090](https://github.com/matrix-org/matrix-js-sdk/pull/1090)
|
||||
* Remove Bluebird: phase 2
|
||||
[\#1087](https://github.com/matrix-org/matrix-js-sdk/pull/1087)
|
||||
* Relax identity server discovery checks to FAIL_PROMPT
|
||||
[\#1062](https://github.com/matrix-org/matrix-js-sdk/pull/1062)
|
||||
* Fix incorrect return value of MatrixClient.prototype.uploadKeys
|
||||
[\#1061](https://github.com/matrix-org/matrix-js-sdk/pull/1061)
|
||||
* Fix calls in e2e rooms
|
||||
[\#1086](https://github.com/matrix-org/matrix-js-sdk/pull/1086)
|
||||
* Monitor verification request over DM as well
|
||||
[\#1085](https://github.com/matrix-org/matrix-js-sdk/pull/1085)
|
||||
* Remove 'check' npm script
|
||||
[\#1084](https://github.com/matrix-org/matrix-js-sdk/pull/1084)
|
||||
* Always process call events in batches
|
||||
[\#1083](https://github.com/matrix-org/matrix-js-sdk/pull/1083)
|
||||
* Fix ringing chirp on loading
|
||||
[\#1082](https://github.com/matrix-org/matrix-js-sdk/pull/1082)
|
||||
* Remove *most* bluebird specific things
|
||||
[\#1081](https://github.com/matrix-org/matrix-js-sdk/pull/1081)
|
||||
* Switch to Jest
|
||||
[\#1080](https://github.com/matrix-org/matrix-js-sdk/pull/1080)
|
||||
|
||||
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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
+16
-21
@@ -1,14 +1,11 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "2.3.2",
|
||||
"version": "2.4.6",
|
||||
"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: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",
|
||||
"test:watch": "jest spec/ --coverage --testEnvironment node --watch",
|
||||
"test": "jest spec/ --coverage --testEnvironment node",
|
||||
"gendoc": "babel --no-babelrc --plugins transform-class-properties -d .jsdocbuild src && jsdoc -r .jsdocbuild -P package.json -R README.md -d .jsdoc",
|
||||
"start": "yarn start:init && yarn start:watch",
|
||||
"start:watch": "babel -s -w --skip-initial-build -d lib src",
|
||||
@@ -17,7 +14,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 +41,6 @@
|
||||
"git-revision.txt",
|
||||
"index.js",
|
||||
"browser-index.js",
|
||||
"jenkins.sh",
|
||||
"lib",
|
||||
"package.json",
|
||||
"release.sh",
|
||||
@@ -54,11 +50,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"
|
||||
@@ -66,28 +62,27 @@
|
||||
"devDependencies": {
|
||||
"babel-cli": "^6.18.0",
|
||||
"babel-eslint": "^10.0.1",
|
||||
"babel-jest": "^23.6.0",
|
||||
"babel-plugin-transform-async-to-bluebird": "^1.1.1",
|
||||
"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",
|
||||
"expect": "^1.20.2",
|
||||
"istanbul": "^0.4.5",
|
||||
"eslint-plugin-jest": "^23.0.4",
|
||||
"exorcist": "^1.0.1",
|
||||
"jest": "^23.6.0",
|
||||
"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",
|
||||
"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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
module.exports = {
|
||||
env: {
|
||||
mocha: true,
|
||||
},
|
||||
}
|
||||
+1
-2
@@ -24,7 +24,6 @@ import './olm-loader';
|
||||
import sdk from '..';
|
||||
import testUtils from './test-utils';
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
import LocalStorageCryptoStore from '../lib/crypto/store/localStorage-crypto-store';
|
||||
import logger from '../src/logger';
|
||||
@@ -159,7 +158,7 @@ TestClient.prototype.awaitOneTimeKeyUpload = function() {
|
||||
.respond(200, (path, content) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).toNotEqual({});
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
logger.log('%s: received %i one-time keys', this,
|
||||
Object.keys(content.one_time_keys).length);
|
||||
this.oneTimeKeys = content.one_time_keys;
|
||||
|
||||
@@ -15,7 +15,6 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import TestClient from '../TestClient';
|
||||
@@ -88,8 +87,6 @@ describe("DeviceList management:", function() {
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
// we create our own sessionStoreBackend so that we can use it for
|
||||
// another TestClient.
|
||||
sessionStoreBackend = new testUtils.MockStorageApi();
|
||||
|
||||
@@ -30,7 +30,6 @@ import 'source-map-support/register';
|
||||
// load olm before the sdk if possible
|
||||
import '../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
const sdk = require("../..");
|
||||
import Promise from 'bluebird';
|
||||
const utils = require("../../lib/utils");
|
||||
@@ -56,7 +55,7 @@ function bobUploadsDeviceKeys() {
|
||||
bobTestClient.client.uploadKeys(),
|
||||
bobTestClient.httpBackend.flush(),
|
||||
]).then(() => {
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).toNotEqual(0);
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -204,7 +203,7 @@ function aliSendsFirstMessage() {
|
||||
expectAliQueryKeys()
|
||||
.then(expectAliClaimKeys)
|
||||
.then(expectAliSendMessageRequest),
|
||||
]).spread(function(_, ciphertext) {
|
||||
]).then(function([_, ciphertext]) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
@@ -219,7 +218,7 @@ function aliSendsMessage() {
|
||||
return Promise.all([
|
||||
sendMessage(aliTestClient.client),
|
||||
expectAliSendMessageRequest(),
|
||||
]).spread(function(_, ciphertext) {
|
||||
]).then(function([_, ciphertext]) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
@@ -235,7 +234,7 @@ function bobSendsReplyMessage() {
|
||||
sendMessage(bobTestClient.client),
|
||||
expectBobQueryKeys()
|
||||
.then(expectBobSendMessageRequest),
|
||||
]).spread(function(_, ciphertext) {
|
||||
]).then(function([_, ciphertext]) {
|
||||
return ciphertext;
|
||||
});
|
||||
}
|
||||
@@ -280,16 +279,17 @@ function sendMessage(client) {
|
||||
|
||||
function expectSendMessageRequest(httpBackend) {
|
||||
const path = "/send/m.room.encrypted/";
|
||||
const deferred = Promise.defer();
|
||||
httpBackend.when("PUT", path).respond(200, function(path, content) {
|
||||
deferred.resolve(content);
|
||||
return {
|
||||
event_id: "asdfgh",
|
||||
};
|
||||
const prom = new Promise((resolve) => {
|
||||
httpBackend.when("PUT", path).respond(200, function(path, content) {
|
||||
resolve(content);
|
||||
return {
|
||||
event_id: "asdfgh",
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
// it can take a while to process the key query
|
||||
return httpBackend.flush(path, 1).then(() => deferred.promise);
|
||||
return httpBackend.flush(path, 1).then(() => prom);
|
||||
}
|
||||
|
||||
function aliRecvMessage() {
|
||||
@@ -406,8 +406,6 @@ describe("MatrixClient crypto", function() {
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
aliTestClient = new TestClient(aliUserId, aliDeviceId, aliAccessToken);
|
||||
await aliTestClient.client.initCrypto();
|
||||
|
||||
@@ -430,15 +428,14 @@ describe("MatrixClient crypto", function() {
|
||||
.then(bobUploadsDeviceKeys);
|
||||
});
|
||||
|
||||
it("Ali downloads Bobs device keys", function(done) {
|
||||
Promise.resolve()
|
||||
it("Ali downloads Bobs device keys", function() {
|
||||
return Promise.resolve()
|
||||
.then(bobUploadsDeviceKeys)
|
||||
.then(aliDownloadsKeys)
|
||||
.nodeify(done);
|
||||
.then(aliDownloadsKeys);
|
||||
});
|
||||
|
||||
it("Ali gets keys with an invalid signature", function(done) {
|
||||
Promise.resolve()
|
||||
it("Ali gets keys with an invalid signature", function() {
|
||||
return Promise.resolve()
|
||||
.then(bobUploadsDeviceKeys)
|
||||
.then(function() {
|
||||
// tamper bob's keys
|
||||
@@ -455,11 +452,10 @@ describe("MatrixClient crypto", function() {
|
||||
}).then((devices) => {
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
})
|
||||
.nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect userId", function(done) {
|
||||
it("Ali gets keys with an incorrect userId", function() {
|
||||
const eveUserId = "@eve:localhost";
|
||||
|
||||
const bobDeviceKeys = {
|
||||
@@ -488,7 +484,7 @@ describe("MatrixClient crypto", function() {
|
||||
return {device_keys: result};
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
return Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId, eveUserId]),
|
||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]).then(function() {
|
||||
@@ -496,14 +492,14 @@ describe("MatrixClient crypto", function() {
|
||||
aliTestClient.client.getStoredDevicesForUser(bobUserId),
|
||||
aliTestClient.client.getStoredDevicesForUser(eveUserId),
|
||||
]);
|
||||
}).spread((bobDevices, eveDevices) => {
|
||||
}).then(([bobDevices, eveDevices]) => {
|
||||
// should get an empty list
|
||||
expect(bobDevices).toEqual([]);
|
||||
expect(eveDevices).toEqual([]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("Ali gets keys with an incorrect deviceId", function(done) {
|
||||
it("Ali gets keys with an incorrect deviceId", function() {
|
||||
const bobDeviceKeys = {
|
||||
algorithms: ['m.olm.v1.curve25519-aes-sha2', 'm.megolm.v1.aes-sha2'],
|
||||
device_id: 'bad_device',
|
||||
@@ -530,7 +526,7 @@ describe("MatrixClient crypto", function() {
|
||||
return {device_keys: result};
|
||||
});
|
||||
|
||||
Promise.all([
|
||||
return Promise.all([
|
||||
aliTestClient.client.downloadKeys([bobUserId]),
|
||||
aliTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]).then(function() {
|
||||
@@ -538,7 +534,7 @@ describe("MatrixClient crypto", function() {
|
||||
}).then((devices) => {
|
||||
// should get an empty list
|
||||
expect(devices).toEqual([]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -548,19 +544,18 @@ describe("MatrixClient crypto", function() {
|
||||
.then(() => bobTestClient.awaitOneTimeKeyUpload())
|
||||
.then((keys) => {
|
||||
expect(Object.keys(keys).length).toEqual(5);
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).toNotEqual(0);
|
||||
expect(Object.keys(bobTestClient.deviceKeys).length).not.toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
it("Ali sends a message", function(done) {
|
||||
it("Ali sends a message", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
Promise.resolve()
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
.then(aliEnablesEncryption)
|
||||
.then(aliSendsFirstMessage)
|
||||
.nodeify(done);
|
||||
.then(aliSendsFirstMessage);
|
||||
});
|
||||
|
||||
it("Bob receives a message", function() {
|
||||
@@ -628,9 +623,9 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
});
|
||||
|
||||
it("Ali blocks Bob's device", function(done) {
|
||||
it("Ali blocks Bob's device", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
Promise.resolve()
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
@@ -645,12 +640,12 @@ describe("MatrixClient crypto", function() {
|
||||
expect(sentContent.ciphertext).toEqual({});
|
||||
});
|
||||
return Promise.all([p1, p2]);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("Bob receives two pre-key messages", function(done) {
|
||||
it("Bob receives two pre-key messages", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
Promise.resolve()
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
@@ -658,8 +653,7 @@ describe("MatrixClient crypto", function() {
|
||||
.then(aliSendsFirstMessage)
|
||||
.then(bobRecvMessage)
|
||||
.then(aliSendsMessage)
|
||||
.then(bobRecvMessage)
|
||||
.nodeify(done);
|
||||
.then(bobRecvMessage);
|
||||
});
|
||||
|
||||
it("Bob replies to the message", function() {
|
||||
@@ -753,9 +747,9 @@ describe("MatrixClient crypto", function() {
|
||||
.then(() => httpBackend.when("POST", "/keys/upload")
|
||||
.respond(200, (path, content) => {
|
||||
expect(content.one_time_keys).toBeTruthy();
|
||||
expect(content.one_time_keys).toNotEqual({});
|
||||
expect(content.one_time_keys).not.toEqual({});
|
||||
expect(Object.keys(content.one_time_keys).length)
|
||||
.toBeGreaterThanOrEqualTo(1);
|
||||
.toBeGreaterThanOrEqual(1);
|
||||
logger.log('received %i one-time keys',
|
||||
Object.keys(content.one_time_keys).length);
|
||||
// cancel futher calls by telling the client
|
||||
|
||||
@@ -4,7 +4,6 @@ const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
describe("MatrixClient events", function() {
|
||||
@@ -15,7 +14,6 @@ describe("MatrixClient events", function() {
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
@@ -219,7 +217,7 @@ describe("MatrixClient events", function() {
|
||||
client.on("RoomState.events", function(event, state) {
|
||||
eventsInvokeCount++;
|
||||
const index = roomStateEventTypes.indexOf(event.getType());
|
||||
expect(index).toNotEqual(
|
||||
expect(index).not.toEqual(
|
||||
-1, "Unexpected room state event type: " + event.getType(),
|
||||
);
|
||||
if (index >= 0) {
|
||||
|
||||
@@ -83,18 +83,19 @@ function startClient(httpBackend, client) {
|
||||
client.startClient();
|
||||
|
||||
// set up a promise which will resolve once the client is initialised
|
||||
const deferred = Promise.defer();
|
||||
client.on("sync", function(state) {
|
||||
logger.log("sync", state);
|
||||
if (state != "SYNCING") {
|
||||
return;
|
||||
}
|
||||
deferred.resolve();
|
||||
const prom = new Promise((resolve) => {
|
||||
client.on("sync", function(state) {
|
||||
logger.log("sync", state);
|
||||
if (state != "SYNCING") {
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
deferred.promise,
|
||||
prom,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -103,7 +104,6 @@ describe("getEventTimeline support", function() {
|
||||
let client;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
});
|
||||
@@ -115,21 +115,20 @@ describe("getEventTimeline support", function() {
|
||||
return httpBackend.stop();
|
||||
});
|
||||
|
||||
it("timeline support must be enabled to work", function(done) {
|
||||
it("timeline support must be enabled to work", function() {
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
});
|
||||
|
||||
startClient(httpBackend, client,
|
||||
).then(function() {
|
||||
return startClient(httpBackend, client).then(function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() {
|
||||
client.getEventTimeline(timelineSet, "event");
|
||||
}).toThrow();
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("timeline support works when enabled", function() {
|
||||
@@ -145,13 +144,13 @@ describe("getEventTimeline support", function() {
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
expect(function() {
|
||||
client.getEventTimeline(timelineSet, "event");
|
||||
}).toNotThrow();
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("scrollback should be able to scroll back to before a gappy /sync",
|
||||
function(done) {
|
||||
function() {
|
||||
// need a client with timelineSupport disabled to make this work
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
@@ -160,8 +159,7 @@ describe("getEventTimeline support", function() {
|
||||
});
|
||||
let room;
|
||||
|
||||
startClient(httpBackend, client,
|
||||
).then(function() {
|
||||
return startClient(httpBackend, client).then(function() {
|
||||
room = client.getRoom(roomId);
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, {
|
||||
@@ -217,18 +215,15 @@ describe("getEventTimeline support", function() {
|
||||
expect(room.timeline[0].event).toEqual(EVENTS[0]);
|
||||
expect(room.timeline[1].event).toEqual(EVENTS[1]);
|
||||
expect(room.oldState.paginationToken).toEqual("pagin_end");
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("MatrixClient event timelines", function() {
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
|
||||
@@ -349,25 +344,25 @@ describe("MatrixClient event timelines", function() {
|
||||
};
|
||||
});
|
||||
|
||||
const deferred = Promise.defer();
|
||||
client.on("sync", function() {
|
||||
client.getEventTimeline(timelineSet, EVENTS[2].event_id,
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getEvents()[3].event).toEqual(EVENTS[3]);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token");
|
||||
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
// .toEqual("s_5_4");
|
||||
}).done(() => deferred.resolve(),
|
||||
(e) => deferred.reject(e));
|
||||
const prom = new Promise((resolve, reject) => {
|
||||
client.on("sync", function() {
|
||||
client.getEventTimeline(timelineSet, EVENTS[2].event_id,
|
||||
).then(function(tl) {
|
||||
expect(tl.getEvents().length).toEqual(4);
|
||||
expect(tl.getEvents()[0].event).toEqual(EVENTS[1]);
|
||||
expect(tl.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||
expect(tl.getEvents()[3].event).toEqual(EVENTS[3]);
|
||||
expect(tl.getPaginationToken(EventTimeline.BACKWARDS))
|
||||
.toEqual("start_token");
|
||||
// expect(tl.getPaginationToken(EventTimeline.FORWARDS))
|
||||
// .toEqual("s_5_4");
|
||||
}).done(resolve, reject);
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
deferred.promise,
|
||||
prom,
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -697,7 +692,7 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
|
||||
|
||||
it("should handle gappy syncs after redactions", function(done) {
|
||||
it("should handle gappy syncs after redactions", function() {
|
||||
// https://github.com/vector-im/vector-web/issues/1389
|
||||
|
||||
// a state event, followed by a redaction thereof
|
||||
@@ -729,7 +724,7 @@ describe("MatrixClient event timelines", function() {
|
||||
};
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
|
||||
Promise.all([
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
utils.syncPromise(client),
|
||||
]).then(function() {
|
||||
@@ -765,6 +760,6 @@ describe("MatrixClient event timelines", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const tl = room.getLiveTimeline();
|
||||
expect(tl.getEvents().length).toEqual(1);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,8 +9,6 @@ const Filter = publicGlobals.Filter;
|
||||
const utils = require("../test-utils");
|
||||
const MockStorageApi = require("../MockStorageApi");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
@@ -21,7 +19,6 @@ describe("MatrixClient", function() {
|
||||
const accessToken = "aseukfgwef";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
store = new MemoryStore();
|
||||
|
||||
@@ -46,7 +43,7 @@ describe("MatrixClient", function() {
|
||||
|
||||
describe("uploadContent", function() {
|
||||
const buf = new Buffer('hello world');
|
||||
it("should upload the file", function(done) {
|
||||
it("should upload the file", function() {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/r0/upload",
|
||||
).check(function(req) {
|
||||
@@ -74,25 +71,26 @@ describe("MatrixClient", function() {
|
||||
expect(uploads[0].promise).toBe(prom);
|
||||
expect(uploads[0].loaded).toEqual(0);
|
||||
|
||||
prom.then(function(response) {
|
||||
const prom2 = prom.then(function(response) {
|
||||
// for backwards compatibility, we return the raw JSON
|
||||
expect(response).toEqual("content");
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(0);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
return prom2;
|
||||
});
|
||||
|
||||
it("should parse the response if rawResponse=false", function(done) {
|
||||
it("should parse the response if rawResponse=false", function() {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/r0/upload",
|
||||
).check(function(req) {
|
||||
expect(req.opts.json).toBeFalsy();
|
||||
}).respond(200, { "content_uri": "uri" });
|
||||
|
||||
client.uploadContent({
|
||||
const prom = client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
@@ -100,12 +98,13 @@ describe("MatrixClient", function() {
|
||||
rawResponse: false,
|
||||
}).then(function(response) {
|
||||
expect(response.content_uri).toEqual("uri");
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
return prom;
|
||||
});
|
||||
|
||||
it("should parse errors into a MatrixError", function(done) {
|
||||
it("should parse errors into a MatrixError", function() {
|
||||
httpBackend.when(
|
||||
"POST", "/_matrix/media/r0/upload",
|
||||
).check(function(req) {
|
||||
@@ -116,7 +115,7 @@ describe("MatrixClient", function() {
|
||||
"error": "broken",
|
||||
});
|
||||
|
||||
client.uploadContent({
|
||||
const prom = client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
type: "text/plain",
|
||||
@@ -126,12 +125,13 @@ describe("MatrixClient", function() {
|
||||
expect(error.httpStatus).toEqual(400);
|
||||
expect(error.errcode).toEqual("M_SNAFU");
|
||||
expect(error.message).toEqual("broken");
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
return prom;
|
||||
});
|
||||
|
||||
it("should return a promise which can be cancelled", function(done) {
|
||||
it("should return a promise which can be cancelled", function() {
|
||||
const prom = client.uploadContent({
|
||||
stream: buf,
|
||||
name: "hi.txt",
|
||||
@@ -143,17 +143,18 @@ describe("MatrixClient", function() {
|
||||
expect(uploads[0].promise).toBe(prom);
|
||||
expect(uploads[0].loaded).toEqual(0);
|
||||
|
||||
prom.then(function(response) {
|
||||
const prom2 = prom.then(function(response) {
|
||||
throw Error("request not aborted");
|
||||
}, function(error) {
|
||||
expect(error).toEqual("aborted");
|
||||
|
||||
const uploads = client.getCurrentUploads();
|
||||
expect(uploads.length).toEqual(0);
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
const r = client.cancelUpload(prom);
|
||||
expect(r).toBe(true);
|
||||
return prom2;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -310,7 +311,7 @@ describe("MatrixClient", function() {
|
||||
return client.initCrypto();
|
||||
});
|
||||
|
||||
it("should do an HTTP request and then store the keys", function(done) {
|
||||
it("should do an HTTP request and then store the keys", function() {
|
||||
const ed25519key = "7wG2lzAqbjcyEkOP7O4gU7ItYcn+chKzh5sT/5r2l78";
|
||||
// ed25519key = client.getDeviceEd25519Key();
|
||||
const borisKeys = {
|
||||
@@ -372,7 +373,7 @@ describe("MatrixClient", function() {
|
||||
},
|
||||
});
|
||||
|
||||
client.downloadKeys(["boris", "chaz"]).then(function(res) {
|
||||
const prom = client.downloadKeys(["boris", "chaz"]).then(function(res) {
|
||||
assertObjectContains(res.boris.dev1, {
|
||||
verified: 0, // DeviceVerification.UNVERIFIED
|
||||
keys: { "ed25519:dev1": ed25519key },
|
||||
@@ -386,26 +387,26 @@ describe("MatrixClient", function() {
|
||||
algorithms: ["2"],
|
||||
unsigned: { "ghi": "def" },
|
||||
});
|
||||
}).nodeify(done);
|
||||
});
|
||||
|
||||
httpBackend.flush();
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteDevice", function() {
|
||||
const auth = {a: 1};
|
||||
it("should pass through an auth dict", function(done) {
|
||||
it("should pass through an auth dict", function() {
|
||||
httpBackend.when(
|
||||
"DELETE", "/_matrix/client/r0/devices/my_device",
|
||||
).check(function(req) {
|
||||
expect(req.data).toEqual({auth: auth});
|
||||
}).respond(200);
|
||||
|
||||
client.deleteDevice(
|
||||
"my_device", auth,
|
||||
).nodeify(done);
|
||||
const prom = client.deleteDevice("my_device", auth);
|
||||
|
||||
httpBackend.flush();
|
||||
return prom;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,7 +5,6 @@ const MatrixClient = sdk.MatrixClient;
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
describe("MatrixClient opts", function() {
|
||||
@@ -58,7 +57,6 @@ describe("MatrixClient opts", function() {
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
});
|
||||
|
||||
@@ -101,7 +99,7 @@ describe("MatrixClient opts", function() {
|
||||
"m.room.create",
|
||||
];
|
||||
client.on("event", function(event) {
|
||||
expect(expectedEventTypes.indexOf(event.getType())).toNotEqual(
|
||||
expect(expectedEventTypes.indexOf(event.getType())).not.toEqual(
|
||||
-1, "Recv unexpected event type: " + event.getType(),
|
||||
);
|
||||
expectedEventTypes.splice(
|
||||
|
||||
@@ -4,11 +4,8 @@ import Promise from 'bluebird';
|
||||
|
||||
const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
const EventStatus = sdk.EventStatus;
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("MatrixClient retrying", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
@@ -20,7 +17,6 @@ describe("MatrixClient retrying", function() {
|
||||
let room;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
scheduler = new sdk.MatrixScheduler();
|
||||
|
||||
@@ -6,7 +6,6 @@ const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import expect from 'expect';
|
||||
|
||||
describe("MatrixClient room timelines", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
@@ -103,8 +102,7 @@ describe("MatrixClient room timelines", function() {
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(function(done) {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
beforeEach(function() {
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
@@ -122,9 +120,9 @@ describe("MatrixClient room timelines", function() {
|
||||
return NEXT_SYNC_DATA;
|
||||
});
|
||||
client.startClient();
|
||||
httpBackend.flush("/pushrules").then(function() {
|
||||
return httpBackend.flush("/pushrules").then(function() {
|
||||
return httpBackend.flush("/filter");
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
|
||||
@@ -6,7 +6,6 @@ const utils = require("../test-utils");
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
const EventTimeline = sdk.EventTimeline;
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
describe("MatrixClient syncing", function() {
|
||||
@@ -23,7 +22,6 @@ describe("MatrixClient syncing", function() {
|
||||
const roomTwo = "!bar:localhost";
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
@@ -528,7 +526,7 @@ describe("MatrixClient syncing", function() {
|
||||
awaitSyncEvent(),
|
||||
]).then(function() {
|
||||
const room = client.getRoom(roomTwo);
|
||||
expect(room).toExist();
|
||||
expect(room).toBeDefined();
|
||||
const tok = room.getLiveTimeline()
|
||||
.getPaginationToken(EventTimeline.BACKWARDS);
|
||||
expect(tok).toEqual("roomtwotok");
|
||||
@@ -693,12 +691,12 @@ describe("MatrixClient syncing", function() {
|
||||
include_leave: true }});
|
||||
}).respond(200, { filter_id: "another_id" });
|
||||
|
||||
const defer = Promise.defer();
|
||||
|
||||
httpBackend.when("GET", "/sync").check(function(req) {
|
||||
expect(req.queryParams.filter).toEqual("another_id");
|
||||
defer.resolve();
|
||||
}).respond(200, {});
|
||||
const prom = new Promise((resolve) => {
|
||||
httpBackend.when("GET", "/sync").check(function(req) {
|
||||
expect(req.queryParams.filter).toEqual("another_id");
|
||||
resolve();
|
||||
}).respond(200, {});
|
||||
});
|
||||
|
||||
client.syncLeftRooms();
|
||||
|
||||
@@ -709,7 +707,7 @@ describe("MatrixClient syncing", function() {
|
||||
// flush the syncs
|
||||
return httpBackend.flushAllExpected();
|
||||
}),
|
||||
defer.promise,
|
||||
prom,
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ limitations under the License.
|
||||
|
||||
const anotherjson = require('another-json');
|
||||
import Promise from 'bluebird';
|
||||
import expect from 'expect';
|
||||
|
||||
const utils = require('../../lib/utils');
|
||||
const testUtils = require('../test-utils');
|
||||
@@ -283,8 +282,6 @@ describe("megolm", function() {
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
aliceTestClient = new TestClient(
|
||||
"@alice:localhost", "xzcvb", "akjgkrgjs",
|
||||
);
|
||||
@@ -713,7 +710,7 @@ describe("megolm", function() {
|
||||
'PUT', '/send/',
|
||||
).respond(200, function(path, content) {
|
||||
logger.log('/send:', content);
|
||||
expect(content.session_id).toNotEqual(megolmSessionId);
|
||||
expect(content.session_id).not.toEqual(megolmSessionId);
|
||||
return {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
|
||||
+142
-14
@@ -1,5 +1,4 @@
|
||||
"use strict";
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
// load olm before the sdk if possible
|
||||
@@ -41,18 +40,6 @@ module.exports.syncPromise = function(client, count) {
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Perform common actions before each test case, e.g. printing the test case
|
||||
* name to stdout.
|
||||
* @param {Mocha.Context} context The test context
|
||||
*/
|
||||
module.exports.beforeEach = function(context) {
|
||||
const desc = context.currentTest.fullTitle();
|
||||
|
||||
logger.log(desc);
|
||||
logger.log(new Array(1 + desc.length).join("="));
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a spy for an object and automatically spy its methods.
|
||||
* @param {*} constr The class constructor (used with 'new')
|
||||
@@ -71,7 +58,7 @@ module.exports.mock = function(constr, name) {
|
||||
for (const key in constr.prototype) { // eslint-disable-line guard-for-in
|
||||
try {
|
||||
if (constr.prototype[key] instanceof Function) {
|
||||
result[key] = expect.createSpy();
|
||||
result[key] = jest.fn();
|
||||
}
|
||||
} catch (ex) {
|
||||
// Direct access to some non-function fields of DOM prototypes may
|
||||
@@ -242,3 +229,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] = jest.fn(); return r;}, {});
|
||||
client._http.authedRequest.mockImplementation(httpReq);
|
||||
client._http.authedRequestWithPrefix.mockImplementation(httpReq);
|
||||
client._http.requestWithPrefix.mockImplementation(httpReq);
|
||||
client._http.request.mockImplementation(httpReq);
|
||||
};
|
||||
|
||||
@@ -18,11 +18,9 @@ limitations under the License.
|
||||
import 'source-map-support/register';
|
||||
import Promise from 'bluebird';
|
||||
const sdk = require("../..");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
const AutoDiscovery = sdk.AutoDiscovery;
|
||||
|
||||
import expect from 'expect';
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
|
||||
@@ -30,7 +28,6 @@ describe("AutoDiscovery", function() {
|
||||
let httpBackend = null;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
httpBackend = new MockHttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
});
|
||||
@@ -416,8 +413,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 +435,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 +453,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 +475,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 +493,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 +516,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 +534,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 +557,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",
|
||||
},
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const ContentRepo = require("../../lib/content-repo");
|
||||
const testUtils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
const baseUrl = "https://my.home.server";
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe("getHttpUriForMxc", function() {
|
||||
it("should do nothing to HTTP URLs when allowing direct links", function() {
|
||||
const httpUrl = "http://example.com/image.jpeg";
|
||||
|
||||
+41
-46
@@ -3,7 +3,6 @@ import 'source-map-support/register';
|
||||
import '../olm-loader';
|
||||
|
||||
import Crypto from '../../lib/crypto';
|
||||
import expect from 'expect';
|
||||
|
||||
import WebStorageSessionStore from '../../lib/store/session/webstorage';
|
||||
import MemoryCryptoStore from '../../lib/crypto/store/memory-crypto-store.js';
|
||||
@@ -12,7 +11,6 @@ import TestClient from '../TestClient';
|
||||
import {MatrixEvent} from '../../lib/models/event';
|
||||
import Room from '../../lib/models/room';
|
||||
import olmlib from '../../lib/crypto/olmlib';
|
||||
import lolex from 'lolex';
|
||||
|
||||
const EventEmitter = require("events").EventEmitter;
|
||||
|
||||
@@ -20,13 +18,15 @@ const sdk = require("../..");
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("Crypto", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(function(done) {
|
||||
Olm.init().then(done);
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
it("Crypto exposes the correct olm library version", function() {
|
||||
@@ -76,9 +76,9 @@ describe("Crypto", function() {
|
||||
});
|
||||
|
||||
mockBaseApis = {
|
||||
sendToDevice: expect.createSpy(),
|
||||
getKeyBackupVersion: expect.createSpy(),
|
||||
isGuest: expect.createSpy(),
|
||||
sendToDevice: jest.fn(),
|
||||
getKeyBackupVersion: jest.fn(),
|
||||
isGuest: jest.fn(),
|
||||
};
|
||||
mockRoomList = {};
|
||||
|
||||
@@ -110,15 +110,16 @@ describe("Crypto", function() {
|
||||
});
|
||||
|
||||
fakeEmitter.emit('toDeviceEvent', {
|
||||
getType: expect.createSpy().andReturn('m.room.message'),
|
||||
getContent: expect.createSpy().andReturn({
|
||||
getId: jest.fn().mockReturnValue("$wedged"),
|
||||
getType: jest.fn().mockReturnValue('m.room.message'),
|
||||
getContent: jest.fn().mockReturnValue({
|
||||
msgtype: 'm.bad.encrypted',
|
||||
}),
|
||||
getWireContent: expect.createSpy().andReturn({
|
||||
getWireContent: jest.fn().mockReturnValue({
|
||||
algorithm: 'm.olm.v1.curve25519-aes-sha2',
|
||||
sender_key: 'this is a key',
|
||||
}),
|
||||
getSender: expect.createSpy().andReturn('@bob:home.server'),
|
||||
getSender: jest.fn().mockReturnValue('@bob:home.server'),
|
||||
});
|
||||
|
||||
await prom;
|
||||
@@ -245,7 +246,7 @@ describe("Crypto", function() {
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await eventPromise;
|
||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).toNotBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
|
||||
const cryptoStore = bobClient._cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
@@ -260,7 +261,7 @@ describe("Crypto", function() {
|
||||
// the room key request should still be there, since we haven't
|
||||
// decrypted everything
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toExist();
|
||||
.toBeDefined();
|
||||
|
||||
// keyshare the session key starting at the first message, so
|
||||
// that it can now be decrypted
|
||||
@@ -268,10 +269,10 @@ describe("Crypto", function() {
|
||||
ksEvent = await keyshareEventForEvent(events[0], 0);
|
||||
await bobDecryptor.onRoomKeyEvent(ksEvent);
|
||||
await eventPromise;
|
||||
expect(events[0].getContent().msgtype).toNotBe("m.bad.encrypted");
|
||||
expect(events[0].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
// the room key request should be gone since we've now decypted everything
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toNotExist();
|
||||
.toBeFalsy();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -296,7 +297,7 @@ describe("Crypto", function() {
|
||||
sender_key: "senderkey",
|
||||
};
|
||||
expect(await cryptoStore.getOutgoingRoomKeyRequest(roomKeyRequestBody))
|
||||
.toExist();
|
||||
.toBeDefined();
|
||||
});
|
||||
|
||||
it("uses a new txnid for re-requesting keys", async function() {
|
||||
@@ -329,38 +330,32 @@ describe("Crypto", function() {
|
||||
|
||||
aliceClient.startClient();
|
||||
|
||||
const clock = lolex.install();
|
||||
let promise;
|
||||
// make a room key request, and record the transaction ID for the
|
||||
// sendToDevice call
|
||||
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
jest.runAllTimers();
|
||||
let args = await promise;
|
||||
const txnId = args[2];
|
||||
jest.runAllTimers();
|
||||
|
||||
try {
|
||||
let promise;
|
||||
// make a room key request, and record the transaction ID for the
|
||||
// sendToDevice call
|
||||
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
clock.runToLast();
|
||||
let args = await promise;
|
||||
const txnId = args[2];
|
||||
clock.runToLast();
|
||||
// give the room key request manager time to update the state
|
||||
// of the request
|
||||
await Promise.resolve();
|
||||
|
||||
// give the room key request manager time to update the state
|
||||
// of the request
|
||||
await Promise.resolve();
|
||||
|
||||
// cancel and resend the room key request
|
||||
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
clock.runToLast();
|
||||
// the first call to sendToDevice will be the cancellation
|
||||
args = await promise;
|
||||
// the second call to sendToDevice will be the key request
|
||||
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
||||
clock.runToLast();
|
||||
args = await promise;
|
||||
clock.runToLast();
|
||||
expect(args[2]).toNotBe(txnId);
|
||||
} finally {
|
||||
clock.uninstall();
|
||||
}
|
||||
// cancel and resend the room key request
|
||||
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
jest.runAllTimers();
|
||||
// the first call to sendToDevice will be the cancellation
|
||||
args = await promise;
|
||||
// the second call to sendToDevice will be the key request
|
||||
({promise, func: aliceClient.sendToDevice} = awaitFunctionCall());
|
||||
jest.runAllTimers();
|
||||
args = await promise;
|
||||
jest.runAllTimers();
|
||||
expect(args[2]).not.toBe(txnId);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,11 +17,9 @@ limitations under the License.
|
||||
|
||||
import DeviceList from '../../../lib/crypto/DeviceList';
|
||||
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
|
||||
import testUtils from '../../test-utils';
|
||||
import utils from '../../../lib/utils';
|
||||
import logger from '../../../src/logger';
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
const signedDeviceList = {
|
||||
@@ -60,11 +58,9 @@ describe('DeviceList', function() {
|
||||
let deviceLists = [];
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
deviceLists = [];
|
||||
|
||||
downloadSpy = expect.createSpy();
|
||||
downloadSpy = jest.fn();
|
||||
cryptoStore = new MemoryCryptoStore();
|
||||
});
|
||||
|
||||
@@ -92,7 +88,7 @@ describe('DeviceList', function() {
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
|
||||
const queryDefer1 = Promise.defer();
|
||||
downloadSpy.andReturn(queryDefer1.promise);
|
||||
downloadSpy.mockReturnValue(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
@@ -111,15 +107,15 @@ describe('DeviceList', function() {
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
|
||||
const queryDefer1 = Promise.defer();
|
||||
downloadSpy.andReturn(queryDefer1.promise);
|
||||
downloadSpy.mockReturnValue(queryDefer1.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
downloadSpy.reset();
|
||||
downloadSpy.mockReset();
|
||||
|
||||
// outdated notif arrives while the request is in flight.
|
||||
const queryDefer2 = Promise.defer();
|
||||
downloadSpy.andReturn(queryDefer2.promise);
|
||||
downloadSpy.mockReturnValue(queryDefer2.promise);
|
||||
|
||||
dl.invalidateUserDeviceList('@test1:sw1v.org');
|
||||
dl.refreshOutdatedDeviceLists();
|
||||
@@ -136,10 +132,10 @@ describe('DeviceList', function() {
|
||||
// uh-oh; user restarts before second request completes. The new instance
|
||||
// should know we never got a complete device list.
|
||||
logger.log("Creating new devicelist to simulate app reload");
|
||||
downloadSpy.reset();
|
||||
downloadSpy.mockReset();
|
||||
const dl2 = createTestDeviceList();
|
||||
const queryDefer3 = Promise.defer();
|
||||
downloadSpy.andReturn(queryDefer3.promise);
|
||||
downloadSpy.mockReturnValue(queryDefer3.promise);
|
||||
|
||||
const prom3 = dl2.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toHaveBeenCalledWith(['@test1:sw1v.org'], {});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import '../../../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import sdk from '../../../..';
|
||||
@@ -26,16 +25,16 @@ describe("MegolmDecryption", function() {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
let megolmDecryption;
|
||||
let mockOlmLib;
|
||||
let mockCrypto;
|
||||
let mockBaseApis;
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
await Olm.init();
|
||||
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||
mockBaseApis = {};
|
||||
|
||||
@@ -55,9 +54,9 @@ describe("MegolmDecryption", function() {
|
||||
|
||||
// we stub out the olm encryption bits
|
||||
mockOlmLib = {};
|
||||
mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy();
|
||||
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
|
||||
mockOlmLib.encryptMessageForDevice =
|
||||
expect.createSpy().andReturn(Promise.resolve());
|
||||
jest.fn().mockReturnValue(Promise.resolve());
|
||||
megolmDecryption.olmlib = mockOlmLib;
|
||||
});
|
||||
|
||||
@@ -135,22 +134,22 @@ describe("MegolmDecryption", function() {
|
||||
|
||||
// set up some pre-conditions for the share call
|
||||
const deviceInfo = {};
|
||||
mockCrypto.getStoredDevice.andReturn(deviceInfo);
|
||||
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
|
||||
|
||||
mockOlmLib.ensureOlmSessionsForDevices.andReturn(
|
||||
mockOlmLib.ensureOlmSessionsForDevices.mockReturnValue(
|
||||
Promise.resolve({'@alice:foo': {'alidevice': {
|
||||
sessionId: 'alisession',
|
||||
}}}),
|
||||
);
|
||||
|
||||
const awaitEncryptForDevice = new Promise((res, rej) => {
|
||||
mockOlmLib.encryptMessageForDevice.andCall(() => {
|
||||
mockOlmLib.encryptMessageForDevice.mockImplementation(() => {
|
||||
res();
|
||||
return Promise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
mockBaseApis.sendToDevice = expect.createSpy();
|
||||
mockBaseApis.sendToDevice = jest.fn();
|
||||
|
||||
// do the share
|
||||
megolmDecryption.shareKeysWithDevice(keyRequest);
|
||||
@@ -160,21 +159,20 @@ describe("MegolmDecryption", function() {
|
||||
}).then(() => {
|
||||
// check that it called encryptMessageForDevice with
|
||||
// appropriate args.
|
||||
expect(mockOlmLib.encryptMessageForDevice.calls.length)
|
||||
.toEqual(1);
|
||||
expect(mockOlmLib.encryptMessageForDevice).toBeCalledTimes(1);
|
||||
|
||||
const call = mockOlmLib.encryptMessageForDevice.calls[0];
|
||||
const payload = call.arguments[6];
|
||||
const call = mockOlmLib.encryptMessageForDevice.mock.calls[0];
|
||||
const payload = call[6];
|
||||
|
||||
expect(payload.type).toEqual("m.forwarded_room_key");
|
||||
expect(payload.content).toInclude({
|
||||
expect(payload.content).toMatchObject({
|
||||
sender_key: "SENDER_CURVE25519",
|
||||
sender_claimed_ed25519_key: "SENDER_ED25519",
|
||||
session_id: groupSession.session_id(),
|
||||
chain_index: 0,
|
||||
forwarding_curve25519_key_chain: [],
|
||||
});
|
||||
expect(payload.content.session_key).toExist();
|
||||
expect(payload.content.session_key).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -201,13 +199,12 @@ describe("MegolmDecryption", function() {
|
||||
origin_server_ts: 1507753886000,
|
||||
});
|
||||
|
||||
const successHandler = expect.createSpy();
|
||||
const failureHandler = expect.createSpy()
|
||||
.andCall((err) => {
|
||||
expect(err.toString()).toMatch(
|
||||
/Duplicate message index, possible replay attack/,
|
||||
);
|
||||
});
|
||||
const successHandler = jest.fn();
|
||||
const failureHandler = jest.fn((err) => {
|
||||
expect(err.toString()).toMatch(
|
||||
/Duplicate message index, possible replay attack/,
|
||||
);
|
||||
});
|
||||
|
||||
return megolmDecryption.decryptEvent(event1).then((res) => {
|
||||
const event2 = new MatrixEvent({
|
||||
@@ -228,7 +225,7 @@ describe("MegolmDecryption", function() {
|
||||
successHandler,
|
||||
failureHandler,
|
||||
).then(() => {
|
||||
expect(successHandler).toNotHaveBeenCalled();
|
||||
expect(successHandler).not.toHaveBeenCalled();
|
||||
expect(failureHandler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -266,10 +263,10 @@ describe("MegolmDecryption", function() {
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
const olmDevice = new OlmDevice(cryptoStore);
|
||||
olmDevice.verifySignature = expect.createSpy();
|
||||
olmDevice.verifySignature = jest.fn();
|
||||
await olmDevice.init();
|
||||
|
||||
mockBaseApis.claimOneTimeKeys = expect.createSpy().andReturn(Promise.resolve({
|
||||
mockBaseApis.claimOneTimeKeys = jest.fn().mockReturnValue(Promise.resolve({
|
||||
one_time_keys: {
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
@@ -285,18 +282,18 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
},
|
||||
}));
|
||||
mockBaseApis.sendToDevice = expect.createSpy().andReturn(Promise.resolve());
|
||||
mockBaseApis.sendToDevice = jest.fn().mockReturnValue(Promise.resolve());
|
||||
|
||||
mockCrypto.downloadKeys.andReturn(Promise.resolve({
|
||||
mockCrypto.downloadKeys.mockReturnValue(Promise.resolve({
|
||||
'@alice:home.server': {
|
||||
aliceDevice: {
|
||||
deviceId: 'aliceDevice',
|
||||
isBlocked: expect.createSpy().andReturn(false),
|
||||
isUnverified: expect.createSpy().andReturn(false),
|
||||
getIdentityKey: expect.createSpy().andReturn(
|
||||
isBlocked: jest.fn().mockReturnValue(false),
|
||||
isUnverified: jest.fn().mockReturnValue(false),
|
||||
getIdentityKey: jest.fn().mockReturnValue(
|
||||
'YWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWE',
|
||||
),
|
||||
getFingerprint: expect.createSpy().andReturn(''),
|
||||
getFingerprint: jest.fn().mockReturnValue(''),
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -312,10 +309,10 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
});
|
||||
const mockRoom = {
|
||||
getEncryptionTargetMembers: expect.createSpy().andReturn(
|
||||
getEncryptionTargetMembers: jest.fn().mockReturnValue(
|
||||
[{userId: "@alice:home.server"}],
|
||||
),
|
||||
getBlacklistUnverifiedDevices: expect.createSpy().andReturn(false),
|
||||
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
const ct1 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some text",
|
||||
@@ -323,25 +320,25 @@ describe("MegolmDecryption", function() {
|
||||
expect(mockRoom.getEncryptionTargetMembers).toHaveBeenCalled();
|
||||
|
||||
// this should have claimed a key for alice as it's starting a new session
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
|
||||
);
|
||||
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
|
||||
['@alice:home.server'], false,
|
||||
);
|
||||
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalled(
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
|
||||
);
|
||||
|
||||
mockBaseApis.claimOneTimeKeys.reset();
|
||||
mockBaseApis.claimOneTimeKeys.mockReset();
|
||||
|
||||
const ct2 = await megolmEncryption.encryptMessage(mockRoom, "a.fake.type", {
|
||||
body: "Some more text",
|
||||
});
|
||||
|
||||
// this should *not* have claimed a key as it should be using the same session
|
||||
expect(mockBaseApis.claimOneTimeKeys).toNotHaveBeenCalled();
|
||||
expect(mockBaseApis.claimOneTimeKeys).not.toHaveBeenCalled();
|
||||
|
||||
// likewise they should show the same session ID
|
||||
expect(ct2.session_id).toEqual(ct1.session_id);
|
||||
|
||||
@@ -16,10 +16,8 @@ limitations under the License.
|
||||
|
||||
import '../../../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
|
||||
import MockStorageApi from '../../../MockStorageApi';
|
||||
import testUtils from '../../../test-utils';
|
||||
import logger from '../../../../src/logger';
|
||||
|
||||
import OlmDevice from '../../../../lib/crypto/OlmDevice';
|
||||
@@ -50,14 +48,14 @@ describe("OlmDecryption", function() {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
let aliceOlmDevice;
|
||||
let bobOlmDevice;
|
||||
|
||||
beforeEach(async function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
await global.Olm.init();
|
||||
|
||||
aliceOlmDevice = makeOlmDevice();
|
||||
bobOlmDevice = makeOlmDevice();
|
||||
await aliceOlmDevice.init();
|
||||
|
||||
+100
-23
@@ -16,7 +16,6 @@ limitations under the License.
|
||||
|
||||
import '../../olm-loader';
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
|
||||
import sdk from '../../..';
|
||||
@@ -29,6 +28,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,20 +83,30 @@ 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",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
const store = [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
|
||||
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
|
||||
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
return new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
@@ -108,6 +118,7 @@ function makeTestClient(sessionStore, cryptoStore) {
|
||||
deviceId: "device",
|
||||
sessionStore: sessionStore,
|
||||
cryptoStore: cryptoStore,
|
||||
cryptoCallbacks: { getCrossSigningKey, saveCrossSigningKeys },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -117,6 +128,10 @@ describe("MegolmBackup", function() {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
let olmDevice;
|
||||
let mockOlmLib;
|
||||
let mockCrypto;
|
||||
@@ -125,9 +140,6 @@ describe("MegolmBackup", function() {
|
||||
let cryptoStore;
|
||||
let megolmDecryption;
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||
mockCrypto.backupKey = new Olm.PkEncryption();
|
||||
mockCrypto.backupKey.set_recipient_key(
|
||||
@@ -143,9 +155,9 @@ describe("MegolmBackup", function() {
|
||||
|
||||
// we stub out the olm encryption bits
|
||||
mockOlmLib = {};
|
||||
mockOlmLib.ensureOlmSessionsForDevices = expect.createSpy();
|
||||
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
|
||||
mockOlmLib.encryptMessageForDevice =
|
||||
expect.createSpy().andReturn(Promise.resolve());
|
||||
jest.fn().mockReturnValue(Promise.resolve());
|
||||
});
|
||||
|
||||
describe("backup", function() {
|
||||
@@ -206,7 +218,7 @@ describe("MegolmBackup", function() {
|
||||
};
|
||||
mockCrypto.cancelRoomKeyRequest = function() {};
|
||||
|
||||
mockCrypto.backupGroupSession = expect.createSpy();
|
||||
mockCrypto.backupGroupSession = jest.fn();
|
||||
|
||||
return event.attemptDecryption(mockCrypto).then(() => {
|
||||
return megolmDecryption.onRoomKeyEvent(event);
|
||||
@@ -267,7 +279,7 @@ describe("MegolmBackup", function() {
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqualTo(1);
|
||||
expect(numCalls).toBeLessThanOrEqual(1);
|
||||
if (numCalls >= 2) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
@@ -276,8 +288,8 @@ describe("MegolmBackup", function() {
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe(1);
|
||||
expect(data.rooms[ROOM_ID].sessions).toExist();
|
||||
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(
|
||||
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
resolve();
|
||||
@@ -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).toBeLessThanOrEqual(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();
|
||||
@@ -305,16 +382,16 @@ describe("MegolmBackup", function() {
|
||||
const scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
const store = [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom",
|
||||
"storeUser", "getFilterIdByName", "setFilterIdByName", "getFilter",
|
||||
"storeFilter", "getSyncAccumulator", "startup", "deleteAllData",
|
||||
].reduce((r, k) => {r[k] = expect.createSpy(); return r;}, {});
|
||||
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
].reduce((r, k) => {r[k] = jest.fn(); return r;}, {});
|
||||
store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
const client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: "https://identity.server",
|
||||
@@ -372,7 +449,7 @@ describe("MegolmBackup", function() {
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqualTo(2);
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
if (numCalls >= 3) {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many timmes"));
|
||||
@@ -381,8 +458,8 @@ describe("MegolmBackup", function() {
|
||||
expect(method).toBe("PUT");
|
||||
expect(path).toBe("/room_keys/keys");
|
||||
expect(queryParams.version).toBe(1);
|
||||
expect(data.rooms[ROOM_ID].sessions).toExist();
|
||||
expect(data.rooms[ROOM_ID].sessions).toIncludeKey(
|
||||
expect(data.rooms[ROOM_ID].sessions).toBeDefined();
|
||||
expect(data.rooms[ROOM_ID].sessions).toHaveProperty(
|
||||
groupSession.session_id(),
|
||||
);
|
||||
if (numCalls > 1) {
|
||||
|
||||
@@ -0,0 +1,798 @@
|
||||
/*
|
||||
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 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;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return 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 = jest.fn(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 = jest.fn(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"]).toBeDefined();
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
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 { 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;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return 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 = jest.fn(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");
|
||||
});
|
||||
});
|
||||
@@ -21,7 +21,6 @@ try {
|
||||
logger.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
|
||||
|
||||
import {ShowQRCode, ScanQRCode} from '../../../../lib/crypto/verification/QRCode';
|
||||
@@ -34,8 +33,8 @@ describe("QR code verification", function() {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
describe("showing", function() {
|
||||
@@ -47,7 +46,7 @@ describe("QR code verification", function() {
|
||||
return "device+ed25519+key";
|
||||
},
|
||||
});
|
||||
const spy = expect.createSpy().andCall((e) => {
|
||||
const spy = jest.fn((e) => {
|
||||
qrCode.done();
|
||||
});
|
||||
qrCode.on("show_qr_code", spy);
|
||||
@@ -77,8 +76,8 @@ describe("QR code verification", function() {
|
||||
"ABCDEFG",
|
||||
);
|
||||
const client = {
|
||||
getStoredDevice: expect.createSpy().andReturn(device),
|
||||
setDeviceVerified: expect.createSpy(),
|
||||
getStoredDevice: jest.fn().mockReturnValue(device),
|
||||
setDeviceVerified: jest.fn(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(client);
|
||||
qrCode.on("confirm_user_id", ({userId, confirm}) => {
|
||||
@@ -100,18 +99,18 @@ describe("QR code verification", function() {
|
||||
|
||||
it("should error when the user ID doesn't match", async function() {
|
||||
const client = {
|
||||
getStoredDevice: expect.createSpy(),
|
||||
setDeviceVerified: expect.createSpy(),
|
||||
getStoredDevice: jest.fn(),
|
||||
setDeviceVerified: jest.fn(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(client, "@bob:example.com", "ABCDEFG");
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
const spy = expect.createSpy();
|
||||
const spy = jest.fn();
|
||||
await qrCode.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(client.getStoredDevice).toNotHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).toNotHaveBeenCalled();
|
||||
expect(client.getStoredDevice).not.toHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should error if the key doesn't match", async function() {
|
||||
@@ -129,18 +128,18 @@ describe("QR code verification", function() {
|
||||
"ABCDEFG",
|
||||
);
|
||||
const client = {
|
||||
getStoredDevice: expect.createSpy().andReturn(device),
|
||||
setDeviceVerified: expect.createSpy(),
|
||||
getStoredDevice: jest.fn().mockReturnValue(device),
|
||||
setDeviceVerified: jest.fn(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(client, "@alice:example.com", "ABCDEFG");
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
const spy = expect.createSpy();
|
||||
const spy = jest.fn();
|
||||
await qrCode.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(client.getStoredDevice).toHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).toNotHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,8 +21,6 @@ try {
|
||||
logger.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
import {verificationMethods} from '../../../../lib/crypto';
|
||||
|
||||
import SAS from '../../../../lib/crypto/verification/SAS';
|
||||
@@ -37,8 +35,8 @@ describe("verification request", function() {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
it("should request and accept a verification", async function() {
|
||||
@@ -51,7 +49,7 @@ describe("verification request", function() {
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
alice._crypto._deviceList.getRawStoredDevicesForUser = function() {
|
||||
alice.client._crypto._deviceList.getRawStoredDevicesForUser = function() {
|
||||
return {
|
||||
Dynabook: {
|
||||
keys: {
|
||||
@@ -60,21 +58,21 @@ 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");
|
||||
expect(aliceVerifier).toBeAn(SAS);
|
||||
const aliceVerifier = await alice.client.requestVerification("@bob:example.com");
|
||||
expect(aliceVerifier).toBeInstanceOf(SAS);
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
aliceVerifier._endTimer();
|
||||
|
||||
@@ -21,7 +21,7 @@ try {
|
||||
logger.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
import olmlib from '../../../../lib/crypto/olmlib';
|
||||
|
||||
import sdk from '../../../..';
|
||||
|
||||
@@ -36,14 +36,17 @@ 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');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(async function() {
|
||||
await Olm.init();
|
||||
beforeAll(function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
it("should error on an unexpected event", async function() {
|
||||
@@ -53,16 +56,15 @@ describe("SAS verification", function() {
|
||||
type: "es.inquisition",
|
||||
content: {},
|
||||
}));
|
||||
const spy = expect.createSpy();
|
||||
await sas.verify()
|
||||
.catch(spy);
|
||||
const spy = jest.fn();
|
||||
await sas.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
// Cancel the SAS for cleanup (we started a verification, so abort)
|
||||
sas.cancel();
|
||||
});
|
||||
|
||||
describe("verification", function() {
|
||||
describe("verification", () => {
|
||||
let alice;
|
||||
let bob;
|
||||
let aliceSasEvent;
|
||||
@@ -70,7 +72,7 @@ describe("SAS verification", function() {
|
||||
let aliceVerifier;
|
||||
let bobPromise;
|
||||
|
||||
beforeEach(async function() {
|
||||
beforeEach(async () => {
|
||||
[alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
@@ -81,39 +83,44 @@ 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",
|
||||
);
|
||||
},
|
||||
};
|
||||
alice.downloadKeys = () => {
|
||||
|
||||
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.client.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.downloadKeys = () => {
|
||||
bob.client._crypto._deviceList.storeDevicesForUser(
|
||||
"@alice:example.com", ALICE_DEVICES,
|
||||
);
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
@@ -121,7 +128,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();
|
||||
@@ -142,8 +149,318 @@ 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) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
afterEach(async () => {
|
||||
await Promise.all([
|
||||
alice.stop(),
|
||||
bob.stop(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should verify a key", async () => {
|
||||
let macMethod;
|
||||
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = function(type, map) {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
|
||||
.message_authentication_code;
|
||||
}
|
||||
return origSendToDevice(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
|
||||
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 () => {
|
||||
// pretend that Alice can only understand the old (incorrect) MAC,
|
||||
// and make sure that she can still verify with Bob
|
||||
let macMethod;
|
||||
const aliceOrigSendToDevice = alice.client.sendToDevice.bind(alice.client);
|
||||
alice.client.sendToDevice = (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.client.getUserId()][bob.client.deviceId]
|
||||
.message_authentication_codes = ['hmac-sha256'];
|
||||
}
|
||||
return aliceOrigSendToDevice(type, map);
|
||||
};
|
||||
const bobOrigSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = (type, map) => {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
|
||||
.message_authentication_code;
|
||||
}
|
||||
return bobOrigSendToDevice(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");
|
||||
|
||||
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 () => {
|
||||
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,
|
||||
},
|
||||
);
|
||||
|
||||
const verifyProm = Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => {
|
||||
bob.httpBackend.when(
|
||||
'POST', '/keys/signatures/upload',
|
||||
).respond(200, {});
|
||||
bob.httpBackend.flush(undefined, 1, 2000);
|
||||
return verifier.verify();
|
||||
}),
|
||||
]);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
it("should send a cancellation message on error", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
alice.client.setDeviceVerified = jest.fn();
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.client.setDeviceVerified = jest.fn();
|
||||
bob.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const bobPromise = new Promise((resolve, reject) => {
|
||||
bob.client.on("crypto.verification.start", (verifier) => {
|
||||
verifier.on("show_sas", (e) => {
|
||||
e.mismatch();
|
||||
});
|
||||
resolve(verifier);
|
||||
});
|
||||
});
|
||||
|
||||
const aliceVerifier = alice.client.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.client.getUserId(), bob.client.deviceId,
|
||||
);
|
||||
|
||||
const aliceSpy = jest.fn();
|
||||
const bobSpy = jest.fn();
|
||||
await Promise.all([
|
||||
aliceVerifier.verify().catch(aliceSpy),
|
||||
bobPromise.then((verifier) => verifier.verify()).catch(bobSpy),
|
||||
]);
|
||||
expect(aliceSpy).toHaveBeenCalled();
|
||||
expect(bobSpy).toHaveBeenCalled();
|
||||
expect(alice.client.setDeviceVerified)
|
||||
.not.toHaveBeenCalled();
|
||||
expect(bob.client.setDeviceVerified)
|
||||
.not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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 = jest.fn();
|
||||
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 = jest.fn();
|
||||
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).toContain(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) {
|
||||
@@ -164,113 +481,16 @@ describe("SAS verification", function() {
|
||||
});
|
||||
|
||||
it("should verify a key", async function() {
|
||||
let macMethod;
|
||||
const origSendToDevice = alice.sendToDevice;
|
||||
bob.sendToDevice = function(type, map) {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.getUserId()][alice.deviceId]
|
||||
.message_authentication_code;
|
||||
}
|
||||
return origSendToDevice.call(this, type, map);
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
bobPromise,
|
||||
]);
|
||||
|
||||
// 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);
|
||||
expect(alice.client.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(bob.client.getUserId(), bob.client.deviceId);
|
||||
expect(bob.client.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(alice.client.getUserId(), alice.client.deviceId);
|
||||
});
|
||||
|
||||
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) {
|
||||
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]
|
||||
.message_authentication_codes = ['hmac-sha256'];
|
||||
}
|
||||
return origSendToDevice.call(this, type, map);
|
||||
};
|
||||
bob.sendToDevice = function(type, map) {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.getUserId()][alice.deviceId]
|
||||
.message_authentication_code;
|
||||
}
|
||||
return origSendToDevice.call(this, type, map);
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
aliceVerifier.verify(),
|
||||
bobPromise.then((verifier) => verifier.verify()),
|
||||
]);
|
||||
|
||||
expect(macMethod).toBe("hmac-sha256");
|
||||
|
||||
expect(alice.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(bob.getUserId(), bob.deviceId);
|
||||
expect(bob.setDeviceVerified)
|
||||
.toHaveBeenCalledWith(alice.getUserId(), alice.deviceId);
|
||||
});
|
||||
});
|
||||
|
||||
it("should send a cancellation message on error", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
alice.setDeviceVerified = expect.createSpy();
|
||||
alice.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
bob.setDeviceVerified = expect.createSpy();
|
||||
bob.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const bobPromise = new Promise((resolve, reject) => {
|
||||
bob.on("crypto.verification.start", (verifier) => {
|
||||
verifier.on("show_sas", (e) => {
|
||||
e.mismatch();
|
||||
});
|
||||
resolve(verifier);
|
||||
});
|
||||
});
|
||||
|
||||
const aliceVerifier = alice.beginKeyVerification(
|
||||
verificationMethods.SAS, bob.getUserId(), bob.deviceId,
|
||||
);
|
||||
|
||||
const aliceSpy = expect.createSpy();
|
||||
const bobSpy = expect.createSpy();
|
||||
await Promise.all([
|
||||
aliceVerifier.verify().catch(aliceSpy),
|
||||
bobPromise.then((verifier) => verifier.verify()).catch(bobSpy),
|
||||
]);
|
||||
expect(aliceSpy).toHaveBeenCalled();
|
||||
expect(bobSpy).toHaveBeenCalled();
|
||||
expect(alice.setDeviceVerified)
|
||||
.toNotHaveBeenCalled();
|
||||
expect(bob.setDeviceVerified)
|
||||
.toNotHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -9,8 +9,6 @@ function mockRoomStates(timeline) {
|
||||
timeline._endState = utils.mock(sdk.RoomState, "endState");
|
||||
}
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("EventTimeline", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
@@ -18,8 +16,6 @@ describe("EventTimeline", function() {
|
||||
let timeline;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
|
||||
// XXX: this is a horrid hack; should use sinon or something instead to mock
|
||||
const timelineSet = { room: { roomId: roomId }};
|
||||
timelineSet.room.getUnfilteredTimelineSet = function() {
|
||||
@@ -78,7 +74,7 @@ describe("EventTimeline", function() {
|
||||
|
||||
expect(function() {
|
||||
timeline.initialiseState(state);
|
||||
}).toNotThrow();
|
||||
}).not.toThrow();
|
||||
timeline.addEvent(event, false);
|
||||
expect(function() {
|
||||
timeline.initialiseState(state);
|
||||
@@ -121,7 +117,7 @@ describe("EventTimeline", function() {
|
||||
const next = {b: "b"};
|
||||
expect(function() {
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
}).toNotThrow();
|
||||
}).not.toThrow();
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS))
|
||||
.toBe(prev);
|
||||
expect(function() {
|
||||
@@ -130,7 +126,7 @@ describe("EventTimeline", function() {
|
||||
|
||||
expect(function() {
|
||||
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
|
||||
}).toNotThrow();
|
||||
}).not.toThrow();
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.FORWARDS))
|
||||
.toBe(next);
|
||||
expect(function() {
|
||||
@@ -187,14 +183,14 @@ describe("EventTimeline", function() {
|
||||
name: "Old Alice",
|
||||
};
|
||||
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
|
||||
.andCall(function(uid) {
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
|
||||
.andCall(function(uid) {
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
@@ -229,14 +225,14 @@ describe("EventTimeline", function() {
|
||||
name: "Old Alice",
|
||||
};
|
||||
timeline.getState(EventTimeline.FORWARDS).getSentinelMember
|
||||
.andCall(function(uid) {
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
timeline.getState(EventTimeline.BACKWARDS).getSentinelMember
|
||||
.andCall(function(uid) {
|
||||
.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
@@ -281,7 +277,7 @@ describe("EventTimeline", function() {
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
|
||||
expect(timeline.getState(EventTimeline.BACKWARDS).setStateEvents).
|
||||
toNotHaveBeenCalled();
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -311,7 +307,7 @@ describe("EventTimeline", function() {
|
||||
expect(events[1].forwardLooking).toBe(false);
|
||||
|
||||
expect(timeline.getState(EventTimeline.FORWARDS).setStateEvents).
|
||||
toNotHaveBeenCalled();
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -17,17 +17,10 @@ limitations under the License.
|
||||
import sdk from '../..';
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
import testUtils from '../test-utils';
|
||||
|
||||
import expect from 'expect';
|
||||
import Promise from 'bluebird';
|
||||
import logger from '../../src/logger';
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe(".attemptDecryption", () => {
|
||||
let encryptedEvent;
|
||||
|
||||
@@ -45,6 +38,7 @@ describe("MatrixEvent", () => {
|
||||
let callCount = 0;
|
||||
|
||||
let prom2;
|
||||
let prom2Fulfilled = false;
|
||||
|
||||
const crypto = {
|
||||
decryptEvent: function() {
|
||||
@@ -54,12 +48,13 @@ describe("MatrixEvent", () => {
|
||||
// schedule a second decryption attempt while
|
||||
// the first one is still running.
|
||||
prom2 = encryptedEvent.attemptDecryption(crypto);
|
||||
prom2.then(() => prom2Fulfilled = true);
|
||||
|
||||
const error = new Error("nope");
|
||||
error.name = 'DecryptionError';
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
expect(prom2.isFulfilled()).toBe(
|
||||
expect(prom2Fulfilled).toBe(
|
||||
false, 'second attemptDecryption resolved too soon');
|
||||
|
||||
return Promise.resolve({
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const Filter = sdk.Filter;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("Filter", function() {
|
||||
const filterId = "f1lt3ring15g00d4ursoul";
|
||||
@@ -12,7 +9,6 @@ describe("Filter", function() {
|
||||
let filter;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
filter = new Filter(userId);
|
||||
});
|
||||
|
||||
|
||||
@@ -18,12 +18,10 @@ limitations under the License.
|
||||
import 'source-map-support/register';
|
||||
import Promise from 'bluebird';
|
||||
const sdk = require("../..");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
const InteractiveAuth = sdk.InteractiveAuth;
|
||||
const MatrixError = sdk.MatrixError;
|
||||
|
||||
import expect from 'expect';
|
||||
import logger from '../../src/logger';
|
||||
|
||||
// Trivial client object to test interactive auth
|
||||
@@ -35,13 +33,9 @@ class FakeClient {
|
||||
}
|
||||
|
||||
describe("InteractiveAuth", function() {
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
it("should start an auth stage and complete it", function(done) {
|
||||
const doRequest = expect.createSpy();
|
||||
const stateUpdated = expect.createSpy();
|
||||
it("should start an auth stage and complete it", function() {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: new FakeClient(),
|
||||
@@ -64,7 +58,7 @@ describe("InteractiveAuth", function() {
|
||||
});
|
||||
|
||||
// first we expect a call here
|
||||
stateUpdated.andCall(function(stage) {
|
||||
stateUpdated.mockImplementation(function(stage) {
|
||||
logger.log('aaaa');
|
||||
expect(stage).toEqual("logintype");
|
||||
ia.submitAuthDict({
|
||||
@@ -75,7 +69,7 @@ describe("InteractiveAuth", function() {
|
||||
|
||||
// .. which should trigger a call here
|
||||
const requestRes = {"a": "b"};
|
||||
doRequest.andCall(function(authData) {
|
||||
doRequest.mockImplementation(function(authData) {
|
||||
logger.log('cccc');
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
@@ -85,16 +79,16 @@ describe("InteractiveAuth", function() {
|
||||
return Promise.resolve(requestRes);
|
||||
});
|
||||
|
||||
ia.attemptAuth().then(function(res) {
|
||||
return ia.attemptAuth().then(function(res) {
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest.calls.length).toEqual(1);
|
||||
expect(stateUpdated.calls.length).toEqual(1);
|
||||
}).nodeify(done);
|
||||
expect(doRequest).toBeCalledTimes(1);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should make a request if no authdata is provided", function(done) {
|
||||
const doRequest = expect.createSpy();
|
||||
const stateUpdated = expect.createSpy();
|
||||
it("should make a request if no authdata is provided", function() {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: new FakeClient(),
|
||||
@@ -106,7 +100,7 @@ describe("InteractiveAuth", function() {
|
||||
expect(ia.getStageParams("logintype")).toBe(undefined);
|
||||
|
||||
// first we expect a call to doRequest
|
||||
doRequest.andCall(function(authData) {
|
||||
doRequest.mockImplementation(function(authData) {
|
||||
logger.log("request1", authData);
|
||||
expect(authData).toEqual({});
|
||||
const err = new MatrixError({
|
||||
@@ -124,7 +118,7 @@ describe("InteractiveAuth", function() {
|
||||
|
||||
// .. which should be followed by a call to stateUpdated
|
||||
const requestRes = {"a": "b"};
|
||||
stateUpdated.andCall(function(stage) {
|
||||
stateUpdated.mockImplementation(function(stage) {
|
||||
expect(stage).toEqual("logintype");
|
||||
expect(ia.getSessionId()).toEqual("sessionId");
|
||||
expect(ia.getStageParams("logintype")).toEqual({
|
||||
@@ -132,7 +126,7 @@ describe("InteractiveAuth", function() {
|
||||
});
|
||||
|
||||
// submitAuthDict should trigger another call to doRequest
|
||||
doRequest.andCall(function(authData) {
|
||||
doRequest.mockImplementation(function(authData) {
|
||||
logger.log("request2", authData);
|
||||
expect(authData).toEqual({
|
||||
session: "sessionId",
|
||||
@@ -148,10 +142,10 @@ describe("InteractiveAuth", function() {
|
||||
});
|
||||
});
|
||||
|
||||
ia.attemptAuth().then(function(res) {
|
||||
return ia.attemptAuth().then(function(res) {
|
||||
expect(res).toBe(requestRes);
|
||||
expect(doRequest.calls.length).toEqual(2);
|
||||
expect(stateUpdated.calls.length).toEqual(1);
|
||||
}).nodeify(done);
|
||||
expect(doRequest).toBeCalledTimes(2);
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import expect from 'expect';
|
||||
import TestClient from '../TestClient';
|
||||
|
||||
describe('Login request', function() {
|
||||
|
||||
@@ -3,12 +3,11 @@ import 'source-map-support/register';
|
||||
import Promise from 'bluebird';
|
||||
const sdk = require("../..");
|
||||
const MatrixClient = sdk.MatrixClient;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
import lolex from 'lolex';
|
||||
import logger from '../../src/logger';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
const userId = "@alice:bar";
|
||||
const identityServerUrl = "https://identity.server";
|
||||
@@ -16,7 +15,6 @@ describe("MatrixClient", function() {
|
||||
let client;
|
||||
let store;
|
||||
let scheduler;
|
||||
let clock;
|
||||
|
||||
const KEEP_ALIVE_PATH = "/_matrix/client/versions";
|
||||
|
||||
@@ -125,24 +123,22 @@ describe("MatrixClient", function() {
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
clock = lolex.install();
|
||||
scheduler = [
|
||||
"getQueueForEvent", "queueEvent", "removeEventFromQueue",
|
||||
"setProcessFunction",
|
||||
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
|
||||
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
|
||||
store = [
|
||||
"getRoom", "getRooms", "getUser", "getSyncToken", "scrollback",
|
||||
"save", "wantsSave", "setSyncToken", "storeEvents", "storeRoom", "storeUser",
|
||||
"getFilterIdByName", "setFilterIdByName", "getFilter", "storeFilter",
|
||||
"getSyncAccumulator", "startup", "deleteAllData",
|
||||
].reduce((r, k) => { r[k] = expect.createSpy(); return r; }, {});
|
||||
store.getSavedSync = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getSavedSyncToken = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.setSyncData = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.getClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.storeClientOptions = expect.createSpy().andReturn(Promise.resolve(null));
|
||||
store.isNewlyCreated = expect.createSpy().andReturn(Promise.resolve(true));
|
||||
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
|
||||
store.getSavedSync = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.getSavedSyncToken = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.setSyncData = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.getClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.storeClientOptions = jest.fn().mockReturnValue(Promise.resolve(null));
|
||||
store.isNewlyCreated = jest.fn().mockReturnValue(Promise.resolve(true));
|
||||
client = new MatrixClient({
|
||||
baseUrl: "https://my.home.server",
|
||||
idBaseUrl: identityServerUrl,
|
||||
@@ -154,13 +150,10 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
// FIXME: We shouldn't be yanking _http like this.
|
||||
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);
|
||||
"authedRequest", "getContentUri", "request", "uploadContent",
|
||||
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
|
||||
client._http.authedRequest.mockImplementation(httpReq);
|
||||
client._http.request.mockImplementation(httpReq);
|
||||
|
||||
// set reasonable working defaults
|
||||
acceptKeepalives = true;
|
||||
@@ -172,16 +165,12 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
clock.uninstall();
|
||||
// need to re-stub the requests with NOPs because there are no guarantees
|
||||
// clients from previous tests will be GC'd before the next test. This
|
||||
// means they may call /events and then fail an expect() which will fail
|
||||
// a DIFFERENT test (pollution between tests!) - we return unresolved
|
||||
// promises to stop the client from continuing to run.
|
||||
client._http.authedRequest.andCall(function() {
|
||||
return Promise.defer().promise;
|
||||
});
|
||||
client._http.authedRequestWithPrefix.andCall(function() {
|
||||
client._http.authedRequest.mockImplementation(function() {
|
||||
return Promise.defer().promise;
|
||||
});
|
||||
});
|
||||
@@ -191,10 +180,10 @@ describe("MatrixClient", function() {
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
const filterId = "ehfewf";
|
||||
store.getFilterIdByName.andReturn(filterId);
|
||||
store.getFilterIdByName.mockReturnValue(filterId);
|
||||
const filter = new sdk.Filter(0, filterId);
|
||||
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
|
||||
store.getFilter.andReturn(filter);
|
||||
store.getFilter.mockReturnValue(filter);
|
||||
const syncPromise = new Promise((resolve, reject) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
if (state === "SYNCING") {
|
||||
@@ -255,7 +244,7 @@ describe("MatrixClient", function() {
|
||||
},
|
||||
});
|
||||
httpLookups.push(FILTER_RESPONSE);
|
||||
store.getFilterIdByName.andReturn(invalidFilterId);
|
||||
store.getFilterIdByName.mockReturnValue(invalidFilterId);
|
||||
|
||||
const filterName = getFilterName(client.credentials.userId);
|
||||
client.store.setFilterIdByName(filterName, invalidFilterId);
|
||||
@@ -287,7 +276,7 @@ describe("MatrixClient", function() {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(2);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
clock.tick(1);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
@@ -313,9 +302,9 @@ describe("MatrixClient", function() {
|
||||
expect(client.retryImmediately()).toBe(
|
||||
true, "retryImmediately returned false",
|
||||
);
|
||||
clock.tick(1);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "RECONNECTING" && httpLookups.length > 0) {
|
||||
clock.tick(10000);
|
||||
jest.advanceTimersByTime(10000);
|
||||
} else if (state === "SYNCING" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
@@ -337,7 +326,7 @@ describe("MatrixClient", function() {
|
||||
if (state === "ERROR" && httpLookups.length > 0) {
|
||||
expect(httpLookups.length).toEqual(3);
|
||||
expect(client.retryImmediately()).toBe(true);
|
||||
clock.tick(1);
|
||||
jest.advanceTimersByTime(1);
|
||||
} else if (state === "PREPARED" && httpLookups.length === 0) {
|
||||
client.removeListener("sync", syncListener);
|
||||
done();
|
||||
@@ -368,7 +357,7 @@ describe("MatrixClient", function() {
|
||||
done();
|
||||
}
|
||||
// standard retry time is 5 to 10 seconds
|
||||
clock.tick(10000);
|
||||
jest.advanceTimersByTime(10000);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ import 'source-map-support/register';
|
||||
const PushProcessor = require("../../lib/pushprocessor");
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe('NotificationService', function() {
|
||||
const testUserId = "@ali:matrix.org";
|
||||
const testDisplayName = "Alice M";
|
||||
|
||||
@@ -1,53 +1,46 @@
|
||||
"use strict";
|
||||
|
||||
import 'source-map-support/register';
|
||||
const callbacks = require("../../lib/realtime-callbacks");
|
||||
const testUtils = require("../test-utils.js");
|
||||
const callbacks = require("../../src/realtime-callbacks");
|
||||
|
||||
import expect from 'expect';
|
||||
import lolex from 'lolex';
|
||||
let wallTime = 1234567890;
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("realtime-callbacks", function() {
|
||||
let clock;
|
||||
|
||||
function tick(millis) {
|
||||
clock.tick(millis);
|
||||
wallTime += millis;
|
||||
jest.advanceTimersByTime(millis);
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
clock = lolex.install();
|
||||
const fakeDate = clock.Date;
|
||||
callbacks.setNow(fakeDate.now.bind(fakeDate));
|
||||
callbacks.setNow(() => wallTime);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
callbacks.setNow();
|
||||
clock.uninstall();
|
||||
});
|
||||
|
||||
describe("setTimeout", function() {
|
||||
it("should call the callback after the timeout", function() {
|
||||
const callback = expect.createSpy();
|
||||
const callback = jest.fn();
|
||||
callbacks.setTimeout(callback, 100);
|
||||
|
||||
expect(callback).toNotHaveBeenCalled();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it("should default to a zero timeout", function() {
|
||||
const callback = expect.createSpy();
|
||||
const callback = jest.fn();
|
||||
callbacks.setTimeout(callback);
|
||||
|
||||
expect(callback).toNotHaveBeenCalled();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
tick(0);
|
||||
expect(callback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should pass any parameters to the callback", function() {
|
||||
const callback = expect.createSpy();
|
||||
const callback = jest.fn();
|
||||
callbacks.setTimeout(callback, 0, "a", "b", "c");
|
||||
tick(0);
|
||||
expect(callback).toHaveBeenCalledWith("a", "b", "c");
|
||||
@@ -66,10 +59,10 @@ describe("realtime-callbacks", function() {
|
||||
});
|
||||
|
||||
it("should handle timeouts of several seconds", function() {
|
||||
const callback = expect.createSpy();
|
||||
const callback = jest.fn();
|
||||
callbacks.setTimeout(callback, 2000);
|
||||
|
||||
expect(callback).toNotHaveBeenCalled();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
for (let i = 0; i < 4; i++) {
|
||||
tick(500);
|
||||
}
|
||||
@@ -77,24 +70,24 @@ describe("realtime-callbacks", function() {
|
||||
});
|
||||
|
||||
it("should call multiple callbacks in the right order", function() {
|
||||
const callback1 = expect.createSpy();
|
||||
const callback2 = expect.createSpy();
|
||||
const callback3 = expect.createSpy();
|
||||
const callback1 = jest.fn();
|
||||
const callback2 = jest.fn();
|
||||
const callback3 = jest.fn();
|
||||
callbacks.setTimeout(callback2, 200);
|
||||
callbacks.setTimeout(callback1, 100);
|
||||
callbacks.setTimeout(callback3, 300);
|
||||
|
||||
expect(callback1).toNotHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback3).toNotHaveBeenCalled();
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback3).toNotHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
expect(callback3).toNotHaveBeenCalled();
|
||||
expect(callback3).not.toHaveBeenCalled();
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
@@ -102,35 +95,34 @@ describe("realtime-callbacks", function() {
|
||||
});
|
||||
|
||||
it("should treat -ve timeouts the same as a zero timeout", function() {
|
||||
const callback1 = expect.createSpy();
|
||||
const callback2 = expect.createSpy();
|
||||
const callback1 = jest.fn();
|
||||
const callback2 = jest.fn();
|
||||
|
||||
// check that cb1 is called before cb2
|
||||
callback1.andCall(function() {
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
callback1.mockImplementation(function() {
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
callbacks.setTimeout(callback1);
|
||||
callbacks.setTimeout(callback2, -100);
|
||||
|
||||
expect(callback1).toNotHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
tick(0);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not get confused by chained calls", function() {
|
||||
const callback2 = expect.createSpy();
|
||||
const callback1 = expect.createSpy();
|
||||
callback1.andCall(function() {
|
||||
const callback2 = jest.fn();
|
||||
const callback1 = jest.fn(function() {
|
||||
callbacks.setTimeout(callback2, 0);
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
callbacks.setTimeout(callback1);
|
||||
expect(callback1).toNotHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
tick(0);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
// the fake timer won't actually run callbacks registered during
|
||||
@@ -140,16 +132,15 @@ describe("realtime-callbacks", function() {
|
||||
});
|
||||
|
||||
it("should be immune to exceptions", function() {
|
||||
const callback1 = expect.createSpy();
|
||||
callback1.andCall(function() {
|
||||
const callback1 = jest.fn(function() {
|
||||
throw new Error("prepare to die");
|
||||
});
|
||||
const callback2 = expect.createSpy();
|
||||
const callback2 = jest.fn();
|
||||
callbacks.setTimeout(callback1, 0);
|
||||
callbacks.setTimeout(callback2, 0);
|
||||
|
||||
expect(callback1).toNotHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback1).not.toHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
tick(0);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toHaveBeenCalled();
|
||||
@@ -158,16 +149,16 @@ describe("realtime-callbacks", function() {
|
||||
|
||||
describe("cancelTimeout", function() {
|
||||
it("should cancel a pending timeout", function() {
|
||||
const callback = expect.createSpy();
|
||||
const callback = jest.fn();
|
||||
const k = callbacks.setTimeout(callback);
|
||||
callbacks.clearTimeout(k);
|
||||
tick(0);
|
||||
expect(callback).toNotHaveBeenCalled();
|
||||
expect(callback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not affect sooner timeouts", function() {
|
||||
const callback1 = expect.createSpy();
|
||||
const callback2 = expect.createSpy();
|
||||
const callback1 = jest.fn();
|
||||
const callback2 = jest.fn();
|
||||
|
||||
callbacks.setTimeout(callback1, 100);
|
||||
const k = callbacks.setTimeout(callback2, 200);
|
||||
@@ -175,10 +166,10 @@ describe("realtime-callbacks", function() {
|
||||
|
||||
tick(100);
|
||||
expect(callback1).toHaveBeenCalled();
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
|
||||
tick(150);
|
||||
expect(callback2).toNotHaveBeenCalled();
|
||||
expect(callback2).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,8 +4,6 @@ const sdk = require("../..");
|
||||
const RoomMember = sdk.RoomMember;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("RoomMember", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
@@ -14,7 +12,6 @@ describe("RoomMember", function() {
|
||||
let member;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
member = new RoomMember(roomId, userA);
|
||||
});
|
||||
|
||||
@@ -36,7 +33,7 @@ describe("RoomMember", function() {
|
||||
const url = member.getAvatarUrl(hsUrl);
|
||||
// we don't care about how the mxc->http conversion is done, other
|
||||
// than it contains the mxc body.
|
||||
expect(url.indexOf("flibble/wibble")).toNotEqual(-1);
|
||||
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return an identicon HTTP URL if allowDefault was set and there " +
|
||||
@@ -255,9 +252,9 @@ describe("RoomMember", function() {
|
||||
member.setMembershipEvent(joinEvent);
|
||||
expect(member.name).toEqual("Alice"); // prefer displayname
|
||||
member.setMembershipEvent(joinEvent, roomState);
|
||||
expect(member.name).toNotEqual("Alice"); // it should disambig.
|
||||
expect(member.name).not.toEqual("Alice"); // it should disambig.
|
||||
// user_id should be there somewhere
|
||||
expect(member.name.indexOf(userA)).toNotEqual(-1);
|
||||
expect(member.name.indexOf(userA)).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should emit 'RoomMember.membership' if the membership changes", function() {
|
||||
@@ -328,9 +325,9 @@ describe("RoomMember", function() {
|
||||
};
|
||||
expect(member.name).toEqual(userA); // default = user_id
|
||||
member.setMembershipEvent(joinEvent, roomState);
|
||||
expect(member.name).toNotEqual("Alíce"); // it should disambig.
|
||||
expect(member.name).not.toEqual("Alíce"); // it should disambig.
|
||||
// user_id should be there somewhere
|
||||
expect(member.name.indexOf(userA)).toNotEqual(-1);
|
||||
expect(member.name.indexOf(userA)).not.toEqual(-1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,8 +5,6 @@ const RoomState = sdk.RoomState;
|
||||
const RoomMember = sdk.RoomMember;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("RoomState", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
@@ -17,7 +15,6 @@ describe("RoomState", function() {
|
||||
let state;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
state = new RoomState(roomId);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({ // userA joined
|
||||
@@ -49,8 +46,8 @@ describe("RoomState", function() {
|
||||
const members = state.getMembers();
|
||||
expect(members.length).toEqual(2);
|
||||
// ordering unimportant
|
||||
expect([userA, userB].indexOf(members[0].userId)).toNotEqual(-1);
|
||||
expect([userA, userB].indexOf(members[1].userId)).toNotEqual(-1);
|
||||
expect([userA, userB].indexOf(members[0].userId)).not.toEqual(-1);
|
||||
expect([userA, userB].indexOf(members[1].userId)).not.toEqual(-1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,8 +117,8 @@ describe("RoomState", function() {
|
||||
const events = state.getStateEvents("m.room.member");
|
||||
expect(events.length).toEqual(2);
|
||||
// ordering unimportant
|
||||
expect([userA, userB].indexOf(events[0].getStateKey())).toNotEqual(-1);
|
||||
expect([userA, userB].indexOf(events[1].getStateKey())).toNotEqual(-1);
|
||||
expect([userA, userB].indexOf(events[0].getStateKey())).not.toEqual(-1);
|
||||
expect([userA, userB].indexOf(events[1].getStateKey())).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return a single MatrixEvent if a state_key was specified",
|
||||
@@ -258,7 +255,7 @@ describe("RoomState", function() {
|
||||
});
|
||||
state.setStateEvents([memberEvent]);
|
||||
|
||||
expect(state.members[userA].setMembershipEvent).toNotHaveBeenCalled();
|
||||
expect(state.members[userA].setMembershipEvent).not.toHaveBeenCalled();
|
||||
expect(state.members[userB].setMembershipEvent).toHaveBeenCalledWith(
|
||||
memberEvent, state,
|
||||
);
|
||||
@@ -306,7 +303,7 @@ describe("RoomState", function() {
|
||||
state.markOutOfBandMembersStarted();
|
||||
state.setOutOfBandMembers([oobMemberEvent]);
|
||||
const memberA = state.getMember(userA);
|
||||
expect(memberA.events.member.getId()).toNotEqual(oobMemberEvent.getId());
|
||||
expect(memberA.events.member.getId()).not.toEqual(oobMemberEvent.getId());
|
||||
expect(memberA.isOutOfBand()).toEqual(false);
|
||||
});
|
||||
|
||||
|
||||
+32
-33
@@ -8,8 +8,6 @@ const EventStatus = sdk.EventStatus;
|
||||
const EventTimeline = sdk.EventTimeline;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("Room", function() {
|
||||
const roomId = "!foo:bar";
|
||||
const userA = "@alice:bar";
|
||||
@@ -19,7 +17,6 @@ describe("Room", function() {
|
||||
let room;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
room = new Room(roomId);
|
||||
// mock RoomStates
|
||||
room.oldState = room.getLiveTimeline()._startState =
|
||||
@@ -32,7 +29,7 @@ describe("Room", function() {
|
||||
const hsUrl = "https://my.home.server";
|
||||
|
||||
it("should return the URL from m.room.avatar preferentially", function() {
|
||||
room.currentState.getStateEvents.andCall(function(type, key) {
|
||||
room.currentState.getStateEvents.mockImplementation(function(type, key) {
|
||||
if (type === "m.room.avatar" && key === "") {
|
||||
return utils.mkEvent({
|
||||
event: true,
|
||||
@@ -49,7 +46,7 @@ describe("Room", function() {
|
||||
const url = room.getAvatarUrl(hsUrl);
|
||||
// we don't care about how the mxc->http conversion is done, other
|
||||
// than it contains the mxc body.
|
||||
expect(url.indexOf("flibble/wibble")).toNotEqual(-1);
|
||||
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return an identicon HTTP URL if allowDefault was set and there " +
|
||||
@@ -67,13 +64,13 @@ describe("Room", function() {
|
||||
|
||||
describe("getMember", function() {
|
||||
beforeEach(function() {
|
||||
room.currentState.getMember.andCall(function(userId) {
|
||||
room.currentState.getMember.mockImplementation(function(userId) {
|
||||
return {
|
||||
"@alice:bar": {
|
||||
userId: userA,
|
||||
roomId: roomId,
|
||||
},
|
||||
}[userId];
|
||||
}[userId] || null;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,7 +79,7 @@ describe("Room", function() {
|
||||
});
|
||||
|
||||
it("should return the member from current state", function() {
|
||||
expect(room.getMember(userA)).toNotEqual(null);
|
||||
expect(room.getMember(userA)).not.toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -174,7 +171,7 @@ describe("Room", function() {
|
||||
);
|
||||
expect(events[0].forwardLooking).toBe(true);
|
||||
expect(events[1].forwardLooking).toBe(true);
|
||||
expect(room.oldState.setStateEvents).toNotHaveBeenCalled();
|
||||
expect(room.oldState.setStateEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should synthesize read receipts for the senders of events", function() {
|
||||
@@ -183,7 +180,7 @@ describe("Room", function() {
|
||||
membership: "join",
|
||||
name: "Alice",
|
||||
};
|
||||
room.currentState.getSentinelMember.andCall(function(uid) {
|
||||
room.currentState.getSentinelMember.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
@@ -292,13 +289,13 @@ describe("Room", function() {
|
||||
membership: "join",
|
||||
name: "Old Alice",
|
||||
};
|
||||
room.currentState.getSentinelMember.andCall(function(uid) {
|
||||
room.currentState.getSentinelMember.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
room.oldState.getSentinelMember.andCall(function(uid) {
|
||||
room.oldState.getSentinelMember.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
@@ -331,13 +328,13 @@ describe("Room", function() {
|
||||
membership: "join",
|
||||
name: "Old Alice",
|
||||
};
|
||||
room.currentState.getSentinelMember.andCall(function(uid) {
|
||||
room.currentState.getSentinelMember.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return sentinel;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
room.oldState.getSentinelMember.andCall(function(uid) {
|
||||
room.oldState.getSentinelMember.mockImplementation(function(uid) {
|
||||
if (uid === userA) {
|
||||
return oldSentinel;
|
||||
}
|
||||
@@ -379,7 +376,7 @@ describe("Room", function() {
|
||||
);
|
||||
expect(events[0].forwardLooking).toBe(false);
|
||||
expect(events[1].forwardLooking).toBe(false);
|
||||
expect(room.currentState.setStateEvents).toNotHaveBeenCalled();
|
||||
expect(room.currentState.setStateEvents).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -545,7 +542,7 @@ describe("Room", function() {
|
||||
|
||||
describe("getJoinedMembers", function() {
|
||||
it("should return members whose membership is 'join'", function() {
|
||||
room.currentState.getMembers.andCall(function() {
|
||||
room.currentState.getMembers.mockImplementation(function() {
|
||||
return [
|
||||
{ userId: "@alice:bar", membership: "join" },
|
||||
{ userId: "@bob:bar", membership: "invite" },
|
||||
@@ -558,7 +555,7 @@ describe("Room", function() {
|
||||
});
|
||||
|
||||
it("should return an empty list if no membership is 'join'", function() {
|
||||
room.currentState.getMembers.andCall(function() {
|
||||
room.currentState.getMembers.mockImplementation(function() {
|
||||
return [
|
||||
{ userId: "@bob:bar", membership: "invite" },
|
||||
];
|
||||
@@ -571,7 +568,7 @@ describe("Room", function() {
|
||||
describe("hasMembershipState", function() {
|
||||
it("should return true for a matching userId and membership",
|
||||
function() {
|
||||
room.currentState.getMember.andCall(function(userId) {
|
||||
room.currentState.getMember.mockImplementation(function(userId) {
|
||||
return {
|
||||
"@alice:bar": { userId: "@alice:bar", membership: "join" },
|
||||
"@bob:bar": { userId: "@bob:bar", membership: "invite" },
|
||||
@@ -582,7 +579,7 @@ describe("Room", function() {
|
||||
|
||||
it("should return false if match membership but no match userId",
|
||||
function() {
|
||||
room.currentState.getMember.andCall(function(userId) {
|
||||
room.currentState.getMember.mockImplementation(function(userId) {
|
||||
return {
|
||||
"@alice:bar": { userId: "@alice:bar", membership: "join" },
|
||||
}[userId];
|
||||
@@ -592,7 +589,7 @@ describe("Room", function() {
|
||||
|
||||
it("should return false if match userId but no match membership",
|
||||
function() {
|
||||
room.currentState.getMember.andCall(function(userId) {
|
||||
room.currentState.getMember.mockImplementation(function(userId) {
|
||||
return {
|
||||
"@alice:bar": { userId: "@alice:bar", membership: "join" },
|
||||
}[userId];
|
||||
@@ -602,7 +599,7 @@ describe("Room", function() {
|
||||
|
||||
it("should return false if no match membership or userId",
|
||||
function() {
|
||||
room.currentState.getMember.andCall(function(userId) {
|
||||
room.currentState.getMember.mockImplementation(function(userId) {
|
||||
return {
|
||||
"@alice:bar": { userId: "@alice:bar", membership: "join" },
|
||||
}[userId];
|
||||
@@ -626,7 +623,9 @@ describe("Room", function() {
|
||||
};
|
||||
const setAliases = function(aliases, stateKey) {
|
||||
if (!stateKey) {
|
||||
stateKey = "flibble";
|
||||
stateKey = aliases.length
|
||||
? aliases[0].split(':').splice(1).join(':') // domain+port
|
||||
: 'fibble';
|
||||
}
|
||||
room.addLiveEvents([utils.mkEvent({
|
||||
type: "m.room.aliases", room: roomId, skey: stateKey, content: {
|
||||
@@ -814,8 +813,8 @@ describe("Room", function() {
|
||||
addMember(userC);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name.indexOf(userB)).toNotEqual(-1, name);
|
||||
expect(name.indexOf(userC)).toNotEqual(-1, name);
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
expect(name.indexOf(userC)).not.toEqual(-1, name);
|
||||
});
|
||||
|
||||
it("should return the names of members in a public (public join_rules)" +
|
||||
@@ -827,8 +826,8 @@ describe("Room", function() {
|
||||
addMember(userC);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name.indexOf(userB)).toNotEqual(-1, name);
|
||||
expect(name.indexOf(userC)).toNotEqual(-1, name);
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
expect(name.indexOf(userC)).not.toEqual(-1, name);
|
||||
});
|
||||
|
||||
it("should show the other user's name for public (public join_rules)" +
|
||||
@@ -839,7 +838,7 @@ describe("Room", function() {
|
||||
addMember(userB);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name.indexOf(userB)).toNotEqual(-1, name);
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
});
|
||||
|
||||
it("should show the other user's name for private " +
|
||||
@@ -850,7 +849,7 @@ describe("Room", function() {
|
||||
addMember(userB);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name.indexOf(userB)).toNotEqual(-1, name);
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
});
|
||||
|
||||
it("should show the other user's name for private" +
|
||||
@@ -860,14 +859,14 @@ describe("Room", function() {
|
||||
addMember(userB);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name.indexOf(userB)).toNotEqual(-1, name);
|
||||
expect(name.indexOf(userB)).not.toEqual(-1, name);
|
||||
});
|
||||
|
||||
it("should show the room alias if one exists for private " +
|
||||
"(invite join_rules) rooms if a room name doesn't exist.", function() {
|
||||
const alias = "#room_alias:here";
|
||||
setJoinRule("invite");
|
||||
setAliases([alias, "#another:one"]);
|
||||
setAliases([alias, "#another:here"]);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name).toEqual(alias);
|
||||
@@ -877,7 +876,7 @@ describe("Room", function() {
|
||||
"(public join_rules) rooms if a room name doesn't exist.", function() {
|
||||
const alias = "#room_alias:here";
|
||||
setJoinRule("public");
|
||||
setAliases([alias, "#another:one"]);
|
||||
setAliases([alias, "#another:here"]);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name).toEqual(alias);
|
||||
@@ -1004,7 +1003,7 @@ describe("Room", function() {
|
||||
|
||||
it("should emit an event when a receipt is added",
|
||||
function() {
|
||||
const listener = expect.createSpy();
|
||||
const listener = jest.fn();
|
||||
room.on("Room.receipt", listener);
|
||||
|
||||
const ts = 13787898424;
|
||||
@@ -1175,7 +1174,7 @@ describe("Room", function() {
|
||||
it("should emit Room.tags event when new tags are " +
|
||||
"received on the event stream",
|
||||
function() {
|
||||
const listener = expect.createSpy();
|
||||
const listener = jest.fn();
|
||||
room.on("Room.tags", listener);
|
||||
|
||||
const tags = { "m.foo": { "order": 0.5 } };
|
||||
|
||||
@@ -8,11 +8,9 @@ const MatrixScheduler = sdk.MatrixScheduler;
|
||||
const MatrixError = sdk.MatrixError;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
import lolex from 'lolex';
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("MatrixScheduler", function() {
|
||||
let clock;
|
||||
let scheduler;
|
||||
let retryFn;
|
||||
let queueFn;
|
||||
@@ -26,8 +24,6 @@ describe("MatrixScheduler", function() {
|
||||
});
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
clock = lolex.install();
|
||||
scheduler = new MatrixScheduler(function(ev, attempts, err) {
|
||||
if (retryFn) {
|
||||
return retryFn(ev, attempts, err);
|
||||
@@ -44,10 +40,6 @@ describe("MatrixScheduler", function() {
|
||||
defer = Promise.defer();
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
clock.uninstall();
|
||||
});
|
||||
|
||||
it("should process events in a queue in a FIFO manner", async function() {
|
||||
retryFn = function() {
|
||||
return 0;
|
||||
@@ -112,7 +104,7 @@ describe("MatrixScheduler", function() {
|
||||
defer.reject({});
|
||||
await retryDefer.promise;
|
||||
expect(procCount).toEqual(1);
|
||||
clock.tick(waitTimeMs);
|
||||
jest.advanceTimersByTime(waitTimeMs);
|
||||
await Promise.resolve();
|
||||
expect(procCount).toEqual(2);
|
||||
});
|
||||
@@ -203,7 +195,7 @@ describe("MatrixScheduler", function() {
|
||||
setTimeout(function() {
|
||||
deferA.resolve({});
|
||||
}, 1000);
|
||||
clock.tick(1000);
|
||||
jest.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
describe("queueEvent", function() {
|
||||
|
||||
@@ -16,9 +16,7 @@ limitations under the License.
|
||||
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
import utils from "../test-utils";
|
||||
import sdk from "../..";
|
||||
import expect from 'expect';
|
||||
|
||||
const SyncAccumulator = sdk.SyncAccumulator;
|
||||
|
||||
@@ -26,7 +24,6 @@ describe("SyncAccumulator", function() {
|
||||
let sa;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
sa = new SyncAccumulator({
|
||||
maxTimelineEntries: 10,
|
||||
});
|
||||
|
||||
@@ -7,7 +7,6 @@ const TimelineWindow = sdk.TimelineWindow;
|
||||
const TimelineIndex = require("../../lib/timeline-window").TimelineIndex;
|
||||
|
||||
const utils = require("../test-utils");
|
||||
import expect from 'expect';
|
||||
|
||||
const ROOM_ID = "roomId";
|
||||
const USER_ID = "userId";
|
||||
@@ -67,10 +66,6 @@ function createLinkedTimelines() {
|
||||
|
||||
|
||||
describe("TimelineIndex", function() {
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe("minIndex", function() {
|
||||
it("should return the min index relative to BaseIndex", function() {
|
||||
const timelineIndex = new TimelineIndex(createTimeline(), 0);
|
||||
@@ -153,7 +148,7 @@ describe("TimelineWindow", function() {
|
||||
let timelineSet;
|
||||
let client;
|
||||
function createWindow(timeline, opts) {
|
||||
timelineSet = {};
|
||||
timelineSet = {getTimelineForEvent: () => null};
|
||||
client = {};
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineSet0).toBe(timelineSet);
|
||||
@@ -163,12 +158,8 @@ describe("TimelineWindow", function() {
|
||||
return new TimelineWindow(client, timelineSet, opts);
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe("load", function() {
|
||||
it("should initialise from the live timeline", function(done) {
|
||||
it("should initialise from the live timeline", function() {
|
||||
const liveTimeline = createTimeline();
|
||||
const room = {};
|
||||
room.getLiveTimeline = function() {
|
||||
@@ -176,17 +167,17 @@ describe("TimelineWindow", function() {
|
||||
};
|
||||
|
||||
const timelineWindow = new TimelineWindow(undefined, room);
|
||||
timelineWindow.load(undefined, 2).then(function() {
|
||||
return timelineWindow.load(undefined, 2).then(function() {
|
||||
const expectedEvents = liveTimeline.getEvents().slice(1);
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should initialise from a specific event", function(done) {
|
||||
it("should initialise from a specific event", function() {
|
||||
const timeline = createTimeline();
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
const timelineSet = {};
|
||||
const timelineSet = {getTimelineForEvent: () => null};
|
||||
const client = {};
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineSet0).toBe(timelineSet);
|
||||
@@ -195,21 +186,20 @@ describe("TimelineWindow", function() {
|
||||
};
|
||||
|
||||
const timelineWindow = new TimelineWindow(client, timelineSet);
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("canPaginate should return false until load has returned",
|
||||
function(done) {
|
||||
it("canPaginate should return false until load has returned", function() {
|
||||
const timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok1", EventTimeline.BACKWARDS);
|
||||
timeline.setPaginationToken("toktok2", EventTimeline.FORWARDS);
|
||||
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
const timelineSet = {};
|
||||
const timelineSet = {getTimelineForEvent: () => null};
|
||||
const client = {};
|
||||
|
||||
const timelineWindow = new TimelineWindow(client, timelineSet);
|
||||
@@ -222,25 +212,24 @@ describe("TimelineWindow", function() {
|
||||
return Promise.resolve(timeline);
|
||||
};
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.BACKWARDS))
|
||||
.toBe(true);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(true);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("pagination", function() {
|
||||
it("should be able to advance across the initial timeline",
|
||||
function(done) {
|
||||
it("should be able to advance across the initial timeline", function() {
|
||||
const timeline = createTimeline();
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
const timelineWindow = createWindow(timeline);
|
||||
|
||||
timelineWindow.load(eventId, 1).then(function() {
|
||||
return timelineWindow.load(eventId, 1).then(function() {
|
||||
const expectedEvents = [timeline.getEvents()[1]];
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
@@ -277,15 +266,15 @@ describe("TimelineWindow", function() {
|
||||
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(false);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should advance into next timeline", function(done) {
|
||||
it("should advance into next timeline", function() {
|
||||
const tls = createLinkedTimelines();
|
||||
const eventId = tls[0].getEvents()[1].getId();
|
||||
const timelineWindow = createWindow(tls[0], {windowLimit: 5});
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = tls[0].getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
@@ -322,15 +311,15 @@ describe("TimelineWindow", function() {
|
||||
return timelineWindow.paginate(EventTimeline.FORWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(false);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should retreat into previous timeline", function(done) {
|
||||
it("should retreat into previous timeline", function() {
|
||||
const tls = createLinkedTimelines();
|
||||
const eventId = tls[1].getEvents()[1].getId();
|
||||
const timelineWindow = createWindow(tls[1], {windowLimit: 5});
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = tls[1].getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
@@ -367,10 +356,10 @@ describe("TimelineWindow", function() {
|
||||
return timelineWindow.paginate(EventTimeline.BACKWARDS, 2);
|
||||
}).then(function(success) {
|
||||
expect(success).toBe(false);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should make forward pagination requests", function(done) {
|
||||
it("should make forward pagination requests", function() {
|
||||
const timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
|
||||
|
||||
@@ -386,7 +375,7 @@ describe("TimelineWindow", function() {
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
@@ -399,11 +388,11 @@ describe("TimelineWindow", function() {
|
||||
expect(success).toBe(true);
|
||||
const expectedEvents = timeline.getEvents().slice(0, 5);
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should make backward pagination requests", function(done) {
|
||||
it("should make backward pagination requests", function() {
|
||||
const timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok", EventTimeline.BACKWARDS);
|
||||
|
||||
@@ -419,7 +408,7 @@ describe("TimelineWindow", function() {
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
@@ -432,11 +421,10 @@ describe("TimelineWindow", function() {
|
||||
expect(success).toBe(true);
|
||||
const expectedEvents = timeline.getEvents().slice(1, 6);
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
|
||||
it("should limit the number of unsuccessful pagination requests",
|
||||
function(done) {
|
||||
it("should limit the number of unsuccessful pagination requests", function() {
|
||||
const timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
|
||||
|
||||
@@ -452,7 +440,7 @@ describe("TimelineWindow", function() {
|
||||
return Promise.resolve(true);
|
||||
};
|
||||
|
||||
timelineWindow.load(eventId, 3).then(function() {
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = timeline.getEvents();
|
||||
expect(timelineWindow.getEvents()).toEqual(expectedEvents);
|
||||
|
||||
@@ -471,7 +459,7 @@ describe("TimelineWindow", function() {
|
||||
.toBe(false);
|
||||
expect(timelineWindow.canPaginate(EventTimeline.FORWARDS))
|
||||
.toBe(true);
|
||||
}).nodeify(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,14 +4,11 @@ const sdk = require("../..");
|
||||
const User = sdk.User;
|
||||
const utils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("User", function() {
|
||||
const userId = "@alice:bar";
|
||||
let user;
|
||||
|
||||
beforeEach(function() {
|
||||
utils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
user = new User(userId);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const utils = require("../../lib/utils");
|
||||
const testUtils = require("../test-utils");
|
||||
|
||||
import expect from 'expect';
|
||||
|
||||
describe("utils", function() {
|
||||
beforeEach(function() {
|
||||
testUtils.beforeEach(this); // eslint-disable-line babel/no-invalid-this
|
||||
});
|
||||
|
||||
describe("encodeParams", function() {
|
||||
it("should url encode and concat with &s", function() {
|
||||
const params = {
|
||||
@@ -135,7 +128,7 @@ describe("utils", function() {
|
||||
utils.checkObjectHasKeys({
|
||||
foo: "bar",
|
||||
}, ["foo"]);
|
||||
}).toNotThrow();
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -152,7 +145,7 @@ describe("utils", function() {
|
||||
utils.checkObjectHasNoAdditionalKeys({
|
||||
foo: "bar",
|
||||
}, ["foo"]);
|
||||
}).toNotThrow();
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+24
-13
@@ -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.
|
||||
|
||||
+445
-22
@@ -63,6 +63,15 @@ function termsUrlForService(serviceType, baseUrl) {
|
||||
*
|
||||
* @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.
|
||||
@@ -79,6 +88,7 @@ function MatrixBaseApis(opts) {
|
||||
|
||||
this.baseUrl = opts.baseUrl;
|
||||
this.idBaseUrl = opts.idBaseUrl;
|
||||
this.identityServer = opts.identityServer;
|
||||
|
||||
const httpOpts = {
|
||||
baseUrl: opts.baseUrl,
|
||||
@@ -117,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
|
||||
@@ -478,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;
|
||||
};
|
||||
@@ -972,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,
|
||||
});
|
||||
@@ -983,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(
|
||||
@@ -1314,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) {
|
||||
@@ -1331,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')
|
||||
@@ -1618,6 +1718,15 @@ MatrixBaseApis.prototype.uploadKeysRequest = function(content, opts, callback) {
|
||||
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,
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Download device keys
|
||||
*
|
||||
@@ -1702,6 +1811,14 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
|
||||
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
|
||||
// ==========================
|
||||
@@ -1720,6 +1837,10 @@ MatrixBaseApis.prototype.getKeyChanges = function(oldToken, newToken) {
|
||||
* @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,
|
||||
@@ -1728,10 +1849,11 @@ MatrixBaseApis.prototype.registerWithIdentityServer = function(hsOpenIdToken) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Requests an email verification token directly from an Identity Server.
|
||||
* Requests an email verification token directly from an identity server.
|
||||
*
|
||||
* Note that the Homeserver offers APIs to proxy this API for specific
|
||||
* situations, allowing for better feedback to the user.
|
||||
* 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.
|
||||
@@ -1743,12 +1865,12 @@ MatrixBaseApis.prototype.registerWithIdentityServer = function(hsOpenIdToken) {
|
||||
* @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}).
|
||||
* @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
|
||||
* @throws Error if no identity server is set
|
||||
*/
|
||||
MatrixBaseApis.prototype.requestEmailToken = async function(
|
||||
email,
|
||||
@@ -1790,7 +1912,75 @@ MatrixBaseApis.prototype.requestEmailToken = async function(
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1840,6 +2030,137 @@ MatrixBaseApis.prototype.submitMsisdnToken = async function(
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
@@ -1861,35 +2182,131 @@ MatrixBaseApis.prototype.lookupThreePid = async function(
|
||||
callback,
|
||||
identityAccessToken,
|
||||
) {
|
||||
const params = {
|
||||
medium: medium,
|
||||
address: address,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this._http.idServerRequest(
|
||||
undefined, "GET", "/lookup",
|
||||
params, httpApi.PREFIX_IDENTITY_V2, identityAccessToken,
|
||||
// Note: we're using the V2 API by calling this function, but our
|
||||
// function contract requires a V1 response. We therefore have to
|
||||
// convert it manually.
|
||||
const response = await this.identityHashedLookup(
|
||||
[[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, response);
|
||||
return response;
|
||||
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);
|
||||
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
|
||||
// ==========================
|
||||
@@ -1916,6 +2333,12 @@ MatrixBaseApis.prototype.sendToDevice = function(
|
||||
messages: contentMap,
|
||||
};
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
|
||||
+727
-197
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
+89
-21
@@ -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,11 +24,14 @@ 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';
|
||||
import {sleep} from '../utils';
|
||||
|
||||
|
||||
/* State transition diagram for DeviceList._deviceTrackingStatus
|
||||
@@ -60,8 +64,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 +77,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 +122,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 +213,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 +347,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 +585,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 +652,7 @@ export default class DeviceList {
|
||||
}
|
||||
});
|
||||
this.saveIfDirty();
|
||||
this.emit("crypto.devicesUpdated", users);
|
||||
};
|
||||
|
||||
return prom;
|
||||
@@ -724,6 +753,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)
|
||||
@@ -732,8 +764,14 @@ class DeviceListUpdateSerialiser {
|
||||
// this serves as an easy solution for now.
|
||||
let prom = Promise.resolve();
|
||||
for (const userId of downloadUsers) {
|
||||
prom = prom.delay(5).then(() => {
|
||||
return this._processQueryResponseForUser(userId, dk[userId]);
|
||||
prom = prom.then(sleep(5)).then(() => {
|
||||
return this._processQueryResponseForUser(
|
||||
userId, dk[userId], {
|
||||
master: masterKeys[userId],
|
||||
self_signing: ssks[userId],
|
||||
user_signing: usks[userId],
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -757,30 +795,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 +920,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 +953,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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -139,7 +139,7 @@ OlmEncryption.prototype.encryptMessage = async function(room, eventType, content
|
||||
}
|
||||
}
|
||||
|
||||
return await Promise.all(promises).return(encryptedContent);
|
||||
return await Promise.all(promises).then(() => encryptedContent);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
+818
-63
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,35 +58,34 @@ export class Backend {
|
||||
getOrAddOutgoingRoomKeyRequest(request) {
|
||||
const requestBody = request.requestBody;
|
||||
|
||||
const deferred = Promise.defer();
|
||||
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
|
||||
txn.onerror = deferred.reject;
|
||||
return new Promise((resolve, reject) => {
|
||||
const txn = this._db.transaction("outgoingRoomKeyRequests", "readwrite");
|
||||
txn.onerror = reject;
|
||||
|
||||
// first see if we already have an entry for this request.
|
||||
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
|
||||
if (existing) {
|
||||
// this entry matches the request - return it.
|
||||
// first see if we already have an entry for this request.
|
||||
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
|
||||
if (existing) {
|
||||
// this entry matches the request - return it.
|
||||
logger.log(
|
||||
`already have key request outstanding for ` +
|
||||
`${requestBody.room_id} / ${requestBody.session_id}: ` +
|
||||
`not sending another`,
|
||||
);
|
||||
resolve(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
// we got to the end of the list without finding a match
|
||||
// - add the new request.
|
||||
logger.log(
|
||||
`already have key request outstanding for ` +
|
||||
`${requestBody.room_id} / ${requestBody.session_id}: ` +
|
||||
`not sending another`,
|
||||
`enqueueing key request for ${requestBody.room_id} / ` +
|
||||
requestBody.session_id,
|
||||
);
|
||||
deferred.resolve(existing);
|
||||
return;
|
||||
}
|
||||
|
||||
// we got to the end of the list without finding a match
|
||||
// - add the new request.
|
||||
logger.log(
|
||||
`enqueueing key request for ${requestBody.room_id} / ` +
|
||||
requestBody.session_id,
|
||||
);
|
||||
txn.oncomplete = () => { deferred.resolve(request); };
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
store.add(request);
|
||||
txn.oncomplete = () => { resolve(request); };
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
store.add(request);
|
||||
});
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,15 +99,14 @@ export class Backend {
|
||||
* not found
|
||||
*/
|
||||
getOutgoingRoomKeyRequest(requestBody) {
|
||||
const deferred = Promise.defer();
|
||||
return new Promise((resolve, reject) => {
|
||||
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||
txn.onerror = reject;
|
||||
|
||||
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||
txn.onerror = deferred.reject;
|
||||
|
||||
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
|
||||
deferred.resolve(existing);
|
||||
this._getOutgoingRoomKeyRequest(txn, requestBody, (existing) => {
|
||||
resolve(existing);
|
||||
});
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -332,6 +330,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) {
|
||||
|
||||
@@ -287,10 +287,12 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {function(string)} func Called with the account pickle
|
||||
*/
|
||||
getAccount(txn, func) {
|
||||
this._backendPromise.value().getAccount(txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getAccount(txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
/**
|
||||
* Write the account pickle to the store.
|
||||
* This requires an active transaction. See doTxn().
|
||||
*
|
||||
@@ -298,7 +300,35 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {string} newData The new account pickle to store.
|
||||
*/
|
||||
storeAccount(txn, newData) {
|
||||
this._backendPromise.value().storeAccount(txn, newData);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.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.then(backend => {
|
||||
backend.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.then(backend => {
|
||||
backend.storeCrossSigningKeys(txn, keys);
|
||||
});
|
||||
}
|
||||
|
||||
// Olm sessions
|
||||
@@ -309,7 +339,9 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {function(int)} func Called with the count of sessions
|
||||
*/
|
||||
countEndToEndSessions(txn, func) {
|
||||
this._backendPromise.value().countEndToEndSessions(txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.countEndToEndSessions(txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -325,7 +357,9 @@ export default class IndexedDBCryptoStore {
|
||||
* a message.
|
||||
*/
|
||||
getEndToEndSession(deviceKey, sessionId, txn, func) {
|
||||
this._backendPromise.value().getEndToEndSession(deviceKey, sessionId, txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndSession(deviceKey, sessionId, txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -340,7 +374,9 @@ export default class IndexedDBCryptoStore {
|
||||
* a message.
|
||||
*/
|
||||
getEndToEndSessions(deviceKey, txn, func) {
|
||||
this._backendPromise.value().getEndToEndSessions(deviceKey, txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndSessions(deviceKey, txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -351,7 +387,9 @@ export default class IndexedDBCryptoStore {
|
||||
* and session keys.
|
||||
*/
|
||||
getAllEndToEndSessions(txn, func) {
|
||||
this._backendPromise.value().getAllEndToEndSessions(txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getAllEndToEndSessions(txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -362,12 +400,14 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
|
||||
this._backendPromise.value().storeEndToEndSession(
|
||||
deviceKey, sessionId, sessionInfo, txn,
|
||||
);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndSession(
|
||||
deviceKey, sessionId, sessionInfo, txn,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Inbound group saessions
|
||||
// Inbound group sessions
|
||||
|
||||
/**
|
||||
* Retrieve the end-to-end inbound group session for a given
|
||||
@@ -379,9 +419,11 @@ export default class IndexedDBCryptoStore {
|
||||
* to Base64 end-to-end session.
|
||||
*/
|
||||
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
|
||||
this._backendPromise.value().getEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, txn, func,
|
||||
);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, txn, func,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -392,7 +434,9 @@ export default class IndexedDBCryptoStore {
|
||||
* sessionData}, then once with null to indicate the end of the list.
|
||||
*/
|
||||
getAllEndToEndInboundGroupSessions(txn, func) {
|
||||
this._backendPromise.value().getAllEndToEndInboundGroupSessions(txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getAllEndToEndInboundGroupSessions(txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -405,9 +449,11 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
|
||||
this._backendPromise.value().addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -420,9 +466,11 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
|
||||
this._backendPromise.value().storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// End-to-end device tracking
|
||||
@@ -438,7 +486,9 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndDeviceData(deviceData, txn) {
|
||||
this._backendPromise.value().storeEndToEndDeviceData(deviceData, txn);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndDeviceData(deviceData, txn);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -449,7 +499,9 @@ export default class IndexedDBCryptoStore {
|
||||
* device data
|
||||
*/
|
||||
getEndToEndDeviceData(txn, func) {
|
||||
this._backendPromise.value().getEndToEndDeviceData(txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndDeviceData(txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
// End to End Rooms
|
||||
@@ -461,7 +513,9 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndRoom(roomId, roomInfo, txn) {
|
||||
this._backendPromise.value().storeEndToEndRoom(roomId, roomInfo, txn);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndRoom(roomId, roomInfo, txn);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -470,7 +524,9 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {function(Object)} func Function called with the end to end encrypted rooms
|
||||
*/
|
||||
getEndToEndRooms(txn, func) {
|
||||
this._backendPromise.value().getEndToEndRooms(txn, func);
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndRooms(txn, func);
|
||||
});
|
||||
}
|
||||
|
||||
// session backups
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+120
-22
@@ -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,42 +48,55 @@ 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);
|
||||
}
|
||||
this._transactionTimeoutTimer = setTimeout(() => {
|
||||
if (!this._done && !this.cancelled) {
|
||||
console.log("Triggering verification timeout");
|
||||
this.cancel(timeoutException);
|
||||
}
|
||||
if (!this._done && !this.cancelled) {
|
||||
logger.info("Triggering verification timeout");
|
||||
this.cancel(timeoutException);
|
||||
}
|
||||
}, 10 * 60 * 1000); // 10 minutes
|
||||
}
|
||||
|
||||
@@ -93,16 +107,57 @@ export default class VerificationBase extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
_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"));
|
||||
@@ -122,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;
|
||||
@@ -140,6 +199,10 @@ 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();
|
||||
}
|
||||
}
|
||||
@@ -153,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) {
|
||||
@@ -163,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,
|
||||
@@ -173,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,
|
||||
@@ -185,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
|
||||
@@ -206,11 +275,23 @@ export default class VerificationBase extends EventEmitter {
|
||||
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);
|
||||
};
|
||||
});
|
||||
@@ -232,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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,4 +344,8 @@ export default class VerificationBase extends EventEmitter {
|
||||
await this._baseApis.setDeviceVerified(userId, deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
setEventsSubscription(subscription) {
|
||||
this._eventsSubscription = subscription;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+21
-85
@@ -23,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
|
||||
@@ -96,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.
|
||||
@@ -382,6 +389,10 @@ module.exports.MatrixHttpApi.prototype = {
|
||||
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)) {
|
||||
@@ -394,18 +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}`,
|
||||
};
|
||||
opts.headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
}
|
||||
|
||||
const defer = Promise.defer();
|
||||
@@ -413,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;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -543,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
|
||||
@@ -901,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
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+10
-1
@@ -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.
|
||||
@@ -297,6 +297,15 @@ utils.extend(module.exports.MatrixEvent.prototype, {
|
||||
return this.getUnsigned().age || this.event.age; // v2 / v1
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the age of the event when this function was called.
|
||||
* Relies on the local clock being in sync with the clock of the original homeserver.
|
||||
* @return {Number} The age of this event in milliseconds.
|
||||
*/
|
||||
getLocalAge: function() {
|
||||
return Date.now() - this.getTs();
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the event state_key if it has one. This will return <code>undefined
|
||||
* </code> for message events.
|
||||
|
||||
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import { EventStatus } from '../../lib/models/event';
|
||||
import { EventStatus } from '../models/event';
|
||||
|
||||
/**
|
||||
* A container for relation events that supports easy access to common ways of
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
+29
-6
@@ -30,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
|
||||
@@ -380,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
|
||||
@@ -822,9 +839,15 @@ Room.prototype.getAliases = function() {
|
||||
for (let i = 0; i < aliasEvents.length; ++i) {
|
||||
const aliasEvent = aliasEvents[i];
|
||||
if (utils.isArray(aliasEvent.getContent().aliases)) {
|
||||
Array.prototype.push.apply(
|
||||
aliasStrings, aliasEvent.getContent().aliases,
|
||||
);
|
||||
const filteredAliases = aliasEvent.getContent().aliases.filter(a => {
|
||||
if (typeof(a) !== "string") return false;
|
||||
if (a[0] !== '#') return false;
|
||||
if (!a.endsWith(`:${aliasEvent.getStateKey()}`)) return false;
|
||||
|
||||
// It's probably valid by here.
|
||||
return true;
|
||||
});
|
||||
Array.prototype.push.apply(aliasStrings, filteredAliases);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -878,7 +901,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||
* @return {RoomMember} The member or <code>null</code>.
|
||||
*/
|
||||
Room.prototype.getMember = function(userId) {
|
||||
return this.currentState.getMember(userId);
|
||||
return this.currentState.getMember(userId);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -886,7 +909,7 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||
* @return {RoomMember[]} A list of currently joined members.
|
||||
*/
|
||||
Room.prototype.getJoinedMembers = function() {
|
||||
return this.getMembersWithMembership("join");
|
||||
return this.getMembersWithMembership("join");
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {escapeRegExp, globToRegexp} from "./utils";
|
||||
import {escapeRegExp, globToRegexp, isNullOrUndefined} from "./utils";
|
||||
|
||||
/**
|
||||
* @module pushprocessor
|
||||
@@ -268,7 +268,7 @@ function PushProcessor(client) {
|
||||
}
|
||||
|
||||
const val = valueForDottedKey(cond.key, ev);
|
||||
if (!val || typeof val != 'string') {
|
||||
if (typeof val !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -304,10 +304,10 @@ function PushProcessor(client) {
|
||||
|
||||
// special-case the first component to deal with encrypted messages
|
||||
const firstPart = parts[0];
|
||||
if (firstPart == 'content') {
|
||||
if (firstPart === 'content') {
|
||||
val = ev.getContent();
|
||||
parts.shift();
|
||||
} else if (firstPart == 'type') {
|
||||
} else if (firstPart === 'type') {
|
||||
val = ev.getType();
|
||||
parts.shift();
|
||||
} else {
|
||||
@@ -316,11 +316,11 @@ function PushProcessor(client) {
|
||||
}
|
||||
|
||||
while (parts.length > 0) {
|
||||
const thispart = parts.shift();
|
||||
if (!val[thispart]) {
|
||||
const thisPart = parts.shift();
|
||||
if (isNullOrUndefined(val[thisPart])) {
|
||||
return null;
|
||||
}
|
||||
val = val[thispart];
|
||||
val = val[thisPart];
|
||||
}
|
||||
return val;
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
@@ -48,7 +48,7 @@ const debuglog = function() {};
|
||||
*
|
||||
* Intended for use by the unit tests.
|
||||
*
|
||||
* @param {function} f function which should return a millisecond counter
|
||||
* @param {function} [f] function which should return a millisecond counter
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import Promise from 'bluebird';
|
||||
import logger from '../../src/logger';
|
||||
import logger from '../logger';
|
||||
import {defer} from '../utils';
|
||||
|
||||
/**
|
||||
* An IndexedDB store backend where the actual backend sits in a web
|
||||
@@ -152,7 +153,7 @@ RemoteIndexedDBStoreBackend.prototype = {
|
||||
// the promise automatically gets rejected
|
||||
return Promise.resolve().then(() => {
|
||||
const seq = this._nextSeq++;
|
||||
const def = Promise.defer();
|
||||
const def = defer();
|
||||
|
||||
this._inFlight[seq] = def;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -21,7 +21,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import utils from "./utils";
|
||||
import logger from '../src/logger';
|
||||
import logger from './logger';
|
||||
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ const utils = require("./utils");
|
||||
const Filter = require("./filter");
|
||||
const EventTimeline = require("./models/event-timeline");
|
||||
const PushProcessor = require("./pushprocessor");
|
||||
import logger from '../src/logger';
|
||||
import logger from './logger';
|
||||
|
||||
import {InvalidStoreError} from './errors';
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -132,14 +132,15 @@ TimelineWindow.prototype.load = function(initialEventId, initialWindowSize) {
|
||||
// feeling snappy.
|
||||
//
|
||||
if (initialEventId) {
|
||||
const prom = this._client.getEventTimeline(this._timelineSet, initialEventId);
|
||||
|
||||
if (prom.isFulfilled()) {
|
||||
initFields(prom.value());
|
||||
return Promise.resolve();
|
||||
} else {
|
||||
return prom.then(initFields);
|
||||
const timeline = this._timelineSet.getTimelineForEvent(initialEventId);
|
||||
if (timeline) {
|
||||
// hot-path optimization to save a reactor tick by replicating the sync check getTimelineForEvent does.
|
||||
initFields(timeline);
|
||||
return Promise.resolve(timeline);
|
||||
}
|
||||
|
||||
const prom = this._client.getEventTimeline(this._timelineSet, initialEventId);
|
||||
return prom.then(initFields);
|
||||
} else {
|
||||
const tl = this._timelineSet.getLiveTimeline();
|
||||
initFields(tl);
|
||||
|
||||
@@ -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.
|
||||
@@ -20,6 +21,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
const unhomoglyph = require('unhomoglyph');
|
||||
import Promise from 'bluebird';
|
||||
|
||||
/**
|
||||
* Encode a dictionary of query parameters.
|
||||
@@ -699,3 +701,33 @@ 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;
|
||||
}
|
||||
};
|
||||
|
||||
// Returns a promise which resolves with a given value after the given number of ms
|
||||
module.exports.sleep = (ms, value) => new Promise((resolve => {
|
||||
setTimeout(resolve, ms, value);
|
||||
}));
|
||||
|
||||
module.exports.isNullOrUndefined = function(val) {
|
||||
return val === null || val === undefined;
|
||||
};
|
||||
|
||||
// Returns a Deferred
|
||||
module.exports.defer = () => {
|
||||
let resolve;
|
||||
let reject;
|
||||
|
||||
const promise = new Promise((_resolve, _reject) => {
|
||||
resolve = _resolve;
|
||||
reject = _reject;
|
||||
});
|
||||
|
||||
return {resolve, reject, promise};
|
||||
};
|
||||
|
||||
+40
-41
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user