Compare commits
611 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f19ab066c | |||
| 17decea576 | |||
| 3339a2874d | |||
| 393047dec5 | |||
| 8c11839e4d | |||
| 41817995bd | |||
| 3a5e4ffa91 | |||
| 02afcc7d4b | |||
| e9007429dd | |||
| 664d920dd1 | |||
| 5a8299f1a5 | |||
| 6017fead19 | |||
| 69050ed338 | |||
| 2664848545 | |||
| 1171c33f7a | |||
| 6deb2bc7a9 | |||
| 7bda13aba6 | |||
| 75719c3e0f | |||
| 2fb033b5ba | |||
| 9f1db7a707 | |||
| a93de99d42 | |||
| 1d8e0398e9 | |||
| cf76375ce0 | |||
| 284e2bb911 | |||
| ecb790c179 | |||
| 467b75f2dc | |||
| 1f728cab92 | |||
| 66b17aa019 | |||
| 27a6d1f878 | |||
| fc67dc6497 | |||
| fedd9a981a | |||
| 47e972a66c | |||
| 36bd4b5408 | |||
| f502233ddc | |||
| 59c1edb623 | |||
| 9dd00c7731 | |||
| 311edb4e4c | |||
| 924b8629d8 | |||
| 9e11da1fa5 | |||
| b760fa0ff5 | |||
| cbce2f46c3 | |||
| 7aa5a79c86 | |||
| 2c6858f149 | |||
| c1bff0b2ea | |||
| 39892c98f9 | |||
| b15487ec03 | |||
| 303119e113 | |||
| 09b729beb5 | |||
| 6b73a77b2d | |||
| a2449ff6a7 | |||
| b1b7522b80 | |||
| 608b0e7b93 | |||
| 7c61b9cf7e | |||
| 50a973409a | |||
| bfea882416 | |||
| f5e8fe836e | |||
| c4664a185f | |||
| c5f093c42f | |||
| 64f369b5de | |||
| 4b5653c09b | |||
| 837e739ec6 | |||
| b780ee8373 | |||
| fe5bfbf76f | |||
| d924617672 | |||
| c545c7ca70 | |||
| 9eb9f3a117 | |||
| 7862fd9679 | |||
| 17402e8475 | |||
| aac77440db | |||
| 13c9c4bea5 | |||
| 68c1171294 | |||
| f4f01913fe | |||
| eb5908d5d2 | |||
| e0c36498e6 | |||
| 913710dd99 | |||
| 2f0d96d030 | |||
| 265802acb1 | |||
| 045f31a0dc | |||
| 9f6ed4fb33 | |||
| 434117c771 | |||
| cd95b8724a | |||
| ec2a4d473e | |||
| 47eaf3cd58 | |||
| d7b23a8634 | |||
| 6db7972f04 | |||
| 6840ee077c | |||
| 3c85bcc3c9 | |||
| 759eca6479 | |||
| 4488f174aa | |||
| 991a255041 | |||
| cfef635e1b | |||
| d3027e1fe8 | |||
| d99ea1c6b4 | |||
| 9af214007e | |||
| 0541b7f3c5 | |||
| 63fa774af7 | |||
| 4b19b36de1 | |||
| 5715df6b18 | |||
| 22bcacc715 | |||
| f1e270ca9d | |||
| f535e7535c | |||
| 912c5e13e1 | |||
| 4eb44ee2ea | |||
| e41a2beb65 | |||
| 1f6ba31a3f | |||
| bcccc909c5 | |||
| b3a11030f2 | |||
| baaf76668f | |||
| 36e2533164 | |||
| 9084b4e7aa | |||
| f724da7e84 | |||
| 4b8f47e2b4 | |||
| 35ecbed29d | |||
| 30ed153ad1 | |||
| a1098989ff | |||
| 42bb63e07d | |||
| 683aae5ed5 | |||
| 49ef274a83 | |||
| af16bba1ec | |||
| 781086608e | |||
| e9c98b03b0 | |||
| 75370f7855 | |||
| 4a79e13410 | |||
| 220061f022 | |||
| 6c317b7f28 | |||
| 3788fbf607 | |||
| 382854c04c | |||
| c2fae3bad8 | |||
| 92ebd39391 | |||
| f53a32a6b4 | |||
| 6c882e6605 | |||
| ca85dfc6ff | |||
| e22ecc6b6d | |||
| 2608dd2d64 | |||
| 7d20d24249 | |||
| a3ac3692af | |||
| 0070c8f843 | |||
| b360fc8308 | |||
| bf9ba65ac4 | |||
| 43abfbc537 | |||
| 2700f0acf6 | |||
| 9156bed961 | |||
| 71dc0bac56 | |||
| 40f55b2964 | |||
| 9307f9f345 | |||
| dfb918adc3 | |||
| 2f87a4859e | |||
| 191f73e0d0 | |||
| 263e55f25d | |||
| 5c55dce13e | |||
| a1a6ec6dfa | |||
| e1edd84700 | |||
| 07ee256756 | |||
| 48888e530e | |||
| 4ef50bef55 | |||
| 486369e97c | |||
| 67994f7a53 | |||
| f027ddaf35 | |||
| f3b27d1e06 | |||
| 92e18b32dc | |||
| 4030ec9c8b | |||
| 497c2dc8df | |||
| 8a1d34c419 | |||
| caab5befaa | |||
| 0d316e3d3e | |||
| 8f9a682d34 | |||
| 102d5acf09 | |||
| e2ec8952e3 | |||
| b4eff9b996 | |||
| d050261fa9 | |||
| d29330815e | |||
| 801b4022de | |||
| e78fbd1dff | |||
| c543358826 | |||
| ff2954839b | |||
| 1a91b88968 | |||
| 25ec81b6c7 | |||
| a50802a63f | |||
| ce1d374a82 | |||
| 6dca8ac460 | |||
| 4dc21674d5 | |||
| 8805dd8c01 | |||
| 73b624e761 | |||
| c44fd972b6 | |||
| ff344bc110 | |||
| b0e2a38325 | |||
| fbb741ab10 | |||
| 3a3be36f4c | |||
| cb91c4292c | |||
| 80722ce145 | |||
| 07bfa5532e | |||
| cea1a3ff91 | |||
| 270be2df7a | |||
| e73b969066 | |||
| 98e2154f0f | |||
| 2c0549a772 | |||
| acb9bc8cc5 | |||
| 6f44aa39e8 | |||
| c706618229 | |||
| c26b571b1c | |||
| cb3075b084 | |||
| 66fb21bd0b | |||
| 498109bd53 | |||
| 6711ab5f9a | |||
| ac8fa58845 | |||
| 095f656998 | |||
| 9e30c4f7dd | |||
| 56a2eeac77 | |||
| d104f2f4a7 | |||
| e8367ad241 | |||
| f17cd142d5 | |||
| e8edc554a6 | |||
| bd8aca83ac | |||
| 7ebc1cfac5 | |||
| 2422204d6a | |||
| 6c98c3c662 | |||
| 1d1310f034 | |||
| 841888c480 | |||
| 52c031f160 | |||
| 155113b75d | |||
| 2a863025c6 | |||
| b6763ce89f | |||
| f346cd6b8d | |||
| 0c47412c75 | |||
| ea1ef3dbec | |||
| 61fd62ff81 | |||
| 32197ea903 | |||
| 65de184d88 | |||
| 52764045ce | |||
| 4b0db6c472 | |||
| 3ec89a89df | |||
| 9e6b72bf38 | |||
| 747723c8fb | |||
| 40cd4629db | |||
| 52a893a811 | |||
| 7cc94e1e1e | |||
| 88945a6d6d | |||
| 7203c5aaf0 | |||
| c305058c46 | |||
| 91bbf7d1a8 | |||
| 2d5857f145 | |||
| 91af9a411d | |||
| 9a9ed124f5 | |||
| 4e5442d972 | |||
| b5fa54f91e | |||
| 2246aede4b | |||
| 4b2d409c69 | |||
| 684511ff06 | |||
| 88b328df7e | |||
| e3583dd04e | |||
| e484a2ebb5 | |||
| 5caf05cfa1 | |||
| 72f86258d0 | |||
| 874cb3b779 | |||
| f21e0228b4 | |||
| 01cd82bc6a | |||
| d2a6a8b283 | |||
| f242e460ed | |||
| 95e08253a6 | |||
| 202a4fa6f1 | |||
| 576f46cb88 | |||
| 2d73805ca3 | |||
| eedfa550a6 | |||
| 4ca718adc4 | |||
| 7c4ced8f46 | |||
| 7018f4ab25 | |||
| fda13875ef | |||
| 8005917452 | |||
| 91eee8587e | |||
| e9132abc25 | |||
| 444eac5c6e | |||
| a449c5f8c2 | |||
| 19d6dbaa52 | |||
| 8820619e82 | |||
| fb33bc7e07 | |||
| 57d1fa8410 | |||
| 838e38d84c | |||
| fc29056530 | |||
| 4e967c979c | |||
| 62a34848c7 | |||
| 01fe6cc542 | |||
| 3fdc25777d | |||
| 3b7d6f8334 | |||
| 4b3c8b2969 | |||
| ad80d69369 | |||
| e2d2249686 | |||
| dc64c34ccb | |||
| 0bf50659ab | |||
| ec26b16ddf | |||
| a044b74a1d | |||
| 1e7a1dce90 | |||
| 41c2772cff | |||
| 1bba2bc0ed | |||
| e11c523a75 | |||
| b911a890cf | |||
| c8f69c0b79 | |||
| 8a6248f120 | |||
| 340fa6c63e | |||
| 1177bf39a2 | |||
| 88b310c394 | |||
| 973de2db55 | |||
| 1fe92f10c1 | |||
| 0e2e906b24 | |||
| 4667f8be03 | |||
| 4a51ac7a74 | |||
| 6099efe41a | |||
| 4254d595fc | |||
| e4fbbd56a9 | |||
| 069ca4a89d | |||
| 4290e8e56b | |||
| e3ba08fbbc | |||
| dd84e51161 | |||
| bca8568d64 | |||
| 5407717534 | |||
| 74ef760591 | |||
| b435b582bd | |||
| d46021a05e | |||
| 1b31d0622e | |||
| a416fd562b | |||
| 323a096dd7 | |||
| 628dd7bf41 | |||
| 71c6d71cae | |||
| e049edd449 | |||
| 68206a6e19 | |||
| 56797948af | |||
| c0af2f25a1 | |||
| 0fdfc3ff53 | |||
| dc12b1df00 | |||
| b13f5aebfd | |||
| 338301bb5d | |||
| 72a0931663 | |||
| 67584b9cc3 | |||
| 1bfaa28f9c | |||
| cbe9b59222 | |||
| 276b52f0fe | |||
| 07f49bcc37 | |||
| 09fac77ce0 | |||
| 102704e91a | |||
| c5fb351baa | |||
| 98f8d4414d | |||
| 1dddcd4925 | |||
| 2666a271a5 | |||
| e277de6e3d | |||
| daa17b3287 | |||
| c7f887131e | |||
| 58546b80d0 | |||
| 466f749b71 | |||
| 4c2a83c470 | |||
| d534ab18c7 | |||
| 64aaed833b | |||
| 2f05be599c | |||
| 7371f8dd3a | |||
| 79aefe9707 | |||
| 6bc80577ee | |||
| 837764190f | |||
| 61948d70e3 | |||
| 9fb0385694 | |||
| 193de8d3a9 | |||
| 1f2b3512c1 | |||
| ddc5bd3b36 | |||
| ee828d454a | |||
| 2916a73f4c | |||
| ae69af7e70 | |||
| 1c6459fe65 | |||
| adaeb42416 | |||
| f1e1daa194 | |||
| 401e89ef78 | |||
| 59e0bd467c | |||
| 7f4397f8ca | |||
| 32830b93f1 | |||
| cdc0d5623b | |||
| e78b415832 | |||
| ff1379fd29 | |||
| 3820c15ecf | |||
| 26ef33e4f3 | |||
| 0534a4ed1b | |||
| f29a24a915 | |||
| cecbcd941e | |||
| 6be99d6397 | |||
| 4e5947af51 | |||
| 4204b2170a | |||
| 0a5ad489b6 | |||
| 5de34a5c99 | |||
| 08da6b8800 | |||
| 02b283be78 | |||
| 10c49c0fd1 | |||
| 9ecc0f5d95 | |||
| 972b59b99e | |||
| 34bb05bd88 | |||
| 37fb21f726 | |||
| b42efa4a07 | |||
| 20b20738a7 | |||
| b28a191c4e | |||
| f92b620434 | |||
| ae6e2cca27 | |||
| bd920eef1f | |||
| bf25cb68da | |||
| 0b063f6b8b | |||
| ed6d4e5f6c | |||
| accfa325b5 | |||
| b6ef8d95cd | |||
| c1144e3810 | |||
| 8663fd402b | |||
| d8134aa168 | |||
| 702b3e8473 | |||
| f34052fd31 | |||
| 27d75a269f | |||
| d208a7fc5f | |||
| 702e16e3df | |||
| 6381018658 | |||
| 12050b14f0 | |||
| 1c191b2278 | |||
| 2d4a4f1736 | |||
| cd38fb9b4c | |||
| 7941b16ec4 | |||
| 3ff517e76e | |||
| 9559b26310 | |||
| 56ea4b8741 | |||
| a489691151 | |||
| 6dabfcda6f | |||
| 0b7b35f800 | |||
| 0bfcb5071d | |||
| ceb162eb01 | |||
| 0ffdf7c0f1 | |||
| 13b6db8eb4 | |||
| 481acb2a1a | |||
| 683092140d | |||
| 1bb8c2d1a5 | |||
| 60fd3b0786 | |||
| b307a177f4 | |||
| 059430bd0a | |||
| 530b60cbc2 | |||
| cd4abc4e9b | |||
| e6a21cc487 | |||
| ba8577f268 | |||
| 6c5fc153bf | |||
| 07f15b41a2 | |||
| 8375638d76 | |||
| bed7543b46 | |||
| dc55236263 | |||
| 5f3427c5d1 | |||
| 66e5af185d | |||
| 0ff611e033 | |||
| 737cadaabc | |||
| 9fb2fbaeec | |||
| 51e817a3a2 | |||
| 59c93b59bf | |||
| c18ef051fc | |||
| 1ac5c9acbd | |||
| a034ca171e | |||
| 977682d37f | |||
| 0bafe263d7 | |||
| 1a8fced80e | |||
| 1c4d0b5e99 | |||
| 844a2b457c | |||
| ccf06f2216 | |||
| f630a9f297 | |||
| 2f71c93b53 | |||
| 92032a17a8 | |||
| e531456d42 | |||
| f0b2d2fe4d | |||
| 427500220d | |||
| 32e19ead74 | |||
| d11adb6f43 | |||
| f6155a50f6 | |||
| 4efee9445d | |||
| 20746a433f | |||
| 31dacc4206 | |||
| 88e5c59a85 | |||
| cf74920b36 | |||
| 972c900b58 | |||
| 12d5fd79f7 | |||
| a29f6979b2 | |||
| 3a7146c77b | |||
| b178d8f629 | |||
| 0c94ee62a3 | |||
| e7562898cd | |||
| fb73ab6878 | |||
| 38f978791d | |||
| 5dd60de57d | |||
| 5efbfc2dba | |||
| fcd1dbad89 | |||
| ad521bf4c2 | |||
| 8152fa44e0 | |||
| e217bf9e37 | |||
| bfad21f811 | |||
| 81e68abce3 | |||
| 7963bb352d | |||
| 14d3882059 | |||
| dede508e89 | |||
| ea39b69f65 | |||
| eafecd36bc | |||
| 198c9a2507 | |||
| d07563013b | |||
| 9e967832cd | |||
| bfe1987cd9 | |||
| 1045538f1f | |||
| fccf08edcf | |||
| f43fe366b5 | |||
| 0f75f2ef9c | |||
| 2cdc68f9c3 | |||
| 6a7d58e22e | |||
| 203829c1cd | |||
| b55e6c4ef0 | |||
| 8d779e8aec | |||
| dd1d48f688 | |||
| 8d14dc9ee3 | |||
| 5849ea8e63 | |||
| 20afebf339 | |||
| 20eaba191e | |||
| ba58d3c544 | |||
| a8b9d8e3ae | |||
| 83d1e61b2f | |||
| 8e0fc8d460 | |||
| f547fa732f | |||
| e24b1519a4 | |||
| 3028fe9c87 | |||
| 0b970b05b6 | |||
| f7bfb1e49e | |||
| 371ca009e9 | |||
| 4a0f848551 | |||
| 5e8b7b2a62 | |||
| 0f27b703bd | |||
| 61e19c30cb | |||
| c82bc35202 | |||
| 65934227c3 | |||
| 7519becd43 | |||
| fe83c15bc6 | |||
| 07e6b47fa7 | |||
| b026e1c2f7 | |||
| f8194d9418 | |||
| 1ecd7f274f | |||
| 66bf0ec7af | |||
| 1b22df2b7b | |||
| 9f993f1f67 | |||
| 975518bd88 | |||
| 66a863456c | |||
| 91290c0d25 | |||
| 8a23e89c87 | |||
| 9e9cf85ba1 | |||
| 3dd365bbea | |||
| 33a824b980 | |||
| 8571884304 | |||
| 4f1067e66c | |||
| 7b5b851db0 | |||
| ed0be0cf84 | |||
| d3775e5cb1 | |||
| 2c8f658810 | |||
| 5c52f5f579 | |||
| 0a81bb3fdc | |||
| f33196bc51 | |||
| 6beb90a835 | |||
| 9d45e6acd6 | |||
| 516c464458 | |||
| 6ad3fb16b3 | |||
| 277fdd9b8c | |||
| 7d56993b39 | |||
| 4e1442fcf6 | |||
| 4777bf3e75 | |||
| 14cd37ec56 | |||
| 8bcdfd50c9 | |||
| 6776df8e80 | |||
| fbec079c9b | |||
| 93f6bc3780 | |||
| dde8f23cc3 | |||
| 7cfbd0da95 | |||
| a27ddfaaaf | |||
| b53f616015 | |||
| 22dc175879 | |||
| 5f23e4699c | |||
| a1bd258a7b | |||
| 39a9c54589 | |||
| 5f68370e07 | |||
| dae2de703d | |||
| fa19c40868 | |||
| 1df69d259a | |||
| 90dda0ca68 | |||
| e2d138cac6 | |||
| 15f968d5f8 | |||
| 4a3b68de8f | |||
| f6aec7f763 | |||
| 212b6c3a0f | |||
| 820256d451 | |||
| 3aba538db3 | |||
| 4820cf8cac | |||
| c289effba0 | |||
| 3edccf496a | |||
| 97b4171b3e | |||
| 4a073a7ba5 | |||
| 9f275d57a9 | |||
| d23bbaeb06 | |||
| c64f7a9ec4 | |||
| 214a9df382 | |||
| 90f6620f1e | |||
| f6e8048d9e | |||
| 5afca17d27 | |||
| 2d7f5ae279 | |||
| 458384d658 | |||
| 159b98132d | |||
| 349bb2730a | |||
| c13813348d | |||
| 26e70d6b30 | |||
| f6d3b50b08 | |||
| 5b1fdb7b37 | |||
| c701bf279f | |||
| f8f76f6806 | |||
| c4e7c149a4 | |||
| f91edfabbb | |||
| f410004d45 | |||
| 49e238d580 | |||
| 22713d8f89 |
+22
-7
@@ -1,13 +1,14 @@
|
||||
module.exports = {
|
||||
extends: ["matrix-org"],
|
||||
plugins: [
|
||||
"babel",
|
||||
"matrix-org",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/babel",
|
||||
],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
|
||||
rules: {
|
||||
"no-var": ["warn"],
|
||||
"prefer-rest-params": ["warn"],
|
||||
@@ -32,12 +33,26 @@ module.exports = {
|
||||
"no-console": "error",
|
||||
},
|
||||
overrides: [{
|
||||
"files": ["src/**/*.ts"],
|
||||
"extends": ["matrix-org/ts"],
|
||||
"rules": {
|
||||
// While we're converting to ts we make heavy use of this
|
||||
files: [
|
||||
"**/*.ts",
|
||||
],
|
||||
extends: [
|
||||
"plugin:matrix-org/typescript",
|
||||
],
|
||||
rules: {
|
||||
// TypeScript has its own version of this
|
||||
"@babel/no-invalid-this": "off",
|
||||
|
||||
// We're okay being explicit at the moment
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
// We disable this while we're transitioning
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
// We'd rather not do this but we do
|
||||
"@typescript-eslint/ban-ts-comment": "off",
|
||||
|
||||
"quotes": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<!-- Please read https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst before submitting your pull request -->
|
||||
|
||||
<!-- Include a Sign-Off as described in https://github.com/matrix-org/matrix-js-sdk/blob/develop/CONTRIBUTING.rst#sign-off -->
|
||||
+427
@@ -1,3 +1,430 @@
|
||||
Changes in [12.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.0.1-rc.1) (2021-06-29)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v12.0.0...v12.0.1-rc.1)
|
||||
|
||||
* Fix broken /messages filtering due to internal field changes in
|
||||
FilterComponent
|
||||
[\#1759](https://github.com/matrix-org/matrix-js-sdk/pull/1759)
|
||||
* Convert crypto index to TS
|
||||
[\#1749](https://github.com/matrix-org/matrix-js-sdk/pull/1749)
|
||||
* Fix typescript return types for membership update events
|
||||
[\#1739](https://github.com/matrix-org/matrix-js-sdk/pull/1739)
|
||||
* Fix types of MatrixEvent sender & target
|
||||
[\#1753](https://github.com/matrix-org/matrix-js-sdk/pull/1753)
|
||||
* Add keysharing on invites to File Tree Spaces
|
||||
[\#1744](https://github.com/matrix-org/matrix-js-sdk/pull/1744)
|
||||
* Convert Room and RoomState to Typescript
|
||||
[\#1746](https://github.com/matrix-org/matrix-js-sdk/pull/1746)
|
||||
* Improve type of IContent msgtype
|
||||
[\#1752](https://github.com/matrix-org/matrix-js-sdk/pull/1752)
|
||||
* Add PR template
|
||||
[\#1747](https://github.com/matrix-org/matrix-js-sdk/pull/1747)
|
||||
* Add functions to assist in immutability of Event objects
|
||||
[\#1738](https://github.com/matrix-org/matrix-js-sdk/pull/1738)
|
||||
* Convert Event Context to TS
|
||||
[\#1742](https://github.com/matrix-org/matrix-js-sdk/pull/1742)
|
||||
* Bump lodash from 4.17.20 to 4.17.21
|
||||
[\#1743](https://github.com/matrix-org/matrix-js-sdk/pull/1743)
|
||||
* Add invite retries to file trees
|
||||
[\#1740](https://github.com/matrix-org/matrix-js-sdk/pull/1740)
|
||||
* Convert IndexedDBStore to TS
|
||||
[\#1741](https://github.com/matrix-org/matrix-js-sdk/pull/1741)
|
||||
* Convert additional files to typescript
|
||||
[\#1736](https://github.com/matrix-org/matrix-js-sdk/pull/1736)
|
||||
|
||||
Changes in [12.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.0.0) (2021-06-21)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v12.0.0-rc.1...v12.0.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [12.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v12.0.0-rc.1) (2021-06-15)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.2.0...v12.0.0-rc.1)
|
||||
|
||||
* Rework how disambiguation is handled
|
||||
[\#1730](https://github.com/matrix-org/matrix-js-sdk/pull/1730)
|
||||
* Fix baseToString for n=0 edge case to match inverse stringToBase
|
||||
[\#1735](https://github.com/matrix-org/matrix-js-sdk/pull/1735)
|
||||
* Move various types from the react-sdk to the js-sdk
|
||||
[\#1734](https://github.com/matrix-org/matrix-js-sdk/pull/1734)
|
||||
* Unstable implementation of MSC3089: File Trees
|
||||
[\#1732](https://github.com/matrix-org/matrix-js-sdk/pull/1732)
|
||||
* Add MSC3230 event type to enum
|
||||
[\#1729](https://github.com/matrix-org/matrix-js-sdk/pull/1729)
|
||||
* Add separate reason code for transferred calls
|
||||
[\#1731](https://github.com/matrix-org/matrix-js-sdk/pull/1731)
|
||||
* Use sendonly for call hold
|
||||
[\#1728](https://github.com/matrix-org/matrix-js-sdk/pull/1728)
|
||||
* Stop breeding sync listeners
|
||||
[\#1727](https://github.com/matrix-org/matrix-js-sdk/pull/1727)
|
||||
* Fix semicolons in TS files
|
||||
[\#1724](https://github.com/matrix-org/matrix-js-sdk/pull/1724)
|
||||
* [BREAKING] Convert MatrixClient to TypeScript
|
||||
[\#1718](https://github.com/matrix-org/matrix-js-sdk/pull/1718)
|
||||
* Factor out backup management to a separate module
|
||||
[\#1697](https://github.com/matrix-org/matrix-js-sdk/pull/1697)
|
||||
* Ignore power_levels events with unknown state_key on room-state
|
||||
initialization
|
||||
[\#1723](https://github.com/matrix-org/matrix-js-sdk/pull/1723)
|
||||
* Revert 1579 (Fix extra negotiate message in Firefox)
|
||||
[\#1725](https://github.com/matrix-org/matrix-js-sdk/pull/1725)
|
||||
|
||||
Changes in [11.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.2.0) (2021-06-07)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.2.0-rc.1...v11.2.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [11.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.2.0-rc.1) (2021-06-01)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.1.0...v11.2.0-rc.1)
|
||||
|
||||
* Switch to stable endpoint/fields for MSC2858
|
||||
[\#1720](https://github.com/matrix-org/matrix-js-sdk/pull/1720)
|
||||
* Bump ws from 7.4.2 to 7.4.6
|
||||
[\#1715](https://github.com/matrix-org/matrix-js-sdk/pull/1715)
|
||||
* Make consistent call event type checks
|
||||
[\#1712](https://github.com/matrix-org/matrix-js-sdk/pull/1712)
|
||||
* Apply new Babel linting config
|
||||
[\#1714](https://github.com/matrix-org/matrix-js-sdk/pull/1714)
|
||||
* Bump browserslist from 4.16.1 to 4.16.6
|
||||
[\#1709](https://github.com/matrix-org/matrix-js-sdk/pull/1709)
|
||||
* Add user_busy call hangup reason
|
||||
[\#1713](https://github.com/matrix-org/matrix-js-sdk/pull/1713)
|
||||
* 👕 New linting rules
|
||||
[\#1688](https://github.com/matrix-org/matrix-js-sdk/pull/1688)
|
||||
* Emit relations created when target event added later
|
||||
[\#1710](https://github.com/matrix-org/matrix-js-sdk/pull/1710)
|
||||
* Bump libolm version and update package name.
|
||||
[\#1705](https://github.com/matrix-org/matrix-js-sdk/pull/1705)
|
||||
* Fix uploadContent not rejecting promise when http status code >= 400
|
||||
[\#1703](https://github.com/matrix-org/matrix-js-sdk/pull/1703)
|
||||
* Reduce noise in tests
|
||||
[\#1702](https://github.com/matrix-org/matrix-js-sdk/pull/1702)
|
||||
* Only log once if a Room lacks an m.room.create event
|
||||
[\#1700](https://github.com/matrix-org/matrix-js-sdk/pull/1700)
|
||||
* Cache normalized room name
|
||||
[\#1701](https://github.com/matrix-org/matrix-js-sdk/pull/1701)
|
||||
* Change call event handlers to adapt to undecrypted events
|
||||
[\#1698](https://github.com/matrix-org/matrix-js-sdk/pull/1698)
|
||||
|
||||
Changes in [11.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.1.0) (2021-05-24)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.1.0-rc.1...v11.1.0)
|
||||
|
||||
* [Release] Bump libolm version and update package name
|
||||
[\#1707](https://github.com/matrix-org/matrix-js-sdk/pull/1707)
|
||||
* [Release] Change call event handlers to adapt to undecrypted events
|
||||
[\#1699](https://github.com/matrix-org/matrix-js-sdk/pull/1699)
|
||||
|
||||
Changes in [11.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.1.0-rc.1) (2021-05-19)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.0.0...v11.1.0-rc.1)
|
||||
|
||||
* Fix regressed glare
|
||||
[\#1690](https://github.com/matrix-org/matrix-js-sdk/pull/1690)
|
||||
* Add m.reaction to EventType enum
|
||||
[\#1692](https://github.com/matrix-org/matrix-js-sdk/pull/1692)
|
||||
* Prioritise and reduce the amount of events decrypted on application startup
|
||||
[\#1684](https://github.com/matrix-org/matrix-js-sdk/pull/1684)
|
||||
* Decrypt relations before applying them to target event
|
||||
[\#1696](https://github.com/matrix-org/matrix-js-sdk/pull/1696)
|
||||
* Guard against duplicates in `Relations` model
|
||||
|
||||
Changes in [11.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.0.0) (2021-05-17)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v11.0.0-rc.1...v11.0.0)
|
||||
|
||||
* [Release] Fix regressed glare
|
||||
[\#1695](https://github.com/matrix-org/matrix-js-sdk/pull/1695)
|
||||
|
||||
Changes in [11.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v11.0.0-rc.1) (2021-05-11)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.1.0...v11.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* `MatrixCall` and related APIs have been redesigned to support multiple streams
|
||||
(see [\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660) for more details)
|
||||
|
||||
All changes
|
||||
---
|
||||
|
||||
* Switch from MSC1772 unstable prefixes to stable
|
||||
[\#1679](https://github.com/matrix-org/matrix-js-sdk/pull/1679)
|
||||
* Update the VoIP example to work with the new changes
|
||||
[\#1680](https://github.com/matrix-org/matrix-js-sdk/pull/1680)
|
||||
* Bump hosted-git-info from 2.8.8 to 2.8.9
|
||||
[\#1687](https://github.com/matrix-org/matrix-js-sdk/pull/1687)
|
||||
* Support for multiple streams (not MSC3077)
|
||||
[\#1660](https://github.com/matrix-org/matrix-js-sdk/pull/1660)
|
||||
* Tweak missing m.room.create errors to describe their source
|
||||
[\#1683](https://github.com/matrix-org/matrix-js-sdk/pull/1683)
|
||||
|
||||
Changes in [10.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.1.0) (2021-05-10)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.1.0-rc.1...v10.1.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [10.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.1.0-rc.1) (2021-05-04)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.0.0...v10.1.0-rc.1)
|
||||
|
||||
* Revert "Raise logging dramatically to chase pending event errors"
|
||||
[\#1681](https://github.com/matrix-org/matrix-js-sdk/pull/1681)
|
||||
* Add test coverage collection script
|
||||
[\#1677](https://github.com/matrix-org/matrix-js-sdk/pull/1677)
|
||||
* Raise logging dramatically to chase pending event errors
|
||||
[\#1678](https://github.com/matrix-org/matrix-js-sdk/pull/1678)
|
||||
* Support MSC3086 asserted identity
|
||||
[\#1674](https://github.com/matrix-org/matrix-js-sdk/pull/1674)
|
||||
* Fix `/search` with no results field work again
|
||||
[\#1670](https://github.com/matrix-org/matrix-js-sdk/pull/1670)
|
||||
* Add room.getMembers method
|
||||
[\#1672](https://github.com/matrix-org/matrix-js-sdk/pull/1672)
|
||||
|
||||
Changes in [10.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.0.0) (2021-04-26)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v10.0.0-rc.1...v10.0.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [10.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v10.0.0-rc.1) (2021-04-21)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.11.0...v10.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* The `RoomState.members` event is now only emitted when the room member's power level or the room's normal power level actually changes
|
||||
|
||||
All changes
|
||||
---
|
||||
|
||||
* Restrict event emit for room members that had power levels changed
|
||||
[\#1675](https://github.com/matrix-org/matrix-js-sdk/pull/1675)
|
||||
* Fix sync with misconfigured push rules
|
||||
[\#1669](https://github.com/matrix-org/matrix-js-sdk/pull/1669)
|
||||
* Add missing await
|
||||
[\#1665](https://github.com/matrix-org/matrix-js-sdk/pull/1665)
|
||||
* Migrate to `eslint-plugin-matrix-org`
|
||||
[\#1642](https://github.com/matrix-org/matrix-js-sdk/pull/1642)
|
||||
* Add missing event type enum for key verification done
|
||||
[\#1664](https://github.com/matrix-org/matrix-js-sdk/pull/1664)
|
||||
* Fix timeline jumpiness by setting correct txnId
|
||||
[\#1663](https://github.com/matrix-org/matrix-js-sdk/pull/1663)
|
||||
* Fix calling addEventListener if it does not exist
|
||||
[\#1661](https://github.com/matrix-org/matrix-js-sdk/pull/1661)
|
||||
* Persist unsent messages for subsequent sessions
|
||||
[\#1655](https://github.com/matrix-org/matrix-js-sdk/pull/1655)
|
||||
|
||||
Changes in [9.11.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.11.0) (2021-04-12)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.11.0-rc.1...v9.11.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.11.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.11.0-rc.1) (2021-04-07)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.10.0...v9.11.0-rc.1)
|
||||
|
||||
* Only try to cache private keys we know exist
|
||||
[\#1657](https://github.com/matrix-org/matrix-js-sdk/pull/1657)
|
||||
* Properly terminate screen-share calls if NoUserMedia
|
||||
[\#1654](https://github.com/matrix-org/matrix-js-sdk/pull/1654)
|
||||
* Attended transfer
|
||||
[\#1652](https://github.com/matrix-org/matrix-js-sdk/pull/1652)
|
||||
* Remove catch handlers in private key retrieval
|
||||
[\#1653](https://github.com/matrix-org/matrix-js-sdk/pull/1653)
|
||||
* Fixed the media fail error on caller's side
|
||||
[\#1651](https://github.com/matrix-org/matrix-js-sdk/pull/1651)
|
||||
* Add function to share megolm keys for historical messages, take 2
|
||||
[\#1640](https://github.com/matrix-org/matrix-js-sdk/pull/1640)
|
||||
* Cache cross-signing private keys if needed on bootstrap
|
||||
[\#1649](https://github.com/matrix-org/matrix-js-sdk/pull/1649)
|
||||
|
||||
Changes in [9.10.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.10.0) (2021-03-29)
|
||||
==================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.10.0-rc.1...v9.10.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.10.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.10.0-rc.1) (2021-03-25)
|
||||
============================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.9.0...v9.10.0-rc.1)
|
||||
|
||||
* Don't send m.call.hangup if m.call.invite wasn't sent either
|
||||
[\#1647](https://github.com/matrix-org/matrix-js-sdk/pull/1647)
|
||||
* docs: registerGuest()
|
||||
[\#1641](https://github.com/matrix-org/matrix-js-sdk/pull/1641)
|
||||
* Download device keys in chunks of 250
|
||||
[\#1639](https://github.com/matrix-org/matrix-js-sdk/pull/1639)
|
||||
* More VoIP connectivity fixes
|
||||
[\#1646](https://github.com/matrix-org/matrix-js-sdk/pull/1646)
|
||||
* Make selectDesktopCapturerSource param optional
|
||||
[\#1644](https://github.com/matrix-org/matrix-js-sdk/pull/1644)
|
||||
* Expose APIs needed for reworked cross-signing login flow
|
||||
[\#1632](https://github.com/matrix-org/matrix-js-sdk/pull/1632)
|
||||
|
||||
Changes in [9.9.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.9.0) (2021-03-15)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.9.0-rc.1...v9.9.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.9.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.9.0-rc.1) (2021-03-10)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.8.0...v9.9.0-rc.1)
|
||||
|
||||
* Remove detailed Olm session logging
|
||||
[\#1638](https://github.com/matrix-org/matrix-js-sdk/pull/1638)
|
||||
* Add space summary suggested only param
|
||||
[\#1637](https://github.com/matrix-org/matrix-js-sdk/pull/1637)
|
||||
* Check TURN servers periodically, and at start of calls
|
||||
[\#1634](https://github.com/matrix-org/matrix-js-sdk/pull/1634)
|
||||
* Support sending invite reasons
|
||||
[\#1624](https://github.com/matrix-org/matrix-js-sdk/pull/1624)
|
||||
* Bump elliptic from 6.5.3 to 6.5.4
|
||||
[\#1636](https://github.com/matrix-org/matrix-js-sdk/pull/1636)
|
||||
* Add a function to get a room's MXC URI
|
||||
[\#1635](https://github.com/matrix-org/matrix-js-sdk/pull/1635)
|
||||
* Stop streams if the call has ended
|
||||
[\#1633](https://github.com/matrix-org/matrix-js-sdk/pull/1633)
|
||||
* Remove export keyword from global.d.ts
|
||||
[\#1631](https://github.com/matrix-org/matrix-js-sdk/pull/1631)
|
||||
* Fix IndexedDB store creation example
|
||||
[\#1445](https://github.com/matrix-org/matrix-js-sdk/pull/1445)
|
||||
* An attempt to cleanup how constraints are handled in calls
|
||||
[\#1613](https://github.com/matrix-org/matrix-js-sdk/pull/1613)
|
||||
* Extract display name patterns to constants
|
||||
[\#1628](https://github.com/matrix-org/matrix-js-sdk/pull/1628)
|
||||
* Bump pug-code-gen from 2.0.2 to 2.0.3
|
||||
[\#1630](https://github.com/matrix-org/matrix-js-sdk/pull/1630)
|
||||
* Avoid deadlocks when ensuring Olm sessions for devices
|
||||
[\#1627](https://github.com/matrix-org/matrix-js-sdk/pull/1627)
|
||||
* Filter out edits from other senders in history
|
||||
[\#1626](https://github.com/matrix-org/matrix-js-sdk/pull/1626)
|
||||
* Fix ContentHelpers export
|
||||
[\#1618](https://github.com/matrix-org/matrix-js-sdk/pull/1618)
|
||||
* Add logging to in progress Olm sessions
|
||||
[\#1621](https://github.com/matrix-org/matrix-js-sdk/pull/1621)
|
||||
* Don't ignore ICE candidates received before offer/answer
|
||||
[\#1623](https://github.com/matrix-org/matrix-js-sdk/pull/1623)
|
||||
* Better handling of send failures on VoIP events
|
||||
[\#1622](https://github.com/matrix-org/matrix-js-sdk/pull/1622)
|
||||
* Log when turn creds expire
|
||||
[\#1620](https://github.com/matrix-org/matrix-js-sdk/pull/1620)
|
||||
* Initial Spaces [MSC1772] support
|
||||
[\#1563](https://github.com/matrix-org/matrix-js-sdk/pull/1563)
|
||||
* Add logging to crypto store transactions
|
||||
[\#1617](https://github.com/matrix-org/matrix-js-sdk/pull/1617)
|
||||
* Room helper for getting type and checking if it is a space room
|
||||
[\#1610](https://github.com/matrix-org/matrix-js-sdk/pull/1610)
|
||||
|
||||
Changes in [9.8.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.8.0) (2021-03-01)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.8.0-rc.1...v9.8.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.8.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.8.0-rc.1) (2021-02-24)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.7.0...v9.8.0-rc.1)
|
||||
|
||||
* Optimise prefixed logger
|
||||
[\#1615](https://github.com/matrix-org/matrix-js-sdk/pull/1615)
|
||||
* Add debug logs to encryption prep, take 3
|
||||
[\#1614](https://github.com/matrix-org/matrix-js-sdk/pull/1614)
|
||||
* Add functions for upper & lowercase random strings
|
||||
[\#1612](https://github.com/matrix-org/matrix-js-sdk/pull/1612)
|
||||
* Room helpers for invite permissions and join rules
|
||||
[\#1609](https://github.com/matrix-org/matrix-js-sdk/pull/1609)
|
||||
* Fixed wording in "Adding video track with id" log
|
||||
[\#1606](https://github.com/matrix-org/matrix-js-sdk/pull/1606)
|
||||
* Add more debug logs to encryption prep
|
||||
[\#1605](https://github.com/matrix-org/matrix-js-sdk/pull/1605)
|
||||
* Add option to set ice candidate pool size
|
||||
[\#1604](https://github.com/matrix-org/matrix-js-sdk/pull/1604)
|
||||
* Cancel call if no source was selected
|
||||
[\#1601](https://github.com/matrix-org/matrix-js-sdk/pull/1601)
|
||||
|
||||
Changes in [9.7.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.7.0) (2021-02-16)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.7.0-rc.1...v9.7.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [9.7.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.7.0-rc.1) (2021-02-10)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.6.0...v9.7.0-rc.1)
|
||||
|
||||
* Handle undefined peerconn
|
||||
[\#1600](https://github.com/matrix-org/matrix-js-sdk/pull/1600)
|
||||
* ReEmitter: Don't throw if no error handler is attached
|
||||
[\#1599](https://github.com/matrix-org/matrix-js-sdk/pull/1599)
|
||||
* Convert ReEmitter to TS
|
||||
[\#1598](https://github.com/matrix-org/matrix-js-sdk/pull/1598)
|
||||
* Fix typo in main readme
|
||||
[\#1597](https://github.com/matrix-org/matrix-js-sdk/pull/1597)
|
||||
* Remove rogue plus character
|
||||
[\#1596](https://github.com/matrix-org/matrix-js-sdk/pull/1596)
|
||||
* Fix call ID NaN
|
||||
[\#1595](https://github.com/matrix-org/matrix-js-sdk/pull/1595)
|
||||
* Fix Electron type merging
|
||||
[\#1594](https://github.com/matrix-org/matrix-js-sdk/pull/1594)
|
||||
* Fix browser screen share
|
||||
[\#1593](https://github.com/matrix-org/matrix-js-sdk/pull/1593)
|
||||
* Fix desktop Matrix screen sharing
|
||||
[\#1570](https://github.com/matrix-org/matrix-js-sdk/pull/1570)
|
||||
* Guard against confused server retry times
|
||||
[\#1591](https://github.com/matrix-org/matrix-js-sdk/pull/1591)
|
||||
* Decrypt redaction events
|
||||
[\#1589](https://github.com/matrix-org/matrix-js-sdk/pull/1589)
|
||||
* Fix edge cases with peeking where a room is re-peeked
|
||||
[\#1587](https://github.com/matrix-org/matrix-js-sdk/pull/1587)
|
||||
|
||||
Changes in [9.6.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.6.0) (2021-02-03)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.6.0-rc.1...v9.6.0)
|
||||
|
||||
* [Release] Fix edge cases with peeking where a room is re-peeked
|
||||
[\#1588](https://github.com/matrix-org/matrix-js-sdk/pull/1588)
|
||||
|
||||
Changes in [9.6.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.6.0-rc.1) (2021-01-29)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.5.1...v9.6.0-rc.1)
|
||||
|
||||
* Add support for getting call stats
|
||||
[\#1584](https://github.com/matrix-org/matrix-js-sdk/pull/1584)
|
||||
* Fix compatibility with v0 calls
|
||||
[\#1583](https://github.com/matrix-org/matrix-js-sdk/pull/1583)
|
||||
* Upgrade deps 2021-01
|
||||
[\#1582](https://github.com/matrix-org/matrix-js-sdk/pull/1582)
|
||||
* Log the call ID when logging that we've received VoIP events
|
||||
[\#1581](https://github.com/matrix-org/matrix-js-sdk/pull/1581)
|
||||
* Fix extra negotiate message in Firefox
|
||||
[\#1579](https://github.com/matrix-org/matrix-js-sdk/pull/1579)
|
||||
* Add debug logs to encryption prep
|
||||
[\#1580](https://github.com/matrix-org/matrix-js-sdk/pull/1580)
|
||||
* Expose getPresence endpoint
|
||||
[\#1578](https://github.com/matrix-org/matrix-js-sdk/pull/1578)
|
||||
* Queue keys for backup even if backup isn't enabled yet
|
||||
[\#1577](https://github.com/matrix-org/matrix-js-sdk/pull/1577)
|
||||
* Stop retrying TURN access when forbidden
|
||||
[\#1576](https://github.com/matrix-org/matrix-js-sdk/pull/1576)
|
||||
* Add DTMF sending support
|
||||
[\#1573](https://github.com/matrix-org/matrix-js-sdk/pull/1573)
|
||||
|
||||
Changes in [9.5.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.5.1) (2021-01-26)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.5.0...v9.5.1)
|
||||
|
||||
* [Release] Fix compatibility with v0 calls
|
||||
[\#1585](https://github.com/matrix-org/matrix-js-sdk/pull/1585)
|
||||
|
||||
Changes in [9.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.5.0) (2021-01-18)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.5.0-rc.1...v9.5.0)
|
||||
|
||||
@@ -307,7 +307,7 @@ The SDK supports end-to-end encryption via the Olm and Megolm protocols, using
|
||||
[libolm](https://gitlab.matrix.org/matrix-org/olm). It is left up to the
|
||||
application to make libolm available, via the ``Olm`` global.
|
||||
|
||||
It is also necessry to call ``matrixClient.initCrypto()`` after creating a new
|
||||
It is also necessary to call ``matrixClient.initCrypto()`` after creating a new
|
||||
``MatrixClient`` (but **before** calling ``matrixClient.startClient()``) to
|
||||
initialise the crypto layer.
|
||||
|
||||
|
||||
@@ -31,6 +31,23 @@ function addListeners(call) {
|
||||
call.hangup();
|
||||
disableButtons(false, true, true);
|
||||
});
|
||||
call.on("feeds_changed", function(feeds) {
|
||||
const localFeed = feeds.find((feed) => feed.isLocal());
|
||||
const remoteFeed = feeds.find((feed) => !feed.isLocal());
|
||||
|
||||
const remoteElement = document.getElementById("remote");
|
||||
const localElement = document.getElementById("local");
|
||||
|
||||
if (remoteFeed) {
|
||||
remoteElement.srcObject = remoteFeed.stream;
|
||||
remoteElement.play();
|
||||
}
|
||||
if (localFeed) {
|
||||
localElement.muted = true;
|
||||
localElement.srcObject = localFeed.stream;
|
||||
localElement.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
@@ -62,10 +79,7 @@ function syncComplete() {
|
||||
);
|
||||
console.log("Call => %s", call);
|
||||
addListeners(call);
|
||||
call.placeVideoCall(
|
||||
document.getElementById("remote"),
|
||||
document.getElementById("local")
|
||||
);
|
||||
call.placeVideoCall();
|
||||
document.getElementById("result").innerHTML = "<p>Placed call.</p>";
|
||||
disableButtons(true, true, false);
|
||||
};
|
||||
|
||||
+21
-13
@@ -1,26 +1,34 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<title>VoIP Test</title>
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
<title>VoIP Test</title>
|
||||
<script src="lib/matrix.js"></script>
|
||||
<script src="browserTest.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
You can place and receive calls with this example. Make sure to edit the
|
||||
You can place and receive calls with this example. Make sure to edit the
|
||||
constants in <code>browserTest.js</code> first.
|
||||
<div id="config"></div>
|
||||
<div id="result"></div>
|
||||
<button id="call">Place Call</button>
|
||||
<button id="answer">Answer Call</button>
|
||||
<button id="hangup">Hangup Call</button>
|
||||
<div id="videoBackground">
|
||||
<div id="videoContainer">
|
||||
<video id="remote"></video>
|
||||
</div>
|
||||
</div>
|
||||
<div id="videoBackground">
|
||||
<div id="videoContainer">
|
||||
<video id="local"></video>
|
||||
</div>
|
||||
<div id="videoBackground" class="video-background">
|
||||
<video class="video-element" id="local"></video>
|
||||
<video class="video-element" id="remote"></video>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
<style>
|
||||
.video-background {
|
||||
height: 500px;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.video-element {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
+34
-20
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "9.5.0",
|
||||
"version": "12.0.1-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepublishOnly": "yarn build",
|
||||
@@ -15,10 +15,11 @@
|
||||
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
||||
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 73 src spec",
|
||||
"lint:js": "eslint --max-warnings 57 src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"test": "jest spec/ --coverage --testEnvironment node",
|
||||
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -54,47 +55,60 @@
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"qs": "^6.9.4",
|
||||
"p-retry": "^4.5.0",
|
||||
"qs": "^6.9.6",
|
||||
"request": "^2.88.2",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.12.8",
|
||||
"@babel/core": "^7.12.9",
|
||||
"@babel/cli": "^7.12.10",
|
||||
"@babel/core": "^7.12.10",
|
||||
"@babel/eslint-parser": "^7.12.10",
|
||||
"@babel/eslint-plugin": "^7.12.10",
|
||||
"@babel/plugin-proposal-class-properties": "^7.12.1",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.12.1",
|
||||
"@babel/preset-env": "^7.12.7",
|
||||
"@babel/plugin-transform-runtime": "^7.12.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.1",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@matrix-org/olm": "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "12",
|
||||
"@types/request": "^2.48.5",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
||||
"@typescript-eslint/parser": "^4.17.0",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.3.2",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "7.14.0",
|
||||
"eslint-config-matrix-org": "^0.1.2",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint": "7.18.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main",
|
||||
"exorcist": "^1.0.1",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"jest": "^26.6.3",
|
||||
"jest-localstorage-mock": "^2.4.3",
|
||||
"jest-localstorage-mock": "^2.4.6",
|
||||
"jsdoc": "^3.6.6",
|
||||
"matrix-mock-request": "^1.2.3",
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^5.5.0",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.2",
|
||||
"typescript": "^4.1.2"
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node"
|
||||
"testEnvironment": "node",
|
||||
"testMatch": [
|
||||
"<rootDir>/spec/**/*.spec.{js,ts}"
|
||||
],
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.{js,ts}"
|
||||
],
|
||||
"coverageReporters": [
|
||||
"text"
|
||||
]
|
||||
},
|
||||
"typings": "./lib/index.d.ts"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export class MockBlob {
|
||||
private contents: number[] = [];
|
||||
|
||||
public constructor(private parts: ArrayLike<number>[]) {
|
||||
parts.forEach(p => Array.from(p).forEach(e => this.contents.push(e)));
|
||||
}
|
||||
|
||||
public get size(): number {
|
||||
return this.contents.length;
|
||||
}
|
||||
}
|
||||
+12
-15
@@ -20,12 +20,12 @@ limitations under the License.
|
||||
import './olm-loader';
|
||||
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
import {LocalStorageCryptoStore} from '../src/crypto/store/localStorage-crypto-store';
|
||||
import {logger} from '../src/logger';
|
||||
import {WebStorageSessionStore} from "../src/store/session/webstorage";
|
||||
import {syncPromise} from "./test-utils";
|
||||
import {createClient} from "../src/matrix";
|
||||
import {MockStorageApi} from "./MockStorageApi";
|
||||
import { LocalStorageCryptoStore } from '../src/crypto/store/localStorage-crypto-store';
|
||||
import { logger } from '../src/logger';
|
||||
import { WebStorageSessionStore } from "../src/store/session/webstorage";
|
||||
import { syncPromise } from "./test-utils";
|
||||
import { createClient } from "../src/matrix";
|
||||
import { MockStorageApi } from "./MockStorageApi";
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
@@ -69,7 +69,7 @@ export function TestClient(
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = {};
|
||||
this._callEventHandler = {
|
||||
this.callEventHandler = {
|
||||
calls: new Map(),
|
||||
};
|
||||
}
|
||||
@@ -129,11 +129,10 @@ TestClient.prototype.expectDeviceKeyUpload = function() {
|
||||
expect(Object.keys(self.oneTimeKeys).length).toEqual(0);
|
||||
|
||||
self.deviceKeys = content.device_keys;
|
||||
return {one_time_key_counts: {signed_curve25519: 0}};
|
||||
return { one_time_key_counts: { signed_curve25519: 0 } };
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* If one-time keys have already been uploaded, return them. Otherwise,
|
||||
* set up an expectation that the keys will be uploaded, and wait for
|
||||
@@ -151,9 +150,9 @@ TestClient.prototype.awaitOneTimeKeyUpload = function() {
|
||||
.respond(200, (path, content) => {
|
||||
expect(content.device_keys).toBe(undefined);
|
||||
expect(content.one_time_keys).toBe(undefined);
|
||||
return {one_time_key_counts: {
|
||||
return { one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
}};
|
||||
} };
|
||||
});
|
||||
|
||||
this.httpBackend.when("POST", "/keys/upload")
|
||||
@@ -164,9 +163,9 @@ TestClient.prototype.awaitOneTimeKeyUpload = function() {
|
||||
logger.log('%s: received %i one-time keys', this,
|
||||
Object.keys(content.one_time_keys).length);
|
||||
this.oneTimeKeys = content.one_time_keys;
|
||||
return {one_time_key_counts: {
|
||||
return { one_time_key_counts: {
|
||||
signed_curve25519: Object.keys(this.oneTimeKeys).length,
|
||||
}};
|
||||
} };
|
||||
});
|
||||
|
||||
// this can take ages
|
||||
@@ -197,7 +196,6 @@ TestClient.prototype.expectKeyQuery = function(response) {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* get the uploaded curve25519 device key
|
||||
*
|
||||
@@ -208,7 +206,6 @@ TestClient.prototype.getDeviceKey = function() {
|
||||
return this.deviceKeys.keys[keyId];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* get the uploaded ed25519 device key
|
||||
*
|
||||
|
||||
@@ -17,10 +17,10 @@ limitations under the License.
|
||||
// load XmlHttpRequest mock
|
||||
import "./setupTests";
|
||||
import "../../dist/browser-matrix"; // uses browser-matrix instead of the src
|
||||
import {MockStorageApi} from "../MockStorageApi";
|
||||
import {WebStorageSessionStore} from "../../src/store/session/webstorage";
|
||||
import { MockStorageApi } from "../MockStorageApi";
|
||||
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
import {LocalStorageCryptoStore} from "../../src/crypto/store/localStorage-crypto-store";
|
||||
import { LocalStorageCryptoStore } from "../../src/crypto/store/localStorage-crypto-store";
|
||||
import * as utils from "../test-utils";
|
||||
|
||||
const USER_ID = "@user:test.server";
|
||||
@@ -58,7 +58,7 @@ describe("Browserify Test", function() {
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
({client, httpBackend} = await createTestClient());
|
||||
({ client, httpBackend } = await createTestClient());
|
||||
await client.startClient();
|
||||
});
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {TestClient} from '../TestClient';
|
||||
import { TestClient } from '../TestClient';
|
||||
import * as testUtils from '../test-utils';
|
||||
import {logger} from '../../src/logger';
|
||||
import { logger } from '../../src/logger';
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -67,7 +67,6 @@ function getSyncResponse(roomMembers) {
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
|
||||
describe("DeviceList management:", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('not running deviceList tests: Olm not present');
|
||||
@@ -98,7 +97,7 @@ describe("DeviceList management:", function() {
|
||||
});
|
||||
|
||||
it("Alice shouldn't do a second /query for non-e2e-capable devices", function() {
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(function() {
|
||||
const syncResponse = getSyncResponse(['@bob:xyz']);
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(200, syncResponse);
|
||||
@@ -137,11 +136,10 @@ describe("DeviceList management:", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("We should not get confused by out-of-order device query responses",
|
||||
() => {
|
||||
// https://github.com/vector-im/element-web/issues/3126
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse(['@bob:xyz', '@chris:abc']));
|
||||
@@ -160,14 +158,14 @@ describe("DeviceList management:", function() {
|
||||
);
|
||||
|
||||
aliceTestClient.httpBackend.when('PUT', '/send/').respond(
|
||||
200, {event_id: '$event1'});
|
||||
200, { event_id: '$event1' });
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
aliceTestClient.httpBackend.flush('/keys/query', 1).then(
|
||||
() => aliceTestClient.httpBackend.flush('/send/', 1),
|
||||
),
|
||||
aliceTestClient.client._crypto._deviceList.saveIfDirty(),
|
||||
aliceTestClient.client.crypto.deviceList.saveIfDirty(),
|
||||
]);
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
@@ -199,12 +197,12 @@ describe("DeviceList management:", function() {
|
||||
},
|
||||
token: '3',
|
||||
}).respond(200, {
|
||||
device_keys: {'@chris:abc': {}},
|
||||
device_keys: { '@chris:abc': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
expect(flushed).toEqual(0);
|
||||
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
@@ -228,7 +226,7 @@ describe("DeviceList management:", function() {
|
||||
},
|
||||
token: '2',
|
||||
}).respond(200, {
|
||||
device_keys: {'@bob:xyz': {}},
|
||||
device_keys: { '@bob:xyz': {} },
|
||||
});
|
||||
return aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
}).then((flushed) => {
|
||||
@@ -237,7 +235,7 @@ describe("DeviceList management:", function() {
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@bob:xyz']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
@@ -258,7 +256,7 @@ describe("DeviceList management:", function() {
|
||||
// wait for the client to stop processing the response
|
||||
return aliceTestClient.client.downloadKeys(['@chris:abc']);
|
||||
}).then(() => {
|
||||
return aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
return aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
@@ -288,7 +286,7 @@ describe("DeviceList management:", function() {
|
||||
},
|
||||
);
|
||||
await aliceTestClient.httpBackend.flush('/keys/query', 1);
|
||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
@@ -323,9 +321,8 @@ describe("DeviceList management:", function() {
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
@@ -361,7 +358,7 @@ describe("DeviceList management:", function() {
|
||||
);
|
||||
|
||||
await aliceTestClient.flushSync();
|
||||
await aliceTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
await aliceTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
|
||||
aliceTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
@@ -382,7 +379,7 @@ describe("DeviceList management:", function() {
|
||||
anotherTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
200, getSyncResponse([]));
|
||||
await anotherTestClient.flushSync();
|
||||
await anotherTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
await anotherTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
|
||||
anotherTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const bobStat = data.trackingStatus['@bob:xyz'];
|
||||
|
||||
@@ -28,11 +28,10 @@ limitations under the License.
|
||||
// load olm before the sdk if possible
|
||||
import '../olm-loader';
|
||||
|
||||
import {logger} from '../../src/logger';
|
||||
import { logger } from '../../src/logger';
|
||||
import * as testUtils from "../test-utils";
|
||||
import * as utils from "../../src/utils";
|
||||
import {TestClient} from "../TestClient";
|
||||
import {CRYPTO_ENABLED} from "../../src/client";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
|
||||
let aliTestClient;
|
||||
const roomId = "!room:localhost";
|
||||
@@ -76,7 +75,7 @@ function expectAliQueryKeys() {
|
||||
);
|
||||
const result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
return { device_keys: result };
|
||||
});
|
||||
return aliTestClient.httpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
@@ -104,7 +103,7 @@ function expectBobQueryKeys() {
|
||||
);
|
||||
const result = {};
|
||||
result[aliUserId] = aliKeys;
|
||||
return {device_keys: result};
|
||||
return { device_keys: result };
|
||||
});
|
||||
return bobTestClient.httpBackend.flush("/keys/query", 1);
|
||||
}
|
||||
@@ -133,7 +132,7 @@ function expectAliClaimKeys() {
|
||||
result[bobUserId] = {};
|
||||
result[bobUserId][bobDeviceId] = {};
|
||||
result[bobUserId][bobDeviceId][keyId] = keys[keyId];
|
||||
return {one_time_keys: result};
|
||||
return { one_time_keys: result };
|
||||
});
|
||||
}).then(() => {
|
||||
// it can take a while to process the key query, so give it some extra
|
||||
@@ -145,7 +144,6 @@ function expectAliClaimKeys() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function aliDownloadsKeys() {
|
||||
// can't query keys before bob has uploaded them
|
||||
expect(bobTestClient.getSigningKey()).toBeTruthy();
|
||||
@@ -161,7 +159,7 @@ function aliDownloadsKeys() {
|
||||
// check that the localStorage is updated as we expect (not sure this is
|
||||
// an integration test, but meh)
|
||||
return Promise.all([p1, p2]).then(() => {
|
||||
return aliTestClient.client._crypto._deviceList.saveIfDirty();
|
||||
return aliTestClient.client.crypto.deviceList.saveIfDirty();
|
||||
}).then(() => {
|
||||
aliTestClient.cryptoStore.getEndToEndDeviceData(null, (data) => {
|
||||
const devices = data.devices[bobUserId];
|
||||
@@ -244,7 +242,7 @@ function bobSendsReplyMessage() {
|
||||
function expectAliSendMessageRequest() {
|
||||
return expectSendMessageRequest(aliTestClient.httpBackend).then(function(content) {
|
||||
aliMessages.push(content);
|
||||
expect(utils.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
|
||||
expect(Object.keys(content.ciphertext)).toEqual([bobTestClient.getDeviceKey()]);
|
||||
const ciphertext = content.ciphertext[bobTestClient.getDeviceKey()];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
@@ -261,7 +259,7 @@ function expectBobSendMessageRequest() {
|
||||
bobMessages.push(content);
|
||||
const aliKeyId = "curve25519:" + aliDeviceId;
|
||||
const aliDeviceCurve25519Key = aliTestClient.deviceKeys.keys[aliKeyId];
|
||||
expect(utils.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
|
||||
expect(Object.keys(content.ciphertext)).toEqual([aliDeviceCurve25519Key]);
|
||||
const ciphertext = content.ciphertext[aliDeviceCurve25519Key];
|
||||
expect(ciphertext).toBeTruthy();
|
||||
return ciphertext;
|
||||
@@ -270,7 +268,7 @@ function expectBobSendMessageRequest() {
|
||||
|
||||
function sendMessage(client) {
|
||||
return client.sendMessage(
|
||||
roomId, {msgtype: "m.text", body: "Hello, World"},
|
||||
roomId, { msgtype: "m.text", body: "Hello, World" },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -358,7 +356,6 @@ function recvMessage(httpBackend, client, sender, message) {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Send an initial sync response to the client (which just includes the member
|
||||
* list for our test room).
|
||||
@@ -396,7 +393,6 @@ function firstSync(testClient) {
|
||||
return testClient.flushSync();
|
||||
}
|
||||
|
||||
|
||||
describe("MatrixClient crypto", function() {
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
@@ -478,7 +474,7 @@ describe("MatrixClient crypto", function() {
|
||||
).respond(200, function(path, content) {
|
||||
const result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
return { device_keys: result };
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
@@ -520,7 +516,7 @@ describe("MatrixClient crypto", function() {
|
||||
).respond(200, function(path, content) {
|
||||
const result = {};
|
||||
result[bobUserId] = bobKeys;
|
||||
return {device_keys: result};
|
||||
return { device_keys: result };
|
||||
});
|
||||
|
||||
return Promise.all([
|
||||
@@ -534,7 +530,6 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("Bob starts his client and uploads device keys and one-time keys", function() {
|
||||
return Promise.resolve()
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -546,7 +541,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Ali sends a message", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -556,7 +551,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob receives a message", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -567,7 +562,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob receives a message with a bogus sender", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -621,7 +616,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Ali blocks Bob's device", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -641,7 +636,7 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob receives two pre-key messages", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -654,8 +649,8 @@ describe("MatrixClient crypto", function() {
|
||||
});
|
||||
|
||||
it("Bob replies to the message", function() {
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
bobTestClient.expectKeyQuery({device_keys: {[bobUserId]: {}}});
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
bobTestClient.expectKeyQuery({ device_keys: { [bobUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => bobTestClient.start())
|
||||
@@ -673,7 +668,7 @@ describe("MatrixClient crypto", function() {
|
||||
it("Ali does a key query when encryption is enabled", function() {
|
||||
// enabling encryption in the room should make alice download devices
|
||||
// for both members.
|
||||
aliTestClient.expectKeyQuery({device_keys: {[aliUserId]: {}}});
|
||||
aliTestClient.expectKeyQuery({ device_keys: { [aliUserId]: {} } });
|
||||
return Promise.resolve()
|
||||
.then(() => aliTestClient.start())
|
||||
.then(() => firstSync(aliTestClient))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {TestClient} from "../TestClient";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient events", function() {
|
||||
let client;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {EventTimeline} from "../../src/matrix";
|
||||
import {logger} from "../../src/logger";
|
||||
import {TestClient} from "../TestClient";
|
||||
import { EventTimeline } from "../../src/matrix";
|
||||
import { logger } from "../../src/logger";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
@@ -127,7 +127,7 @@ describe("getEventTimeline support", function() {
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{timelineSupport: true},
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
@@ -141,7 +141,6 @@ describe("getEventTimeline support", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("scrollback should be able to scroll back to before a gappy /sync",
|
||||
function() {
|
||||
// need a client with timelineSupport disabled to make this work
|
||||
@@ -218,7 +217,7 @@ describe("MatrixClient event timelines", function() {
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{timelineSupport: true},
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
@@ -516,7 +515,7 @@ describe("MatrixClient event timelines", function() {
|
||||
client.getEventTimeline(timelineSet, EVENTS[0].event_id,
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(tl, {backwards: true});
|
||||
return client.paginateEventTimeline(tl, { backwards: true });
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
@@ -532,7 +531,6 @@ describe("MatrixClient event timelines", function() {
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
it("should allow you to paginate forwards", function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
@@ -569,7 +567,7 @@ describe("MatrixClient event timelines", function() {
|
||||
).then(function(tl0) {
|
||||
tl = tl0;
|
||||
return client.paginateEventTimeline(
|
||||
tl, {backwards: false, limit: 20});
|
||||
tl, { backwards: false, limit: 20 });
|
||||
}).then(function(success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl.getEvents().length).toEqual(3);
|
||||
@@ -591,7 +589,7 @@ describe("MatrixClient event timelines", function() {
|
||||
const event = utils.mkMessage({
|
||||
room: roomId, user: userId, msg: "a body",
|
||||
});
|
||||
event.unsigned = {transaction_id: TXN_ID};
|
||||
event.unsigned = { transaction_id: TXN_ID };
|
||||
|
||||
beforeEach(function() {
|
||||
// set up handlers for both the message send, and the
|
||||
@@ -680,7 +678,6 @@ describe("MatrixClient event timelines", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should handle gappy syncs after redactions", function() {
|
||||
// https://github.com/vector-im/vector-web/issues/1389
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {CRYPTO_ENABLED} from "../../src/client";
|
||||
import {Filter, MemoryStore, Room} from "../../src/matrix";
|
||||
import {TestClient} from "../TestClient";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { Filter, MemoryStore, Room } from "../../src/matrix";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
let client = null;
|
||||
@@ -285,7 +285,6 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("downloadKeys", function() {
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
@@ -337,7 +336,7 @@ describe("MatrixClient", function() {
|
||||
var b = JSON.parse(JSON.stringify(o));
|
||||
delete(b.signatures);
|
||||
delete(b.unsigned);
|
||||
return client._crypto._olmDevice.sign(anotherjson.stringify(b));
|
||||
return client.crypto.olmDevice.sign(anotherjson.stringify(b));
|
||||
};
|
||||
|
||||
logger.log("Ed25519: " + ed25519key);
|
||||
@@ -346,10 +345,10 @@ describe("MatrixClient", function() {
|
||||
*/
|
||||
|
||||
httpBackend.when("POST", "/keys/query").check(function(req) {
|
||||
expect(req.data).toEqual({device_keys: {
|
||||
expect(req.data).toEqual({ device_keys: {
|
||||
'boris': [],
|
||||
'chaz': [],
|
||||
}});
|
||||
} });
|
||||
}).respond(200, {
|
||||
device_keys: {
|
||||
boris: borisKeys,
|
||||
@@ -379,12 +378,12 @@ describe("MatrixClient", function() {
|
||||
});
|
||||
|
||||
describe("deleteDevice", function() {
|
||||
const auth = {a: 1};
|
||||
const auth = { a: 1 };
|
||||
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});
|
||||
expect(req.data).toEqual({ auth: auth });
|
||||
}).respond(200);
|
||||
|
||||
const prom = client.deleteDevice("my_device", auth);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import * as utils from "../test-utils";
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
import {MatrixClient} from "../../src/matrix";
|
||||
import {MatrixScheduler} from "../../src/scheduler";
|
||||
import {MemoryStore} from "../../src/store/memory";
|
||||
import {MatrixError} from "../../src/http-api";
|
||||
import { MatrixClient } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { MemoryStore } from "../../src/store/memory";
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
|
||||
describe("MatrixClient opts", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {EventStatus} from "../../src/matrix";
|
||||
import {MatrixScheduler} from "../../src/scheduler";
|
||||
import {Room} from "../../src/models/room";
|
||||
import {TestClient} from "../TestClient";
|
||||
import { EventStatus } from "../../src/matrix";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient retrying", function() {
|
||||
let client = null;
|
||||
@@ -19,7 +19,7 @@ describe("MatrixClient retrying", function() {
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{scheduler},
|
||||
{ scheduler },
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {EventStatus} from "../../src/models/event";
|
||||
import {TestClient} from "../TestClient";
|
||||
|
||||
import { EventStatus } from "../../src/models/event";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient room timelines", function() {
|
||||
let client = null;
|
||||
@@ -104,7 +103,7 @@ describe("MatrixClient room timelines", function() {
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{timelineSupport: true},
|
||||
{ timelineSupport: true },
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
@@ -166,7 +165,7 @@ describe("MatrixClient room timelines", function() {
|
||||
body: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = {transaction_id: "txn1"};
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
@@ -198,7 +197,7 @@ describe("MatrixClient room timelines", function() {
|
||||
body: "I am a fish", user: userId, room: roomId,
|
||||
});
|
||||
ev.event_id = eventId;
|
||||
ev.unsigned = {transaction_id: "txn1"};
|
||||
ev.unsigned = { transaction_id: "txn1" };
|
||||
setNextSyncData([ev]);
|
||||
|
||||
client.on("sync", function(state) {
|
||||
@@ -396,8 +395,8 @@ describe("MatrixClient room timelines", function() {
|
||||
describe("new events", function() {
|
||||
it("should be added to the right place in the timeline", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
|
||||
@@ -434,11 +433,11 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
it("should set the right event.sender values", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
utils.mkMembership({
|
||||
user: userId, room: roomId, mship: "join", name: "New Name",
|
||||
}),
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
eventData[1].__prev_event = USER_MEMBERSHIP_EVENT;
|
||||
setNextSyncData(eventData);
|
||||
@@ -546,7 +545,7 @@ describe("MatrixClient room timelines", function() {
|
||||
describe("gappy sync", function() {
|
||||
it("should copy the last known state to the new timeline", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
@@ -579,7 +578,7 @@ describe("MatrixClient room timelines", function() {
|
||||
|
||||
it("should emit a 'Room.timelineReset' event", function() {
|
||||
const eventData = [
|
||||
utils.mkMessage({user: userId, room: roomId}),
|
||||
utils.mkMessage({ user: userId, room: roomId }),
|
||||
];
|
||||
setNextSyncData(eventData);
|
||||
NEXT_SYNC_DATA.rooms.join[roomId].timeline.limited = true;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {MatrixEvent} from "../../src/models/event";
|
||||
import {EventTimeline} from "../../src/models/event-timeline";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import * as utils from "../test-utils";
|
||||
import {TestClient} from "../TestClient";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("MatrixClient syncing", function() {
|
||||
let client = null;
|
||||
@@ -122,7 +122,6 @@ describe("MatrixClient syncing", function() {
|
||||
resolveInvitesToProfiles: true,
|
||||
});
|
||||
|
||||
|
||||
return Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
awaitSyncEvent(),
|
||||
@@ -677,8 +676,8 @@ describe("MatrixClient syncing", function() {
|
||||
it("should create and use an appropriate filter", function() {
|
||||
httpBackend.when("POST", "/filter").check(function(req) {
|
||||
expect(req.data).toEqual({
|
||||
room: { timeline: {limit: 1},
|
||||
include_leave: true }});
|
||||
room: { timeline: { limit: 1 },
|
||||
include_leave: true } });
|
||||
}).respond(200, { filter_id: "another_id" });
|
||||
|
||||
const prom = new Promise((resolve) => {
|
||||
|
||||
@@ -16,10 +16,9 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import anotherjson from "another-json";
|
||||
import * as utils from "../../src/utils";
|
||||
import * as testUtils from "../test-utils";
|
||||
import {TestClient} from "../TestClient";
|
||||
import {logger} from "../../src/logger";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { logger } from "../../src/logger";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -32,7 +31,7 @@ const ROOM_ID = "!room:id";
|
||||
*/
|
||||
function createOlmSession(olmAccount, recipientTestClient) {
|
||||
return recipientTestClient.awaitOneTimeKeyUpload().then((keys) => {
|
||||
const otkId = utils.keys(keys)[0];
|
||||
const otkId = Object.keys(keys)[0];
|
||||
const otk = keys[otkId];
|
||||
|
||||
const session = new global.Olm.Session();
|
||||
@@ -197,7 +196,6 @@ function getSyncResponse(roomMembers) {
|
||||
return syncResponse;
|
||||
}
|
||||
|
||||
|
||||
describe("megolm", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('not running megolm tests: Olm not present');
|
||||
@@ -257,7 +255,7 @@ describe("megolm", function() {
|
||||
const testOneTimeKeys = JSON.parse(testOlmAccount.one_time_keys());
|
||||
testOlmAccount.mark_keys_as_published();
|
||||
|
||||
const keyId = utils.keys(testOneTimeKeys.curve25519)[0];
|
||||
const keyId = Object.keys(testOneTimeKeys.curve25519)[0];
|
||||
const oneTimeKey = testOneTimeKeys.curve25519[keyId];
|
||||
const keyResult = {
|
||||
'key': oneTimeKey,
|
||||
@@ -269,7 +267,7 @@ describe("megolm", function() {
|
||||
'ed25519:DEVICE_ID': sig,
|
||||
};
|
||||
|
||||
const claimResponse = {one_time_keys: {}};
|
||||
const claimResponse = { one_time_keys: {} };
|
||||
claimResponse.one_time_keys[userId] = {
|
||||
'DEVICE_ID': {},
|
||||
};
|
||||
@@ -484,8 +482,9 @@ describe("megolm", function() {
|
||||
return aliceTestClient.flushSync().then(() => {
|
||||
return aliceTestClient.flushSync();
|
||||
});
|
||||
}).then(function() {
|
||||
}).then(async function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
await room.decryptCriticalEvents();
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
@@ -494,7 +493,7 @@ describe("megolm", function() {
|
||||
it('Alice sends a megolm message', function() {
|
||||
let p2pSession;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -577,7 +576,7 @@ describe("megolm", function() {
|
||||
});
|
||||
|
||||
it("We shouldn't attempt to send to blocked devices", function() {
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -634,7 +633,7 @@ describe("megolm", function() {
|
||||
let p2pSession;
|
||||
let megolmSessionId;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -841,13 +840,12 @@ describe("megolm", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('Alice should wait for device list to complete when sending a megolm message',
|
||||
function() {
|
||||
let downloadPromise;
|
||||
let sendPromise;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -887,11 +885,10 @@ describe("megolm", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("Alice exports megolm keys and imports them to a new device", function() {
|
||||
let messageEncrypted;
|
||||
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { '@alice:localhost': {} } });
|
||||
return aliceTestClient.start().then(() => {
|
||||
// establish an olm session with alice
|
||||
return createOlmSession(testOlmAccount, aliceTestClient);
|
||||
@@ -933,8 +930,9 @@ describe("megolm", function() {
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, syncResponse);
|
||||
return aliceTestClient.flushSync();
|
||||
}).then(function() {
|
||||
}).then(async function() {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID);
|
||||
await room.decryptCriticalEvents();
|
||||
const event = room.getLiveTimeline().getEvents()[0];
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
|
||||
@@ -1000,7 +998,7 @@ describe("megolm", function() {
|
||||
...rawEvent,
|
||||
room: ROOM_ID,
|
||||
});
|
||||
return event.attemptDecryption(testClient.client._crypto, true).then(() => {
|
||||
return event.attemptDecryption(testClient.client.crypto, true).then(() => {
|
||||
expect(event.isKeySourceUntrusted()).toBeTruthy();
|
||||
});
|
||||
}).then(() => {
|
||||
@@ -1014,15 +1012,15 @@ describe("megolm", function() {
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
event._senderCurve25519Key = testSenderKey;
|
||||
return testClient.client._crypto._onRoomKeyEvent(event);
|
||||
event.senderCurve25519Key = testSenderKey;
|
||||
return testClient.client.crypto.onRoomKeyEvent(event);
|
||||
}).then(() => {
|
||||
const event = testUtils.mkEvent({
|
||||
event: true,
|
||||
...rawEvent,
|
||||
room: ROOM_ID,
|
||||
});
|
||||
return event.attemptDecryption(testClient.client._crypto, true).then(() => {
|
||||
return event.attemptDecryption(testClient.client.crypto, true).then(() => {
|
||||
expect(event.isKeySourceUntrusted()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
+2
-2
@@ -15,12 +15,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from '../src/logger';
|
||||
import { logger } from '../src/logger';
|
||||
import * as utils from "../src/utils";
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
global.Olm = require('@matrix-org/olm');
|
||||
logger.log('loaded libolm');
|
||||
} catch (e) {
|
||||
logger.warn("unable to run crypto tests: libolm not available");
|
||||
|
||||
+21
-21
@@ -1,8 +1,8 @@
|
||||
// load olm before the sdk if possible
|
||||
import './olm-loader';
|
||||
|
||||
import {logger} from '../src/logger';
|
||||
import {MatrixEvent} from "../src/models/event";
|
||||
import { logger } from '../src/logger';
|
||||
import { MatrixEvent } from "../src/models/event";
|
||||
|
||||
/**
|
||||
* Return a promise that is resolved when the client next emits a
|
||||
@@ -51,7 +51,7 @@ export function mock(constr, name) {
|
||||
result.toString = function() {
|
||||
return "mock" + (name ? " of " + name : "");
|
||||
};
|
||||
for (const key in constr.prototype) { // eslint-disable-line guard-for-in
|
||||
for (const key of Object.getOwnPropertyNames(constr.prototype)) { // eslint-disable-line guard-for-in
|
||||
try {
|
||||
if (constr.prototype[key] instanceof Function) {
|
||||
result[key] = jest.fn();
|
||||
@@ -177,7 +177,6 @@ export function mkMessage(opts) {
|
||||
return mkEvent(opts);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A mock implementation of webstorage
|
||||
*
|
||||
@@ -204,7 +203,6 @@ MockStorageApi.prototype = {
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* If an event is being decrypted, wait for it to finish being decrypted.
|
||||
*
|
||||
@@ -212,21 +210,23 @@ MockStorageApi.prototype = {
|
||||
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
|
||||
*/
|
||||
export function awaitDecryption(event) {
|
||||
if (!event.isBeingDecrypted()) {
|
||||
return Promise.resolve(event);
|
||||
}
|
||||
// An event is not always decrypted ahead of time
|
||||
// getClearContent is a good signal to know whether an event has been decrypted
|
||||
// already
|
||||
if (event.getClearContent() !== null) {
|
||||
return event;
|
||||
} else {
|
||||
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
||||
|
||||
logger.log(`${Date.now()} event ${event.getId()} is being decrypted; waiting`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
return new Promise((resolve, reject) => {
|
||||
event.once('Event.decrypted', (ev) => {
|
||||
logger.log(`${Date.now()} event ${event.getId()} now decrypted`);
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function HttpResponse(
|
||||
httpLookups, acceptKeepalives, ignoreUnhandledSync,
|
||||
) {
|
||||
@@ -357,12 +357,12 @@ export function setHttpResponses(
|
||||
);
|
||||
|
||||
const httpReq = httpResponseObj.request.bind(httpResponseObj);
|
||||
client._http = [
|
||||
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);
|
||||
client.http.authedRequest.mockImplementation(httpReq);
|
||||
client.http.authedRequestWithPrefix.mockImplementation(httpReq);
|
||||
client.http.requestWithPrefix.mockImplementation(httpReq);
|
||||
client.http.request.mockImplementation(httpReq);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
Copyright 2021 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 { NamespacedValue, UnstableValue } from "../../src/NamespacedValue";
|
||||
|
||||
describe("NamespacedValue", () => {
|
||||
it("should prefer stable over unstable", () => {
|
||||
const ns = new NamespacedValue("stable", "unstable");
|
||||
expect(ns.name).toBe(ns.stable);
|
||||
expect(ns.altName).toBe(ns.unstable);
|
||||
});
|
||||
|
||||
it("should return unstable if there is no stable", () => {
|
||||
const ns = new NamespacedValue(null, "unstable");
|
||||
expect(ns.name).toBe(ns.unstable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should have a falsey unstable if needed", () => {
|
||||
const ns = new NamespacedValue("stable", null);
|
||||
expect(ns.name).toBe(ns.stable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should match against either stable or unstable", () => {
|
||||
const ns = new NamespacedValue("stable", "unstable");
|
||||
expect(ns.matches("no")).toBe(false);
|
||||
expect(ns.matches(ns.stable)).toBe(true);
|
||||
expect(ns.matches(ns.unstable)).toBe(true);
|
||||
});
|
||||
|
||||
it("should not permit falsey values for both parts", () => {
|
||||
try {
|
||||
new UnstableValue(null, null);
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("One of stable or unstable values must be supplied");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("UnstableValue", () => {
|
||||
it("should prefer unstable over stable", () => {
|
||||
const ns = new UnstableValue("stable", "unstable");
|
||||
expect(ns.name).toBe(ns.unstable);
|
||||
expect(ns.altName).toBe(ns.stable);
|
||||
});
|
||||
|
||||
it("should return unstable if there is no stable", () => {
|
||||
const ns = new UnstableValue(null, "unstable");
|
||||
expect(ns.name).toBe(ns.unstable);
|
||||
expect(ns.altName).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not permit falsey unstable values", () => {
|
||||
try {
|
||||
new UnstableValue("stable", null);
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.message).toBe("Unstable value must be supplied");
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2021 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 { ReEmitter } from "../../src/ReEmitter";
|
||||
|
||||
const EVENTNAME = "UnknownEntry";
|
||||
|
||||
class EventSource extends EventEmitter {
|
||||
doTheThing() {
|
||||
this.emit(EVENTNAME, "foo", "bar");
|
||||
}
|
||||
|
||||
doAnError() {
|
||||
this.emit('error');
|
||||
}
|
||||
}
|
||||
|
||||
class EventTarget extends EventEmitter {
|
||||
|
||||
}
|
||||
|
||||
describe("ReEmitter", function() {
|
||||
it("Re-Emits events with the same args", function() {
|
||||
const src = new EventSource();
|
||||
const tgt = new EventTarget();
|
||||
|
||||
const handler = jest.fn();
|
||||
tgt.on(EVENTNAME, handler);
|
||||
|
||||
const reEmitter = new ReEmitter(tgt);
|
||||
reEmitter.reEmit(src, [EVENTNAME]);
|
||||
|
||||
src.doTheThing();
|
||||
|
||||
// Args should be the args passed to 'emit' after the event name, and
|
||||
// also the source object of the event which re-emitter adds
|
||||
expect(handler).toHaveBeenCalledWith("foo", "bar", src);
|
||||
});
|
||||
|
||||
it("Doesn't throw if no handler for 'error' event", function() {
|
||||
const src = new EventSource();
|
||||
const tgt = new EventTarget();
|
||||
|
||||
const reEmitter = new ReEmitter(tgt);
|
||||
reEmitter.reEmit(src, ['error']);
|
||||
|
||||
// without the workaround in ReEmitter, this would throw
|
||||
src.doAnError();
|
||||
|
||||
const handler = jest.fn();
|
||||
tgt.on('error', handler);
|
||||
|
||||
src.doAnError();
|
||||
|
||||
// Now we've attached an error handler, it should be called
|
||||
expect(handler).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
import * as sdk from "../../src";
|
||||
import {AutoDiscovery} from "../../src/autodiscovery";
|
||||
import { AutoDiscovery } from "../../src/autodiscovery";
|
||||
|
||||
describe("AutoDiscovery", function() {
|
||||
let httpBackend = null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {getHttpUriForMxc} from "../../src/content-repo";
|
||||
import { getHttpUriForMxc } from "../../src/content-repo";
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
const baseUrl = "https://my.home.server";
|
||||
|
||||
+24
-24
@@ -1,16 +1,16 @@
|
||||
import '../olm-loader';
|
||||
import {Crypto} from "../../src/crypto";
|
||||
import {WebStorageSessionStore} from "../../src/store/session/webstorage";
|
||||
import {MemoryCryptoStore} from "../../src/crypto/store/memory-crypto-store";
|
||||
import {MockStorageApi} from "../MockStorageApi";
|
||||
import {TestClient} from "../TestClient";
|
||||
import {MatrixEvent} from "../../src/models/event";
|
||||
import {Room} from "../../src/models/room";
|
||||
import { Crypto } from "../../src/crypto";
|
||||
import { WebStorageSessionStore } from "../../src/store/session/webstorage";
|
||||
import { MemoryCryptoStore } from "../../src/crypto/store/memory-crypto-store";
|
||||
import { MockStorageApi } from "../MockStorageApi";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import * as olmlib from "../../src/crypto/olmlib";
|
||||
import {sleep} from "../../src/utils";
|
||||
import {EventEmitter} from "events";
|
||||
import {CRYPTO_ENABLED} from "../../src/client";
|
||||
import {DeviceInfo} from "../../src/crypto/deviceinfo";
|
||||
import { sleep } from "../../src/utils";
|
||||
import { EventEmitter } from "events";
|
||||
import { CRYPTO_ENABLED } from "../../src/client";
|
||||
import { DeviceInfo } from "../../src/crypto/deviceinfo";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -46,7 +46,7 @@ describe("Crypto", function() {
|
||||
|
||||
// unknown sender (e.g. deleted device), forwarded megolm key (untrusted)
|
||||
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
event.getWireContent = () => {return {algorithm: olmlib.MEGOLM_ALGORITHM};};
|
||||
event.getWireContent = () => {return { algorithm: olmlib.MEGOLM_ALGORITHM };};
|
||||
event.getForwardingCurve25519KeyChain = () => ["not empty"];
|
||||
event.isKeySourceUntrusted = () => false;
|
||||
event.getClaimedEd25519Key =
|
||||
@@ -65,7 +65,7 @@ describe("Crypto", function() {
|
||||
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
device.keys["ed25519:FLIBBLE"] =
|
||||
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
client._crypto._deviceList.getDeviceByIdentityKey = () => device;
|
||||
client.crypto.deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
@@ -213,7 +213,7 @@ describe("Crypto", function() {
|
||||
|
||||
async function keyshareEventForEvent(event, index) {
|
||||
const eventContent = event.getWireContent();
|
||||
const key = await aliceClient._crypto._olmDevice
|
||||
const key = await aliceClient.crypto.olmDevice
|
||||
.getInboundGroupSessionKey(
|
||||
roomId, eventContent.sender_key, eventContent.session_id,
|
||||
index,
|
||||
@@ -234,7 +234,7 @@ describe("Crypto", function() {
|
||||
},
|
||||
});
|
||||
// make onRoomKeyEvent think this was an encrypted event
|
||||
ksEvent._senderCurve25519Key = "akey";
|
||||
ksEvent.senderCurve25519Key = "akey";
|
||||
return ksEvent;
|
||||
}
|
||||
|
||||
@@ -273,19 +273,19 @@ describe("Crypto", function() {
|
||||
await Promise.all(events.map(async (event) => {
|
||||
// alice encrypts each event, and then bob tries to decrypt
|
||||
// them without any keys, so that they'll be in pending
|
||||
await aliceClient._crypto.encryptEvent(event, aliceRoom);
|
||||
event._clearEvent = {};
|
||||
event._senderCurve25519Key = null;
|
||||
event._claimedEd25519Key = null;
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
event.clearEvent = {};
|
||||
event.senderCurve25519Key = null;
|
||||
event.claimedEd25519Key = null;
|
||||
try {
|
||||
await bobClient._crypto.decryptEvent(event);
|
||||
await bobClient.crypto.decryptEvent(event);
|
||||
} catch (e) {
|
||||
// we expect this to fail because we don't have the
|
||||
// decryption keys yet
|
||||
}
|
||||
}));
|
||||
|
||||
const bobDecryptor = bobClient._crypto._getRoomDecryptor(
|
||||
const bobDecryptor = bobClient.crypto.getRoomDecryptor(
|
||||
roomId, olmlib.MEGOLM_ALGORITHM,
|
||||
);
|
||||
|
||||
@@ -302,7 +302,7 @@ describe("Crypto", function() {
|
||||
expect(events[0].getContent().msgtype).toBe("m.bad.encrypted");
|
||||
expect(events[1].getContent().msgtype).not.toBe("m.bad.encrypted");
|
||||
|
||||
const cryptoStore = bobClient._cryptoStore;
|
||||
const cryptoStore = bobClient.cryptoStore;
|
||||
const eventContent = events[0].getWireContent();
|
||||
const senderKey = eventContent.sender_key;
|
||||
const sessionId = eventContent.session_id;
|
||||
@@ -344,7 +344,7 @@ describe("Crypto", function() {
|
||||
},
|
||||
});
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
const cryptoStore = aliceClient._cryptoStore;
|
||||
const cryptoStore = aliceClient.cryptoStore;
|
||||
const roomKeyRequestBody = {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: "!someroom",
|
||||
@@ -377,7 +377,7 @@ describe("Crypto", function() {
|
||||
// key requests get queued until the sync has finished, but we don't
|
||||
// let the client set up enough for that to happen, so gut-wrench a bit
|
||||
// to force it to send now.
|
||||
aliceClient._crypto._outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||
aliceClient.crypto.outgoingRoomKeyRequestManager.sendQueuedRequests();
|
||||
jest.runAllTimers();
|
||||
await Promise.resolve();
|
||||
expect(aliceClient.sendToDevice).toBeCalledTimes(1);
|
||||
|
||||
@@ -22,11 +22,11 @@ import {
|
||||
import {
|
||||
IndexedDBCryptoStore,
|
||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import {MemoryCryptoStore} from '../../../src/crypto/store/memory-crypto-store';
|
||||
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
import {OlmDevice} from "../../../src/crypto/OlmDevice";
|
||||
import {logger} from '../../../src/logger';
|
||||
import { OlmDevice } from "../../../src/crypto/OlmDevice";
|
||||
import { logger } from '../../../src/logger';
|
||||
|
||||
const userId = "@alice:example.com";
|
||||
|
||||
@@ -66,7 +66,7 @@ describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
});
|
||||
|
||||
it.each(types)("should throw if the callback returns falsey",
|
||||
async ({type, shouldCache}) => {
|
||||
async ({ type, shouldCache }) => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: () => false,
|
||||
});
|
||||
|
||||
@@ -16,10 +16,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from "../../../src/logger";
|
||||
import { logger } from "../../../src/logger";
|
||||
import * as utils from "../../../src/utils";
|
||||
import {MemoryCryptoStore} from "../../../src/crypto/store/memory-crypto-store";
|
||||
import {DeviceList} from "../../../src/crypto/DeviceList";
|
||||
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
|
||||
import { DeviceList } from "../../../src/crypto/DeviceList";
|
||||
|
||||
const signedDeviceList = {
|
||||
"failures": {},
|
||||
@@ -51,6 +51,36 @@ const signedDeviceList = {
|
||||
},
|
||||
};
|
||||
|
||||
const signedDeviceList2 = {
|
||||
"failures": {},
|
||||
"device_keys": {
|
||||
"@test2:sw1v.org": {
|
||||
"QJVRHWAKGH": {
|
||||
"signatures": {
|
||||
"@test2:sw1v.org": {
|
||||
"ed25519:QJVRHWAKGH":
|
||||
"w1xxdLe1iIqzEFHLRVYQeuiM6t2N2ZRiI8s5nDKxf054BP8" +
|
||||
"1CPEX/AQXh5BhkKAVMlKnwg4T9zU1/wBALeajk3",
|
||||
},
|
||||
},
|
||||
"user_id": "@test2:sw1v.org",
|
||||
"keys": {
|
||||
"ed25519:QJVRHWAKGH":
|
||||
"Ig0/C6T+bBII1l2By2Wnnvtjp1nm/iXBlLU5/QESFXL",
|
||||
"curve25519:QJVRHWAKGH":
|
||||
"YR3eQnUvTQzGlWih4rsmJkKxpDxzgkgIgsBd1DEZIbm",
|
||||
},
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2",
|
||||
],
|
||||
"device_id": "QJVRHWAKGH",
|
||||
"unsigned": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('DeviceList', function() {
|
||||
let downloadSpy;
|
||||
let cryptoStore;
|
||||
@@ -69,7 +99,7 @@ describe('DeviceList', function() {
|
||||
}
|
||||
});
|
||||
|
||||
function createTestDeviceList() {
|
||||
function createTestDeviceList(keyDownloadChunkSize = 250) {
|
||||
const baseApis = {
|
||||
downloadKeysForUsers: downloadSpy,
|
||||
getUserId: () => '@test1:sw1v.org',
|
||||
@@ -78,7 +108,7 @@ describe('DeviceList', function() {
|
||||
const mockOlm = {
|
||||
verifySignature: function(key, message, signature) {},
|
||||
};
|
||||
const dl = new DeviceList(baseApis, cryptoStore, mockOlm);
|
||||
const dl = new DeviceList(baseApis, cryptoStore, mockOlm, keyDownloadChunkSize);
|
||||
deviceLists.push(dl);
|
||||
return dl;
|
||||
}
|
||||
@@ -150,4 +180,30 @@ describe('DeviceList', function() {
|
||||
expect(Object.keys(storedKeys)).toEqual(['HGKAWHRVJQ']);
|
||||
});
|
||||
});
|
||||
|
||||
it("should download device keys in batches", function() {
|
||||
const dl = createTestDeviceList(1);
|
||||
|
||||
dl.startTrackingDeviceList('@test1:sw1v.org');
|
||||
dl.startTrackingDeviceList('@test2:sw1v.org');
|
||||
|
||||
const queryDefer1 = utils.defer();
|
||||
downloadSpy.mockReturnValueOnce(queryDefer1.promise);
|
||||
const queryDefer2 = utils.defer();
|
||||
downloadSpy.mockReturnValueOnce(queryDefer2.promise);
|
||||
|
||||
const prom1 = dl.refreshOutdatedDeviceLists();
|
||||
expect(downloadSpy).toBeCalledTimes(2);
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(1, ['@test1:sw1v.org'], {});
|
||||
expect(downloadSpy).toHaveBeenNthCalledWith(2, ['@test2:sw1v.org'], {});
|
||||
queryDefer1.resolve(utils.deepCopy(signedDeviceList));
|
||||
queryDefer2.resolve(utils.deepCopy(signedDeviceList2));
|
||||
|
||||
return prom1.then(() => {
|
||||
const storedKeys1 = dl.getRawStoredDevicesForUser('@test1:sw1v.org');
|
||||
expect(Object.keys(storedKeys1)).toEqual(['HGKAWHRVJQ']);
|
||||
const storedKeys2 = dl.getRawStoredDevicesForUser('@test2:sw1v.org');
|
||||
expect(Object.keys(storedKeys2)).toEqual(['QJVRHWAKGH']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import '../../../olm-loader';
|
||||
import * as algorithms from "../../../../src/crypto/algorithms";
|
||||
import {MemoryCryptoStore} from "../../../../src/crypto/store/memory-crypto-store";
|
||||
import {MockStorageApi} from "../../../MockStorageApi";
|
||||
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
|
||||
import { MockStorageApi } from "../../../MockStorageApi";
|
||||
import * as testUtils from "../../../test-utils";
|
||||
import {OlmDevice} from "../../../../src/crypto/OlmDevice";
|
||||
import {Crypto} from "../../../../src/crypto";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import {TestClient} from "../../../TestClient";
|
||||
import {Room} from "../../../../src/models/room";
|
||||
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
|
||||
import { Crypto } from "../../../../src/crypto";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { TestClient } from "../../../TestClient";
|
||||
import { Room } from "../../../../src/models/room";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
@@ -50,7 +50,6 @@ describe("MegolmDecryption", function() {
|
||||
roomId: ROOM_ID,
|
||||
});
|
||||
|
||||
|
||||
// we stub out the olm encryption bits
|
||||
mockOlmLib = {};
|
||||
mockOlmLib.ensureOlmSessionsForDevices = jest.fn();
|
||||
@@ -136,9 +135,9 @@ describe("MegolmDecryption", function() {
|
||||
mockCrypto.getStoredDevice.mockReturnValue(deviceInfo);
|
||||
|
||||
mockOlmLib.ensureOlmSessionsForDevices.mockResolvedValue({
|
||||
'@alice:foo': {'alidevice': {
|
||||
'@alice:foo': { 'alidevice': {
|
||||
sessionId: 'alisession',
|
||||
}},
|
||||
} },
|
||||
});
|
||||
|
||||
const awaitEncryptForDevice = new Promise((res, rej) => {
|
||||
@@ -258,6 +257,9 @@ describe("MegolmDecryption", function() {
|
||||
});
|
||||
|
||||
it("re-uses sessions for sequential messages", async function() {
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: () => {},
|
||||
};
|
||||
const mockStorage = new MockStorageApi();
|
||||
const cryptoStore = new MemoryCryptoStore(mockStorage);
|
||||
|
||||
@@ -313,7 +315,7 @@ describe("MegolmDecryption", function() {
|
||||
});
|
||||
const mockRoom = {
|
||||
getEncryptionTargetMembers: jest.fn().mockReturnValue(
|
||||
[{userId: "@alice:home.server"}],
|
||||
[{ userId: "@alice:home.server" }],
|
||||
),
|
||||
getBlacklistUnverifiedDevices: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
@@ -363,9 +365,9 @@ describe("MegolmDecryption", function() {
|
||||
bobClient1.initCrypto(),
|
||||
bobClient2.initCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient._crypto._olmDevice;
|
||||
const bobDevice1 = bobClient1._crypto._olmDevice;
|
||||
const bobDevice2 = bobClient2._crypto._olmDevice;
|
||||
const aliceDevice = aliceClient.crypto.olmDevice;
|
||||
const bobDevice1 = bobClient1.crypto.olmDevice;
|
||||
const bobDevice2 = bobClient2.crypto.olmDevice;
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -373,7 +375,7 @@ describe("MegolmDecryption", function() {
|
||||
const roomId = "!someroom";
|
||||
const room = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
room.getEncryptionTargetMembers = async function() {
|
||||
return [{userId: "@bob:example.com"}];
|
||||
return [{ userId: "@bob:example.com" }];
|
||||
};
|
||||
room.setBlacklistUnverifiedDevices(true);
|
||||
aliceClient.store.storeRoom(room);
|
||||
@@ -402,11 +404,11 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient._crypto._deviceList.storeDevicesForUser(
|
||||
aliceClient.crypto.deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
|
||||
return this._getDevicesFromStore(userIds);
|
||||
aliceClient.crypto.deviceList.downloadKeys = async function(userIds) {
|
||||
return this.getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
let run = false;
|
||||
@@ -446,7 +448,7 @@ describe("MegolmDecryption", function() {
|
||||
body: "secret",
|
||||
},
|
||||
});
|
||||
await aliceClient._crypto.encryptEvent(event, room);
|
||||
await aliceClient.crypto.encryptEvent(event, room);
|
||||
|
||||
expect(run).toBe(true);
|
||||
|
||||
@@ -466,8 +468,8 @@ describe("MegolmDecryption", function() {
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient._crypto._olmDevice;
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
const aliceDevice = aliceClient.crypto.olmDevice;
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
@@ -506,11 +508,11 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient._crypto._deviceList.storeDevicesForUser(
|
||||
aliceClient.crypto.deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
|
||||
return this._getDevicesFromStore(userIds);
|
||||
aliceClient.crypto.deviceList.downloadKeys = async function(userIds) {
|
||||
return this.getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
aliceClient.claimOneTimeKeys = async () => {
|
||||
@@ -544,7 +546,7 @@ describe("MegolmDecryption", function() {
|
||||
event_id: "$event",
|
||||
content: {},
|
||||
});
|
||||
await aliceClient._crypto.encryptEvent(event, aliceRoom);
|
||||
await aliceClient.crypto.encryptEvent(event, aliceRoom);
|
||||
await sendPromise;
|
||||
});
|
||||
|
||||
@@ -559,11 +561,11 @@ describe("MegolmDecryption", function() {
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
||||
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
|
||||
type: "org.matrix.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
@@ -576,7 +578,7 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
@@ -602,14 +604,14 @@ describe("MegolmDecryption", function() {
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
aliceClient._crypto.downloadKeys = async () => {};
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
aliceClient.crypto.downloadKeys = async () => {};
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
||||
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
|
||||
type: "org.matrix.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
@@ -626,7 +628,7 @@ describe("MegolmDecryption", function() {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
@@ -653,15 +655,15 @@ describe("MegolmDecryption", function() {
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
aliceClient._crypto.downloadKeys = async () => {};
|
||||
const bobDevice = bobClient.crypto.olmDevice;
|
||||
aliceClient.crypto.downloadKeys = async () => {};
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// pretend we got an event that we can't decrypt
|
||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
||||
aliceClient.crypto.onToDeviceEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
@@ -676,7 +678,7 @@ describe("MegolmDecryption", function() {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
|
||||
await expect(aliceClient.crypto.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
|
||||
@@ -16,12 +16,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import '../../../olm-loader';
|
||||
import {MemoryCryptoStore} from "../../../../src/crypto/store/memory-crypto-store";
|
||||
import {MockStorageApi} from "../../../MockStorageApi";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {OlmDevice} from "../../../../src/crypto/OlmDevice";
|
||||
import { MemoryCryptoStore } from "../../../../src/crypto/store/memory-crypto-store";
|
||||
import { MockStorageApi } from "../../../MockStorageApi";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { OlmDevice } from "../../../../src/crypto/OlmDevice";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
|
||||
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
|
||||
|
||||
function makeOlmDevice() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
@@ -190,5 +190,91 @@ describe("OlmDevice", function() {
|
||||
// new session and will have called claimOneTimeKeys
|
||||
expect(count).toBe(2);
|
||||
});
|
||||
|
||||
it("avoids deadlocks when two tasks are ensuring the same devices", async function() {
|
||||
// This test checks whether `ensureOlmSessionsForDevices` properly
|
||||
// handles multiple tasks in flight ensuring some set of devices in
|
||||
// common without deadlocks.
|
||||
|
||||
let claimRequestCount = 0;
|
||||
const baseApis = {
|
||||
claimOneTimeKeys: () => {
|
||||
// simulate a very slow server (.5 seconds to respond)
|
||||
claimRequestCount++;
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(reject, 500);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
const deviceBobA = DeviceInfo.fromStorage({
|
||||
keys: {
|
||||
"curve25519:BOB-A": "akey",
|
||||
},
|
||||
}, "BOB-A");
|
||||
const deviceBobB = DeviceInfo.fromStorage({
|
||||
keys: {
|
||||
"curve25519:BOB-B": "bkey",
|
||||
},
|
||||
}, "BOB-B");
|
||||
|
||||
// There's no required ordering of devices per user, so here we
|
||||
// create two different orderings so that each task reserves a
|
||||
// device the other task needs before continuing.
|
||||
const devicesByUserAB = {
|
||||
"@bob:example.com": [
|
||||
deviceBobA,
|
||||
deviceBobB,
|
||||
],
|
||||
};
|
||||
const devicesByUserBA = {
|
||||
"@bob:example.com": [
|
||||
deviceBobB,
|
||||
deviceBobA,
|
||||
],
|
||||
};
|
||||
|
||||
function alwaysSucceed(promise) {
|
||||
// swallow any exception thrown by a promise, so that
|
||||
// Promise.all doesn't abort
|
||||
return promise.catch(() => {});
|
||||
}
|
||||
|
||||
const task1 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUserAB,
|
||||
));
|
||||
|
||||
// After a single tick through the first task, it should have
|
||||
// claimed ownership of all devices to avoid deadlocking others.
|
||||
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
|
||||
|
||||
const task2 = alwaysSucceed(olmlib.ensureOlmSessionsForDevices(
|
||||
aliceOlmDevice, baseApis, devicesByUserBA,
|
||||
));
|
||||
|
||||
// The second task should not have changed the ownership count, as
|
||||
// it's waiting on the first task.
|
||||
expect(Object.keys(aliceOlmDevice._sessionsInProgress).length).toBe(2);
|
||||
|
||||
// Track the tasks, but don't await them yet.
|
||||
const promises = Promise.all([
|
||||
task1,
|
||||
task2,
|
||||
]);
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 200);
|
||||
});
|
||||
|
||||
// After .2s, the first task should have made an initial claim request.
|
||||
expect(claimRequestCount).toBe(1);
|
||||
|
||||
await promises;
|
||||
|
||||
// After waiting for both tasks to complete, the first task should
|
||||
// have failed, so the second task should have tried to create a
|
||||
// new session and will have called claimOneTimeKeys
|
||||
expect(claimRequestCount).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,18 +16,19 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import '../../olm-loader';
|
||||
import {logger} from "../../../src/logger";
|
||||
import { logger } from "../../../src/logger";
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import {MatrixClient} from "../../../src/client";
|
||||
import {MatrixEvent} from "../../../src/models/event";
|
||||
import { MatrixClient } from "../../../src/client";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import * as algorithms from "../../../src/crypto/algorithms";
|
||||
import {WebStorageSessionStore} from "../../../src/store/session/webstorage";
|
||||
import {MemoryCryptoStore} from "../../../src/crypto/store/memory-crypto-store";
|
||||
import {MockStorageApi} from "../../MockStorageApi";
|
||||
import { WebStorageSessionStore } from "../../../src/store/session/webstorage";
|
||||
import { MemoryCryptoStore } from "../../../src/crypto/store/memory-crypto-store";
|
||||
import { MockStorageApi } from "../../MockStorageApi";
|
||||
import * as testUtils from "../../test-utils";
|
||||
import {OlmDevice} from "../../../src/crypto/OlmDevice";
|
||||
import {Crypto} from "../../../src/crypto";
|
||||
import {resetCrossSigningKeys} from "./crypto-utils";
|
||||
import { OlmDevice } from "../../../src/crypto/OlmDevice";
|
||||
import { Crypto } from "../../../src/crypto";
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { BackupManager } from "../../../src/crypto/backup";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -73,7 +74,7 @@ const KEY_BACKUP_DATA = {
|
||||
};
|
||||
|
||||
const BACKUP_INFO = {
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
@@ -138,6 +139,7 @@ describe("MegolmBackup", function() {
|
||||
let megolmDecryption;
|
||||
beforeEach(async function() {
|
||||
mockCrypto = testUtils.mock(Crypto, 'Crypto');
|
||||
mockCrypto.backupManager = testUtils.mock(BackupManager, "BackupManager");
|
||||
mockCrypto.backupKey = new Olm.PkEncryption();
|
||||
mockCrypto.backupKey.set_recipient_key(
|
||||
"hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
@@ -215,12 +217,14 @@ describe("MegolmBackup", function() {
|
||||
};
|
||||
mockCrypto.cancelRoomKeyRequest = function() {};
|
||||
|
||||
mockCrypto.backupGroupSession = jest.fn();
|
||||
mockCrypto.backupManager = {
|
||||
backupGroupSession: jest.fn(),
|
||||
};
|
||||
|
||||
return event.attemptDecryption(mockCrypto).then(() => {
|
||||
return megolmDecryption.onRoomKeyEvent(event);
|
||||
}).then(() => {
|
||||
expect(mockCrypto.backupGroupSession).toHaveBeenCalled();
|
||||
expect(mockCrypto.backupManager.backupGroupSession).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -264,7 +268,7 @@ describe("MegolmBackup", function() {
|
||||
})
|
||||
.then(() => {
|
||||
client.enableKeyBackup({
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
@@ -272,7 +276,7 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
client._http.authedRequest = function(
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
@@ -292,12 +296,9 @@ describe("MegolmBackup", function() {
|
||||
resolve();
|
||||
return Promise.resolve({});
|
||||
};
|
||||
client._crypto.backupGroupSession(
|
||||
"roomId",
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
[],
|
||||
groupSession.session_id(),
|
||||
groupSession.session_key(),
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(1);
|
||||
@@ -335,39 +336,48 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
await resetCrossSigningKeys(client);
|
||||
let numCalls = 0;
|
||||
await new Promise((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 Promise.all([
|
||||
new Promise((resolve, reject) => {
|
||||
let backupInfo;
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
expect(numCalls).toBeLessThanOrEqual(2);
|
||||
if (numCalls === 1) {
|
||||
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({});
|
||||
}
|
||||
backupInfo = data;
|
||||
return Promise.resolve({});
|
||||
} else if (numCalls === 2) {
|
||||
expect(method).toBe("GET");
|
||||
expect(path).toBe("/room_keys/version");
|
||||
resolve();
|
||||
return Promise.resolve(backupInfo);
|
||||
} else {
|
||||
// exit out of retry loop if there's something wrong
|
||||
reject(new Error("authedRequest called too many times"));
|
||||
return Promise.resolve({});
|
||||
}
|
||||
};
|
||||
}),
|
||||
client.createKeyBackupVersion({
|
||||
algorithm: "m.megolm_backup.v1",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
},
|
||||
});
|
||||
});
|
||||
expect(numCalls).toBe(1);
|
||||
}),
|
||||
]);
|
||||
expect(numCalls).toBe(2);
|
||||
});
|
||||
|
||||
it('retries when a backup fails', function() {
|
||||
@@ -434,7 +444,7 @@ describe("MegolmBackup", function() {
|
||||
})
|
||||
.then(() => {
|
||||
client.enableKeyBackup({
|
||||
algorithm: "foobar",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
version: 1,
|
||||
auth_data: {
|
||||
public_key: "hSDwCYkwp1R0i33ctD73Wg2/Og0mOBr066SpjqqbTmo",
|
||||
@@ -442,7 +452,7 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
let numCalls = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
client._http.authedRequest = function(
|
||||
client.http.authedRequest = function(
|
||||
callback, method, path, queryParams, data, opts,
|
||||
) {
|
||||
++numCalls;
|
||||
@@ -468,12 +478,9 @@ describe("MegolmBackup", function() {
|
||||
);
|
||||
}
|
||||
};
|
||||
client._crypto.backupGroupSession(
|
||||
"roomId",
|
||||
client.crypto.backupManager.backupGroupSession(
|
||||
"F0Q2NmyJNgUVj9DGsb4ZQt3aVxhVcUQhg7+gvW0oyKI",
|
||||
[],
|
||||
groupSession.session_id(),
|
||||
groupSession.session_key(),
|
||||
);
|
||||
}).then(() => {
|
||||
expect(numCalls).toBe(2);
|
||||
@@ -506,7 +513,7 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
|
||||
it('can restore from backup', function() {
|
||||
client._http.authedRequest = function() {
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(KEY_BACKUP_DATA);
|
||||
};
|
||||
return client.restoreKeyBackupWithRecoveryKey(
|
||||
@@ -523,7 +530,7 @@ describe("MegolmBackup", function() {
|
||||
});
|
||||
|
||||
it('can restore backup by room', function() {
|
||||
client._http.authedRequest = function() {
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve({
|
||||
rooms: {
|
||||
[ROOM_ID]: {
|
||||
@@ -546,15 +553,15 @@ describe("MegolmBackup", function() {
|
||||
|
||||
it('has working cache functions', async function() {
|
||||
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
await client._crypto.storeSessionBackupPrivateKey(key);
|
||||
const result = await client._crypto.getSessionBackupPrivateKey();
|
||||
await client.crypto.storeSessionBackupPrivateKey(key);
|
||||
const result = await client.crypto.getSessionBackupPrivateKey();
|
||||
expect(new Uint8Array(result)).toEqual(key);
|
||||
});
|
||||
|
||||
it('caches session backup keys as it encounters them', async function() {
|
||||
const cachedNull = await client._crypto.getSessionBackupPrivateKey();
|
||||
const cachedNull = await client.crypto.getSessionBackupPrivateKey();
|
||||
expect(cachedNull).toBeNull();
|
||||
client._http.authedRequest = function() {
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(KEY_BACKUP_DATA);
|
||||
};
|
||||
await new Promise((resolve) => {
|
||||
@@ -566,8 +573,24 @@ describe("MegolmBackup", function() {
|
||||
{ cacheCompleteCallback: resolve },
|
||||
);
|
||||
});
|
||||
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
|
||||
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
|
||||
expect(cachedKey).not.toBeNull();
|
||||
});
|
||||
|
||||
it("fails if an known algorithm is used", async function() {
|
||||
const BAD_BACKUP_INFO = Object.assign({}, BACKUP_INFO, {
|
||||
algorithm: "this.algorithm.does.not.exist",
|
||||
});
|
||||
client.http.authedRequest = function() {
|
||||
return Promise.resolve(KEY_BACKUP_DATA);
|
||||
};
|
||||
|
||||
await expect(client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
BAD_BACKUP_INFO,
|
||||
)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,11 +18,11 @@ limitations under the License.
|
||||
import '../../olm-loader';
|
||||
import anotherjson from 'another-json';
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {HttpResponse, setHttpResponses} from '../../test-utils';
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { HttpResponse, setHttpResponses } from '../../test-utils';
|
||||
import { resetCrossSigningKeys } from "./crypto-utils";
|
||||
import { MatrixError } from '../../../src/http-api';
|
||||
import {logger} from '../../../src/logger';
|
||||
import { logger } from '../../../src/logger';
|
||||
|
||||
async function makeTestClient(userInfo, options, keys) {
|
||||
if (!keys) keys = {};
|
||||
@@ -60,12 +60,12 @@ describe("Cross Signing", function() {
|
||||
|
||||
it("should sign the master key with the device key", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ 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.crypto.olmDevice, keys.master_key, "@alice:example.com",
|
||||
"Osborne2", alice.crypto.olmDevice.deviceEd25519Key,
|
||||
);
|
||||
});
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
@@ -80,7 +80,7 @@ describe("Cross Signing", function() {
|
||||
|
||||
it("should abort bootstrap if device signing auth fails", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async (auth, keys) => {
|
||||
const errorResponse = {
|
||||
@@ -131,14 +131,14 @@ describe("Cross Signing", function() {
|
||||
|
||||
it("should upload a signature when a user is verified", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's device key
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -175,7 +175,7 @@ describe("Cross Signing", function() {
|
||||
]);
|
||||
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
// will be called to sign our own device
|
||||
@@ -193,30 +193,36 @@ describe("Cross Signing", function() {
|
||||
const keyChangePromise = new Promise((resolve, reject) => {
|
||||
alice.once("crossSigning.keysChanged", async (e) => {
|
||||
resolve(e);
|
||||
await alice.checkOwnCrossSigningTrust();
|
||||
await alice.checkOwnCrossSigningTrust({
|
||||
allowPrivateKeyRequests: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
try {
|
||||
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();
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
|
||||
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const aliceDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -224,7 +230,7 @@ describe("Cross Signing", function() {
|
||||
};
|
||||
aliceDevice.keys = deviceInfo.keys;
|
||||
aliceDevice.algorithms = deviceInfo.algorithms;
|
||||
await alice._crypto._signObject(aliceDevice);
|
||||
await alice.crypto.signObject(aliceDevice);
|
||||
olmlib.pkSign(aliceDevice, selfSigningKey, "@alice:example.com");
|
||||
|
||||
// feed sync result that includes master key, ssk, device key
|
||||
@@ -326,7 +332,7 @@ describe("Cross Signing", function() {
|
||||
|
||||
it("should use trust chain to determine device verification", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
@@ -352,7 +358,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -381,7 +387,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobPubkey]: sig,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
// Bob's device key should be TOFU
|
||||
@@ -411,12 +417,12 @@ describe("Cross Signing", function() {
|
||||
it("should trust signatures received from other devices", async function() {
|
||||
const aliceKeys = {};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
null,
|
||||
aliceKeys,
|
||||
);
|
||||
alice._crypto._deviceList.startTrackingDeviceList("@bob:example.com");
|
||||
alice._crypto._deviceList.stopTrackingAllDeviceLists = () => {};
|
||||
alice.crypto.deviceList.startTrackingDeviceList("@bob:example.com");
|
||||
alice.crypto.deviceList.stopTrackingAllDeviceLists = () => {};
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
|
||||
@@ -431,14 +437,14 @@ describe("Cross Signing", function() {
|
||||
]);
|
||||
|
||||
const keyChangePromise = new Promise((resolve, reject) => {
|
||||
alice._crypto._deviceList.once("userCrossSigningUpdated", (userId) => {
|
||||
alice.crypto.deviceList.once("userCrossSigningUpdated", (userId) => {
|
||||
if (userId === "@bob:example.com") {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
|
||||
const deviceInfo = alice.crypto.deviceList.devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const aliceDevice = {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -446,7 +452,7 @@ describe("Cross Signing", function() {
|
||||
};
|
||||
aliceDevice.keys = deviceInfo.keys;
|
||||
aliceDevice.algorithms = deviceInfo.algorithms;
|
||||
await alice._crypto._signObject(aliceDevice);
|
||||
await alice.crypto.signObject(aliceDevice);
|
||||
|
||||
const bobOlmAccount = new global.Olm.Account();
|
||||
bobOlmAccount.create();
|
||||
@@ -573,7 +579,7 @@ describe("Cross Signing", function() {
|
||||
|
||||
it("should dis-trust an unsigned device", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
@@ -600,7 +606,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -623,7 +629,7 @@ describe("Cross Signing", function() {
|
||||
"ed25519:Dynabook": "someOtherPubkey",
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
// Bob's device key should be untrusted
|
||||
@@ -642,7 +648,7 @@ describe("Cross Signing", function() {
|
||||
|
||||
it("should dis-trust a user when their ssk changes", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
@@ -667,7 +673,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey]: sskSig,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -695,7 +701,7 @@ describe("Cross Signing", function() {
|
||||
bobDevice.signatures = {};
|
||||
bobDevice.signatures["@bob:example.com"] = {};
|
||||
bobDevice.signatures["@bob:example.com"]["ed25519:" + bobPubkey] = sig;
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
// Alice verifies Bob's SSK
|
||||
@@ -727,7 +733,7 @@ describe("Cross Signing", function() {
|
||||
["ed25519:" + bobMasterPubkey2]: sskSig2,
|
||||
},
|
||||
};
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@bob:example.com",
|
||||
@@ -764,7 +770,7 @@ describe("Cross Signing", function() {
|
||||
// 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", {
|
||||
alice.crypto.deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: bobDevice,
|
||||
});
|
||||
|
||||
@@ -780,7 +786,7 @@ describe("Cross Signing", function() {
|
||||
let upgradeResolveFunc;
|
||||
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
shouldUpgradeDeviceVerifications: (verifs) => {
|
||||
@@ -792,27 +798,27 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
);
|
||||
const bob = await makeTestClient(
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
);
|
||||
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
// set Bob's cross-signing key
|
||||
await resetCrossSigningKeys(bob);
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
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,
|
||||
"curve25519:Dynabook": bob.crypto.olmDevice.deviceCurve25519Key,
|
||||
"ed25519:Dynabook": bob.crypto.olmDevice.deviceEd25519Key,
|
||||
},
|
||||
verified: 1,
|
||||
known: true,
|
||||
},
|
||||
});
|
||||
alice._crypto._deviceList.storeCrossSigningForUser(
|
||||
alice.crypto.deviceList.storeCrossSigningForUser(
|
||||
"@bob:example.com",
|
||||
bob._crypto._crossSigningInfo.toStorage(),
|
||||
bob.crypto.crossSigningInfo.toStorage(),
|
||||
);
|
||||
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
@@ -832,7 +838,7 @@ describe("Cross Signing", function() {
|
||||
expect(bobTrust.isTofu()).toBeTruthy();
|
||||
|
||||
// "forget" that Bob is trusted
|
||||
delete alice._crypto._deviceList._crossSigningInfo["@bob:example.com"]
|
||||
delete alice.crypto.deviceList.crossSigningInfo["@bob:example.com"]
|
||||
.keys.master.signatures["@alice:example.com"];
|
||||
|
||||
const bobTrust2 = alice.checkUserTrust("@bob:example.com");
|
||||
@@ -842,9 +848,9 @@ describe("Cross Signing", function() {
|
||||
upgradePromise = new Promise((resolve) => {
|
||||
upgradeResolveFunc = resolve;
|
||||
});
|
||||
alice._crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
|
||||
alice.crypto.deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
|
||||
await new Promise((resolve) => {
|
||||
alice._crypto.on("userTrustStatusChanged", resolve);
|
||||
alice.crypto.on("userTrustStatusChanged", resolve);
|
||||
});
|
||||
await upgradePromise;
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {IndexedDBCryptoStore} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
|
||||
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
|
||||
// needs to be phased out and replaced with bootstrapSecretStorage,
|
||||
// but that is doing too much extra stuff for it to be an easy transition.
|
||||
@@ -7,28 +6,28 @@ export async function resetCrossSigningKeys(client, {
|
||||
level,
|
||||
authUploadDeviceSigningKeys = async func => await func(),
|
||||
} = {}) {
|
||||
const crypto = client._crypto;
|
||||
const crypto = client.crypto;
|
||||
|
||||
const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys);
|
||||
const oldKeys = Object.assign({}, crypto.crossSigningInfo.keys);
|
||||
try {
|
||||
await crypto._crossSigningInfo.resetKeys(level);
|
||||
await crypto._signObject(crypto._crossSigningInfo.keys.master);
|
||||
await crypto.crossSigningInfo.resetKeys(level);
|
||||
await crypto.signObject(crypto.crossSigningInfo.keys.master);
|
||||
// write a copy locally so we know these are trusted keys
|
||||
await crypto._cryptoStore.doTxn(
|
||||
await crypto.cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
crypto._cryptoStore.storeCrossSigningKeys(
|
||||
txn, crypto._crossSigningInfo.keys);
|
||||
crypto.cryptoStore.storeCrossSigningKeys(
|
||||
txn, crypto.crossSigningInfo.keys);
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
// If anything failed here, revert the keys so we know to try again from the start
|
||||
// next time.
|
||||
crypto._crossSigningInfo.keys = oldKeys;
|
||||
crypto.crossSigningInfo.keys = oldKeys;
|
||||
throw e;
|
||||
}
|
||||
crypto._baseApis.emit("crossSigning.keysChanged", {});
|
||||
await crypto._afterCrossSigningLocalKeyChange();
|
||||
crypto.baseApis.emit("crossSigning.keysChanged", {});
|
||||
await crypto.afterCrossSigningLocalKeyChange();
|
||||
}
|
||||
|
||||
export async function createSecretStorageKey() {
|
||||
|
||||
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
import {
|
||||
IndexedDBCryptoStore,
|
||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import {MemoryCryptoStore} from '../../../src/crypto/store/memory-crypto-store';
|
||||
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
|
||||
|
||||
@@ -16,13 +16,13 @@ limitations under the License.
|
||||
|
||||
import '../../olm-loader';
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import {SECRET_STORAGE_ALGORITHM_V1_AES} from "../../../src/crypto/SecretStorage";
|
||||
import {MatrixEvent} from "../../../src/models/event";
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {makeTestClients} from './verification/util';
|
||||
import {encryptAES} from "../../../src/crypto/aes";
|
||||
import {resetCrossSigningKeys, createSecretStorageKey} from "./crypto-utils";
|
||||
import {logger} from '../../../src/logger';
|
||||
import { SECRET_STORAGE_ALGORITHM_V1_AES } from "../../../src/crypto/SecretStorage";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { makeTestClients } from './verification/util';
|
||||
import { encryptAES } from "../../../src/crypto/aes";
|
||||
import { resetCrossSigningKeys, createSecretStorageKey } from "./crypto-utils";
|
||||
import { logger } from '../../../src/logger';
|
||||
|
||||
import * as utils from "../../../src/utils";
|
||||
|
||||
@@ -47,7 +47,7 @@ async function makeTestClient(userInfo, options) {
|
||||
await client.initCrypto();
|
||||
|
||||
// No need to download keys for these tests
|
||||
client._crypto.downloadKeys = async function() {};
|
||||
client.crypto.downloadKeys = async function() {};
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -91,7 +91,7 @@ describe("Secrets", function() {
|
||||
});
|
||||
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => signingKey,
|
||||
@@ -99,11 +99,11 @@ describe("Secrets", function() {
|
||||
},
|
||||
},
|
||||
);
|
||||
alice._crypto._crossSigningInfo.setKeys({
|
||||
alice.crypto.crossSigningInfo.setKeys({
|
||||
master: signingkeyInfo,
|
||||
});
|
||||
|
||||
const secretStorage = alice._crypto._secretStorage;
|
||||
const secretStorage = alice.crypto.secretStorage;
|
||||
|
||||
alice.setAccountData = async function(eventType, contents, callback) {
|
||||
alice.store.storeAccountDataEvents([
|
||||
@@ -120,7 +120,7 @@ describe("Secrets", function() {
|
||||
const keyAccountData = {
|
||||
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
};
|
||||
await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master');
|
||||
await alice.crypto.crossSigningInfo.signObject(keyAccountData, 'master');
|
||||
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
@@ -141,7 +141,7 @@ describe("Secrets", function() {
|
||||
|
||||
it("should throw if given a key that doesn't exist", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -155,7 +155,7 @@ describe("Secrets", function() {
|
||||
|
||||
it("should refuse to encrypt with zero keys", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -175,7 +175,7 @@ describe("Secrets", function() {
|
||||
|
||||
let keys = {};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => keys[t],
|
||||
@@ -208,7 +208,7 @@ describe("Secrets", function() {
|
||||
|
||||
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"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
);
|
||||
|
||||
try {
|
||||
@@ -221,8 +221,8 @@ describe("Secrets", function() {
|
||||
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"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@alice:example.com", deviceId: "VAX" },
|
||||
],
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
@@ -234,11 +234,11 @@ describe("Secrets", function() {
|
||||
},
|
||||
);
|
||||
|
||||
const vaxDevice = vax.client._crypto._olmDevice;
|
||||
const osborne2Device = osborne2.client._crypto._olmDevice;
|
||||
const secretStorage = osborne2.client._crypto._secretStorage;
|
||||
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", {
|
||||
osborne2.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
"VAX": {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "VAX",
|
||||
@@ -249,7 +249,7 @@ describe("Secrets", function() {
|
||||
},
|
||||
},
|
||||
});
|
||||
vax.client._crypto._deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
vax.client.crypto.deviceList.storeDevicesForUser("@alice:example.com", {
|
||||
"Osborne2": {
|
||||
user_id: "@alice:example.com",
|
||||
device_id: "Osborne2",
|
||||
@@ -265,7 +265,7 @@ describe("Secrets", function() {
|
||||
const otks = (await osborne2Device.getOneTimeKeys()).curve25519;
|
||||
await osborne2Device.markKeysAsPublished();
|
||||
|
||||
await vax.client._crypto._olmDevice.createOutboundSession(
|
||||
await vax.client.crypto.olmDevice.createOutboundSession(
|
||||
osborne2Device.deviceCurve25519Key,
|
||||
Object.values(otks)[0],
|
||||
);
|
||||
@@ -334,8 +334,8 @@ describe("Secrets", function() {
|
||||
createSecretStorageKey,
|
||||
});
|
||||
|
||||
const crossSigning = bob._crypto._crossSigningInfo;
|
||||
const secretStorage = bob._crypto._secretStorage;
|
||||
const crossSigning = bob.crypto.crossSigningInfo;
|
||||
const secretStorage = bob.crypto.secretStorage;
|
||||
|
||||
expect(crossSigning.getId()).toBeTruthy();
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
@@ -376,10 +376,10 @@ describe("Secrets", function() {
|
||||
]);
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
bob._crypto.checkKeyBackup = async () => {};
|
||||
bob.crypto.backupManager.checkKeyBackup = async () => {};
|
||||
|
||||
const crossSigning = bob._crypto._crossSigningInfo;
|
||||
const secretStorage = bob._crypto._secretStorage;
|
||||
const crossSigning = bob.crypto.crossSigningInfo;
|
||||
const secretStorage = bob.crypto.secretStorage;
|
||||
|
||||
// Set up cross-signing keys from scratch with specific storage key
|
||||
await bob.bootstrapCrossSigning({
|
||||
@@ -394,7 +394,7 @@ describe("Secrets", function() {
|
||||
});
|
||||
|
||||
// Clear local cross-signing keys and read from secret storage
|
||||
bob._crypto._deviceList.storeCrossSigningForUser(
|
||||
bob.crypto.deviceList.storeCrossSigningForUser(
|
||||
"@bob:example.com",
|
||||
crossSigning.toStorage(),
|
||||
);
|
||||
@@ -419,12 +419,12 @@ describe("Secrets", function() {
|
||||
key_id: SSSSKey,
|
||||
};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => crossSigningKeys[t],
|
||||
saveCrossSigningKeys: k => crossSigningKeys = k,
|
||||
getSecretStorageKey: ({keys}, name) => {
|
||||
getSecretStorageKey: ({ keys }, name) => {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (secretStorageKeys[keyId]) {
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
@@ -458,7 +458,7 @@ describe("Secrets", function() {
|
||||
type: "m.cross_signing.master",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -466,7 +466,7 @@ describe("Secrets", function() {
|
||||
type: "m.cross_signing.self_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -474,12 +474,12 @@ describe("Secrets", function() {
|
||||
type: "m.cross_signing.user_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
@@ -525,7 +525,7 @@ describe("Secrets", function() {
|
||||
await alice.bootstrapSecretStorage();
|
||||
|
||||
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
|
||||
.toEqual({key: "key_id"});
|
||||
.toEqual({ key: "key_id" });
|
||||
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")
|
||||
.getContent();
|
||||
expect(keyInfo.algorithm)
|
||||
@@ -550,12 +550,12 @@ describe("Secrets", function() {
|
||||
key_id: SSSSKey,
|
||||
};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => crossSigningKeys[t],
|
||||
saveCrossSigningKeys: k => crossSigningKeys = k,
|
||||
getSecretStorageKey: ({keys}, name) => {
|
||||
getSecretStorageKey: ({ keys }, name) => {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (secretStorageKeys[keyId]) {
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
@@ -587,7 +587,7 @@ describe("Secrets", function() {
|
||||
type: "m.cross_signing.master",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -595,7 +595,7 @@ describe("Secrets", function() {
|
||||
type: "m.cross_signing.self_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -603,7 +603,7 @@ describe("Secrets", function() {
|
||||
type: "m.cross_signing.user_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
key_id: { ciphertext: "bla", mac: "bla", iv: "bla" },
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -619,7 +619,7 @@ describe("Secrets", function() {
|
||||
},
|
||||
}),
|
||||
]);
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
alice.crypto.deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
|
||||
@@ -13,9 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import {InRoomChannel} from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
|
||||
describe("InRoomChannel tests", function() {
|
||||
const ALICE = "@alice:hs.tld";
|
||||
|
||||
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import { logger } from "../../../../src/logger";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
|
||||
@@ -15,10 +15,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import {verificationMethods} from "../../../../src/crypto";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {SAS} from "../../../../src/crypto/verification/SAS";
|
||||
import {makeTestClients, setupWebcrypto, teardownWebcrypto} from './util';
|
||||
import { verificationMethods } from "../../../../src/crypto";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -42,14 +42,14 @@ describe("verification request integration tests with crypto layer", function()
|
||||
it("should request and accept a verification", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
alice.client._crypto._deviceList.getRawStoredDevicesForUser = function() {
|
||||
alice.client.crypto.deviceList.getRawStoredDevicesForUser = function() {
|
||||
return {
|
||||
Dynabook: {
|
||||
keys: {
|
||||
|
||||
@@ -15,14 +15,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import "../../../olm-loader";
|
||||
import {makeTestClients, setupWebcrypto, teardownWebcrypto} from './util';
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import {SAS} from "../../../../src/crypto/verification/SAS";
|
||||
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
|
||||
import {verificationMethods} from "../../../../src/crypto";
|
||||
import { makeTestClients, setupWebcrypto, teardownWebcrypto } from './util';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { SAS } from "../../../../src/crypto/verification/SAS";
|
||||
import { DeviceInfo } from "../../../../src/crypto/deviceinfo";
|
||||
import { verificationMethods } from "../../../../src/crypto";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {resetCrossSigningKeys} from "../crypto-utils";
|
||||
import { logger } from "../../../../src/logger";
|
||||
import { resetCrossSigningKeys } from "../crypto-utils";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -79,16 +79,16 @@ describe("SAS verification", function() {
|
||||
beforeEach(async () => {
|
||||
[alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
},
|
||||
);
|
||||
|
||||
const aliceDevice = alice.client._crypto._olmDevice;
|
||||
const bobDevice = bob.client._crypto._olmDevice;
|
||||
const aliceDevice = alice.client.crypto.olmDevice;
|
||||
const bobDevice = bob.client.crypto.olmDevice;
|
||||
|
||||
ALICE_DEVICES = {
|
||||
Osborne2: {
|
||||
@@ -114,14 +114,14 @@ describe("SAS verification", function() {
|
||||
},
|
||||
};
|
||||
|
||||
alice.client._crypto._deviceList.storeDevicesForUser(
|
||||
alice.client.crypto.deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
alice.client.downloadKeys = () => {
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
bob.client._crypto._deviceList.storeDevicesForUser(
|
||||
bob.client.crypto.deviceList.storeDevicesForUser(
|
||||
"@alice:example.com", ALICE_DEVICES,
|
||||
);
|
||||
bob.client.downloadKeys = () => {
|
||||
@@ -296,9 +296,9 @@ describe("SAS verification", function() {
|
||||
|
||||
await resetCrossSigningKeys(bob.client);
|
||||
|
||||
bob.client._crypto._deviceList.storeCrossSigningForUser(
|
||||
bob.client.crypto.deviceList.storeCrossSigningForUser(
|
||||
"@alice:example.com", {
|
||||
keys: alice.client._crypto._crossSigningInfo.keys,
|
||||
keys: alice.client.crypto.crossSigningInfo.keys,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -336,8 +336,8 @@ describe("SAS verification", function() {
|
||||
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"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
@@ -390,8 +390,8 @@ describe("SAS verification", function() {
|
||||
beforeEach(async function() {
|
||||
[alice, bob] = await makeTestClients(
|
||||
[
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
{ userId: "@alice:example.com", deviceId: "Osborne2" },
|
||||
{ userId: "@bob:example.com", deviceId: "Dynabook" },
|
||||
],
|
||||
{
|
||||
verificationMethods: [verificationMethods.SAS],
|
||||
|
||||
@@ -14,10 +14,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {VerificationBase} from '../../../../src/crypto/verification/Base';
|
||||
import {CrossSigningInfo} from '../../../../src/crypto/CrossSigning';
|
||||
import {encodeBase64} from "../../../../src/crypto/olmlib";
|
||||
import {setupWebcrypto, teardownWebcrypto} from './util';
|
||||
import { VerificationBase } from '../../../../src/crypto/verification/Base';
|
||||
import { CrossSigningInfo } from '../../../../src/crypto/CrossSigning';
|
||||
import { encodeBase64 } from "../../../../src/crypto/olmlib";
|
||||
import { setupWebcrypto, teardownWebcrypto } from './util';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -48,18 +48,18 @@ describe("self-verifications", () => {
|
||||
storeCrossSigningKeyCache: jest.fn(),
|
||||
};
|
||||
|
||||
const _crossSigningInfo = new CrossSigningInfo(
|
||||
const crossSigningInfo = new CrossSigningInfo(
|
||||
userId,
|
||||
{},
|
||||
cacheCallbacks,
|
||||
);
|
||||
_crossSigningInfo.keys = {
|
||||
crossSigningInfo.keys = {
|
||||
master: { keys: { X: testKeyPub } },
|
||||
self_signing: { keys: { X: testKeyPub } },
|
||||
user_signing: { keys: { X: testKeyPub } },
|
||||
};
|
||||
|
||||
const _secretStorage = {
|
||||
const secretStorage = {
|
||||
request: jest.fn().mockReturnValue({
|
||||
promise: Promise.resolve(encodeBase64(testKey)),
|
||||
}),
|
||||
@@ -69,13 +69,13 @@ describe("self-verifications", () => {
|
||||
const restoreKeyBackupWithCache = jest.fn(() => Promise.resolve());
|
||||
|
||||
const client = {
|
||||
_crypto: {
|
||||
_crossSigningInfo,
|
||||
_secretStorage,
|
||||
crypto: {
|
||||
crossSigningInfo,
|
||||
secretStorage,
|
||||
storeSessionBackupPrivateKey,
|
||||
getSessionBackupPrivateKey: () => null,
|
||||
},
|
||||
requestSecret: _secretStorage.request.bind(_secretStorage),
|
||||
requestSecret: secretStorage.request.bind(secretStorage),
|
||||
getUserId: () => userId,
|
||||
getKeyBackupVersion: () => Promise.resolve({}),
|
||||
restoreKeyBackupWithCache,
|
||||
@@ -99,7 +99,7 @@ describe("self-verifications", () => {
|
||||
|
||||
/* We should request, and store, 3 cross signing keys and the key backup key */
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(3);
|
||||
expect(_secretStorage.request.mock.calls.length).toBe(4);
|
||||
expect(secretStorage.request.mock.calls.length).toBe(4);
|
||||
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1])
|
||||
.toEqual(testKey);
|
||||
|
||||
@@ -15,10 +15,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {TestClient} from '../../../TestClient';
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import { TestClient } from '../../../TestClient';
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import nodeCrypto from "crypto";
|
||||
import {logger} from '../../../../src/logger';
|
||||
import { logger } from '../../../../src/logger';
|
||||
|
||||
export async function makeTestClients(userInfos, options) {
|
||||
const clients = [];
|
||||
@@ -30,13 +30,13 @@ export async function makeTestClients(userInfos, options) {
|
||||
for (const [deviceId, msg] of Object.entries(devMap)) {
|
||||
if (deviceId in clientMap[userId]) {
|
||||
const event = new MatrixEvent({
|
||||
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
|
||||
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
|
||||
type: type,
|
||||
content: msg,
|
||||
});
|
||||
const client = clientMap[userId][deviceId];
|
||||
const decryptionPromise = event.isEncrypted() ?
|
||||
event.attemptDecryption(client._crypto) :
|
||||
event.attemptDecryption(client.crypto) :
|
||||
Promise.resolve();
|
||||
|
||||
decryptionPromise.then(
|
||||
@@ -49,9 +49,9 @@ export async function makeTestClients(userInfos, options) {
|
||||
};
|
||||
const sendEvent = function(room, type, content) {
|
||||
// make up a unique ID as the event ID
|
||||
const eventId = "$" + this.makeTxnId(); // eslint-disable-line babel/no-invalid-this
|
||||
const eventId = "$" + this.makeTxnId(); // eslint-disable-line @babel/no-invalid-this
|
||||
const rawEvent = {
|
||||
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
|
||||
sender: this.getUserId(), // eslint-disable-line @babel/no-invalid-this
|
||||
type: type,
|
||||
content: content,
|
||||
room_id: room,
|
||||
@@ -61,13 +61,13 @@ export async function makeTestClients(userInfos, options) {
|
||||
const event = new MatrixEvent(rawEvent);
|
||||
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
|
||||
unsigned: {
|
||||
transaction_id: this.makeTxnId(), // eslint-disable-line babel/no-invalid-this
|
||||
transaction_id: this.makeTxnId(), // eslint-disable-line @babel/no-invalid-this
|
||||
},
|
||||
}));
|
||||
|
||||
setImmediate(() => {
|
||||
for (const tc of clients) {
|
||||
if (tc.client === this) { // eslint-disable-line babel/no-invalid-this
|
||||
if (tc.client === this) { // eslint-disable-line @babel/no-invalid-this
|
||||
logger.log("sending remote echo!!");
|
||||
tc.client.emit("Room.timeline", remoteEcho);
|
||||
} else {
|
||||
@@ -76,7 +76,7 @@ export async function makeTestClients(userInfos, options) {
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.resolve({event_id: eventId});
|
||||
return Promise.resolve({ event_id: eventId });
|
||||
};
|
||||
|
||||
for (const userInfo of userInfos) {
|
||||
|
||||
@@ -13,13 +13,13 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
import {VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE} from
|
||||
import { VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE } from
|
||||
"../../../../src/crypto/verification/request/VerificationRequest";
|
||||
import {InRoomChannel} from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
import {ToDeviceChannel} from
|
||||
import { InRoomChannel } from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
import { ToDeviceChannel } from
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import {setupWebcrypto, teardownWebcrypto} from "./util";
|
||||
import { MatrixEvent } from "../../../../src/models/event";
|
||||
import { setupWebcrypto, teardownWebcrypto } from "./util";
|
||||
|
||||
function makeMockClient(userId, deviceId) {
|
||||
let counter = 1;
|
||||
@@ -40,7 +40,7 @@ function makeMockClient(userId, deviceId) {
|
||||
content,
|
||||
origin_server_ts: Date.now(),
|
||||
}));
|
||||
return Promise.resolve({event_id: eventId});
|
||||
return Promise.resolve({ event_id: eventId });
|
||||
},
|
||||
|
||||
sendToDevice(type, msgMap) {
|
||||
@@ -48,7 +48,7 @@ function makeMockClient(userId, deviceId) {
|
||||
const deviceMap = msgMap[userId];
|
||||
for (const deviceId of Object.keys(deviceMap)) {
|
||||
const content = deviceMap[deviceId];
|
||||
const event = new MatrixEvent({content, type});
|
||||
const event = new MatrixEvent({ content, type });
|
||||
deviceEvents[userId] = deviceEvents[userId] || {};
|
||||
deviceEvents[userId][deviceId] = deviceEvents[userId][deviceId] || [];
|
||||
deviceEvents[userId][deviceId].push(event);
|
||||
@@ -90,7 +90,7 @@ class MockVerifier {
|
||||
if (this._startEvent) {
|
||||
await this._channel.send(DONE_TYPE, {});
|
||||
} else {
|
||||
await this._channel.send(START_TYPE, {method: MOCK_METHOD});
|
||||
await this._channel.send(START_TYPE, { method: MOCK_METHOD });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ describe("verification request unit tests", function() {
|
||||
new ToDeviceChannel(bob1, bob1.getUserId(), ["device1", "device2"],
|
||||
ToDeviceChannel.makeTransactionId(), "device2"),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), bob1);
|
||||
const to = {userId: "@bob:matrix.tld", deviceId: "device2"};
|
||||
const to = { userId: "@bob:matrix.tld", deviceId: "device2" };
|
||||
const verifier = bob1Request.beginKeyVerification(MOCK_METHOD, to);
|
||||
expect(verifier).toBeInstanceOf(MockVerifier);
|
||||
await verifier.start();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {EventTimeline} from "../../src/models/event-timeline";
|
||||
import {RoomState} from "../../src/models/room-state";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import { RoomState } from "../../src/models/room-state";
|
||||
|
||||
function mockRoomStates(timeline) {
|
||||
timeline._startState = utils.mock(RoomState, "startState");
|
||||
@@ -15,7 +15,7 @@ describe("EventTimeline", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
// XXX: this is a horrid hack; should use sinon or something instead to mock
|
||||
const timelineSet = { room: { roomId: roomId }};
|
||||
const timelineSet = { room: { roomId: roomId } };
|
||||
timelineSet.room.getUnfilteredTimelineSet = function() {
|
||||
return timelineSet;
|
||||
};
|
||||
@@ -94,7 +94,6 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("neighbouringTimelines", function() {
|
||||
it("neighbouring timelines should start null", function() {
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(null);
|
||||
@@ -102,8 +101,8 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
it("setNeighbouringTimeline should set neighbour", function() {
|
||||
const prev = {a: "a"};
|
||||
const next = {b: "b"};
|
||||
const prev = { a: "a" };
|
||||
const next = { b: "b" };
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
timeline.setNeighbouringTimeline(next, EventTimeline.FORWARDS);
|
||||
expect(timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)).toBe(prev);
|
||||
@@ -111,8 +110,8 @@ describe("EventTimeline", function() {
|
||||
});
|
||||
|
||||
it("setNeighbouringTimeline should throw if called twice", function() {
|
||||
const prev = {a: "a"};
|
||||
const next = {b: "b"};
|
||||
const prev = { a: "a" };
|
||||
const next = { b: "b" };
|
||||
expect(function() {
|
||||
timeline.setNeighbouringTimeline(prev, EventTimeline.BACKWARDS);
|
||||
}).not.toThrow();
|
||||
@@ -278,7 +277,6 @@ describe("EventTimeline", function() {
|
||||
not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value for old events", function() {
|
||||
const events = [
|
||||
|
||||
@@ -15,8 +15,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from "../../src/logger";
|
||||
import {MatrixEvent} from "../../src/models/event";
|
||||
import { logger } from "../../src/logger";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
describe(".attemptDecryption", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {FilterComponent} from "../../src/filter-component";
|
||||
import {mkEvent} from '../test-utils';
|
||||
import { FilterComponent } from "../../src/filter-component";
|
||||
import { mkEvent } from '../test-utils';
|
||||
|
||||
describe("Filter Component", function() {
|
||||
describe("types", function() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {Filter} from "../../src/filter";
|
||||
import { Filter } from "../../src/filter";
|
||||
|
||||
describe("Filter", function() {
|
||||
const filterId = "f1lt3ring15g00d4ursoul";
|
||||
|
||||
@@ -15,9 +15,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from "../../src/logger";
|
||||
import {InteractiveAuth} from "../../src/interactive-auth";
|
||||
import {MatrixError} from "../../src/http-api";
|
||||
import { logger } from "../../src/logger";
|
||||
import { InteractiveAuth } from "../../src/interactive-auth";
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
|
||||
// Trivial client object to test interactive auth
|
||||
// (we do not need TestClient here)
|
||||
@@ -63,7 +63,7 @@ describe("InteractiveAuth", function() {
|
||||
});
|
||||
|
||||
// .. which should trigger a call here
|
||||
const requestRes = {"a": "b"};
|
||||
const requestRes = { "a": "b" };
|
||||
doRequest.mockImplementation(function(authData) {
|
||||
logger.log('cccc');
|
||||
expect(authData).toEqual({
|
||||
@@ -112,7 +112,7 @@ describe("InteractiveAuth", function() {
|
||||
});
|
||||
|
||||
// .. which should be followed by a call to stateUpdated
|
||||
const requestRes = {"a": "b"};
|
||||
const requestRes = { "a": "b" };
|
||||
stateUpdated.mockImplementation(function(stage) {
|
||||
expect(stage).toEqual("logintype");
|
||||
expect(ia.getSessionId()).toEqual("sessionId");
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {TestClient} from '../TestClient';
|
||||
import { TestClient } from '../TestClient';
|
||||
|
||||
describe('Login request', function() {
|
||||
let client;
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import {logger} from "../../src/logger";
|
||||
import {MatrixClient} from "../../src/client";
|
||||
import {Filter} from "../../src/filter";
|
||||
import { logger } from "../../src/logger";
|
||||
import { MatrixClient } from "../../src/client";
|
||||
import { Filter } from "../../src/filter";
|
||||
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
|
||||
import {
|
||||
EventType,
|
||||
RoomCreateTypeField,
|
||||
RoomType,
|
||||
UNSTABLE_MSC3088_ENABLED,
|
||||
UNSTABLE_MSC3088_PURPOSE,
|
||||
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
||||
} from "../../src/@types/event";
|
||||
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { Preset } from "../../src/@types/partials";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -144,12 +156,12 @@ describe("MatrixClient", function() {
|
||||
scheduler: scheduler,
|
||||
userId: userId,
|
||||
});
|
||||
// FIXME: We shouldn't be yanking _http like this.
|
||||
client._http = [
|
||||
// FIXME: We shouldn't be yanking http like this.
|
||||
client.http = [
|
||||
"authedRequest", "getContentUri", "request", "uploadContent",
|
||||
].reduce((r, k) => { r[k] = jest.fn(); return r; }, {});
|
||||
client._http.authedRequest.mockImplementation(httpReq);
|
||||
client._http.request.mockImplementation(httpReq);
|
||||
client.http.authedRequest.mockImplementation(httpReq);
|
||||
client.http.request.mockImplementation(httpReq);
|
||||
|
||||
// set reasonable working defaults
|
||||
acceptKeepalives = true;
|
||||
@@ -166,11 +178,165 @@ describe("MatrixClient", function() {
|
||||
// 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.mockImplementation(function() {
|
||||
client.http.authedRequest.mockImplementation(function() {
|
||||
return new Promise(() => {});
|
||||
});
|
||||
});
|
||||
|
||||
it("should create (unstable) file trees", async () => {
|
||||
const userId = "@test:example.org";
|
||||
const roomId = "!room:example.org";
|
||||
const roomName = "Test Tree";
|
||||
const mockRoom = {};
|
||||
const fn = jest.fn().mockImplementation((opts) => {
|
||||
expect(opts).toMatchObject({
|
||||
name: roomName,
|
||||
preset: Preset.PrivateChat,
|
||||
power_level_content_override: {
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users: {
|
||||
[userId]: 100,
|
||||
},
|
||||
},
|
||||
creation_content: {
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
initial_state: [
|
||||
{
|
||||
// We use `unstable` to ensure that the code is actually using the right identifier
|
||||
type: UNSTABLE_MSC3088_PURPOSE.unstable,
|
||||
state_key: UNSTABLE_MSC3089_TREE_SUBTYPE.unstable,
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: EventType.RoomEncryption,
|
||||
state_key: "",
|
||||
content: {
|
||||
algorithm: MEGOLM_ALGORITHM,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
return { room_id: roomId };
|
||||
});
|
||||
client.getUserId = () => userId;
|
||||
client.createRoom = fn;
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
const tree = await client.unstableCreateFileTree(roomName);
|
||||
expect(tree).toBeDefined();
|
||||
expect(tree.roomId).toEqual(roomId);
|
||||
expect(tree.room).toBe(mockRoom);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should get (unstable) file trees with valid state", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
});
|
||||
} else if (eventType === UNSTABLE_MSC3088_PURPOSE.unstable) {
|
||||
// We use `unstable` to ensure that the code is actually using the right identifier
|
||||
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||
expect(tree).toBeDefined();
|
||||
expect(tree.roomId).toEqual(roomId);
|
||||
expect(tree.room).toBe(mockRoom);
|
||||
});
|
||||
|
||||
it("should not get (unstable) file trees with invalid create contents", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[RoomCreateTypeField]: "org.example.not_space",
|
||||
},
|
||||
});
|
||||
} else if (eventType === UNSTABLE_MSC3088_PURPOSE.unstable) {
|
||||
// We use `unstable` to ensure that the code is actually using the right identifier
|
||||
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: true,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||
expect(tree).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not get (unstable) file trees with invalid purpose/subtype contents", async () => {
|
||||
const roomId = "!room:example.org";
|
||||
const mockRoom = {
|
||||
currentState: {
|
||||
getStateEvents: (eventType, stateKey) => {
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[RoomCreateTypeField]: RoomType.Space,
|
||||
},
|
||||
});
|
||||
} else if (eventType === UNSTABLE_MSC3088_PURPOSE.unstable) {
|
||||
expect(stateKey).toEqual(UNSTABLE_MSC3089_TREE_SUBTYPE.unstable);
|
||||
return new MatrixEvent({
|
||||
content: {
|
||||
[UNSTABLE_MSC3088_ENABLED.unstable]: false,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
throw new Error("Unexpected event type or state key");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
client.getRoom = (getRoomId) => {
|
||||
expect(getRoomId).toEqual(roomId);
|
||||
return mockRoom;
|
||||
};
|
||||
const tree = client.unstableGetFileTreeSpace(roomId);
|
||||
expect(tree).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should not POST /filter if a matching filter already exists", async function() {
|
||||
httpLookups = [];
|
||||
httpLookups.push(PUSH_RULES_RESPONSE);
|
||||
@@ -178,7 +344,7 @@ describe("MatrixClient", function() {
|
||||
const filterId = "ehfewf";
|
||||
store.getFilterIdByName.mockReturnValue(filterId);
|
||||
const filter = new Filter(0, filterId);
|
||||
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
|
||||
filter.setDefinition({ "room": { "timeline": { "limit": 8 } } });
|
||||
store.getFilter.mockReturnValue(filter);
|
||||
const syncPromise = new Promise((resolve, reject) => {
|
||||
client.on("sync", function syncListener(state) {
|
||||
@@ -521,4 +687,19 @@ describe("MatrixClient", function() {
|
||||
xit("should be able to peek into a room using peekInRoom", function(done) {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getPresence", function() {
|
||||
it("should send a presence HTTP GET", function() {
|
||||
httpLookups = [{
|
||||
method: "GET",
|
||||
path: `/presence/${encodeURIComponent(userId)}/status`,
|
||||
data: {
|
||||
"presence": "unavailable",
|
||||
"last_active_ago": 420845,
|
||||
},
|
||||
}];
|
||||
client.getPresence(userId);
|
||||
expect(httpLookups.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
Copyright 2021 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 { MatrixClient } from "../../../src";
|
||||
import { Room } from "../../../src/models/room";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import { UNSTABLE_MSC3089_BRANCH } from "../../../src/@types/event";
|
||||
import { EventTimelineSet } from "../../../src/models/event-timeline-set";
|
||||
import { EventTimeline } from "../../../src/models/event-timeline";
|
||||
import { MSC3089Branch } from "../../../src/models/MSC3089Branch";
|
||||
|
||||
describe("MSC3089Branch", () => {
|
||||
let client: MatrixClient;
|
||||
// @ts-ignore - TS doesn't know that this is a type
|
||||
let indexEvent: MatrixEvent;
|
||||
let branch: MSC3089Branch;
|
||||
|
||||
const branchRoomId = "!room:example.org";
|
||||
const fileEventId = "$file";
|
||||
|
||||
const staticTimelineSets = {} as EventTimelineSet;
|
||||
const staticRoom = {
|
||||
getUnfilteredTimelineSet: () => staticTimelineSets,
|
||||
} as any as Room; // partial
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Use utility functions to create test rooms and clients
|
||||
client = <MatrixClient>{
|
||||
getRoom: (roomId: string) => {
|
||||
if (roomId === branchRoomId) {
|
||||
return staticRoom;
|
||||
} else {
|
||||
throw new Error("Unexpected fetch for unknown room");
|
||||
}
|
||||
},
|
||||
};
|
||||
indexEvent = {
|
||||
getRoomId: () => branchRoomId,
|
||||
getStateKey: () => fileEventId,
|
||||
};
|
||||
branch = new MSC3089Branch(client, indexEvent);
|
||||
});
|
||||
|
||||
it('should know the file event ID', () => {
|
||||
expect(branch.id).toEqual(fileEventId);
|
||||
});
|
||||
|
||||
it('should know if the file is active or not', () => {
|
||||
indexEvent.getContent = () => ({});
|
||||
expect(branch.isActive).toBe(false);
|
||||
indexEvent.getContent = () => ({ active: false });
|
||||
expect(branch.isActive).toBe(false);
|
||||
indexEvent.getContent = () => ({ active: true });
|
||||
expect(branch.isActive).toBe(true);
|
||||
indexEvent.getContent = () => ({ active: "true" }); // invalid boolean, inactive
|
||||
expect(branch.isActive).toBe(false);
|
||||
});
|
||||
|
||||
it('should be able to delete the file', async () => {
|
||||
const stateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
|
||||
expect(content).toMatchObject({});
|
||||
expect(content['active']).toBeUndefined();
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
client.sendStateEvent = stateFn;
|
||||
|
||||
const redactFn = jest.fn().mockImplementation((roomId: string, eventId: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventId).toEqual(fileEventId);
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
client.redactEvent = redactFn;
|
||||
|
||||
await branch.delete();
|
||||
|
||||
expect(stateFn).toHaveBeenCalledTimes(1);
|
||||
expect(redactFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should know its name', async () => {
|
||||
const name = "My File.txt";
|
||||
indexEvent.getContent = () => ({ active: true, name: name });
|
||||
|
||||
const res = branch.getName();
|
||||
|
||||
expect(res).toEqual(name);
|
||||
});
|
||||
|
||||
it('should be able to change its name', async () => {
|
||||
const name = "My File.txt";
|
||||
indexEvent.getContent = () => ({ active: true, retained: true });
|
||||
const stateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(branchRoomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test that we're definitely using the unstable value
|
||||
expect(content).toMatchObject({
|
||||
retained: true, // canary for copying state
|
||||
active: true,
|
||||
name: name,
|
||||
});
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
client.sendStateEvent = stateFn;
|
||||
|
||||
await branch.setName(name);
|
||||
|
||||
expect(stateFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should be able to return event information', async () => {
|
||||
const mxcLatter = "example.org/file";
|
||||
const fileContent = { isFile: "not quite", url: "mxc://" + mxcLatter };
|
||||
const eventsArr = [
|
||||
{ getId: () => "$not-file", getContent: () => ({}) },
|
||||
{ getId: () => fileEventId, getContent: () => ({ file: fileContent }) },
|
||||
];
|
||||
client.getEventTimeline = () => Promise.resolve({
|
||||
getEvents: () => eventsArr,
|
||||
}) as any as Promise<EventTimeline>; // partial
|
||||
client.mxcUrlToHttp = (mxc: string) => {
|
||||
expect(mxc).toEqual("mxc://" + mxcLatter);
|
||||
return `https://example.org/_matrix/media/v1/download/${mxcLatter}`;
|
||||
};
|
||||
client.decryptEventIfNeeded = () => Promise.resolve();
|
||||
|
||||
const res = await branch.getFileInfo();
|
||||
expect(res).toBeDefined();
|
||||
expect(res).toMatchObject({
|
||||
info: fileContent,
|
||||
// Escape regex from MDN guides: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
|
||||
httpUrl: expect.stringMatching(`.+${mxcLatter.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,963 @@
|
||||
/*
|
||||
Copyright 2021 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 { MatrixClient } from "../../../src";
|
||||
import { Room } from "../../../src/models/room";
|
||||
import { MatrixEvent } from "../../../src/models/event";
|
||||
import { EventType, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../../../src/@types/event";
|
||||
import {
|
||||
DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
MSC3089TreeSpace,
|
||||
TreePermissions,
|
||||
} from "../../../src/models/MSC3089TreeSpace";
|
||||
import { DEFAULT_ALPHABET } from "../../../src/utils";
|
||||
import { MockBlob } from "../../MockBlob";
|
||||
import { MatrixError } from "../../../src/http-api";
|
||||
|
||||
describe("MSC3089TreeSpace", () => {
|
||||
let client: MatrixClient;
|
||||
let room: Room;
|
||||
let tree: MSC3089TreeSpace;
|
||||
const roomId = "!tree:localhost";
|
||||
const targetUser = "@target:example.org";
|
||||
|
||||
let powerLevels;
|
||||
|
||||
beforeEach(() => {
|
||||
// TODO: Use utility functions to create test rooms and clients
|
||||
client = <MatrixClient>{
|
||||
getRoom: (fetchRoomId: string) => {
|
||||
if (fetchRoomId === roomId) {
|
||||
return room;
|
||||
} else {
|
||||
throw new Error("Unexpected fetch for unknown room");
|
||||
}
|
||||
},
|
||||
};
|
||||
room = <Room>{
|
||||
currentState: {
|
||||
getStateEvents: (evType: EventType, stateKey: string) => {
|
||||
if (evType === EventType.RoomPowerLevels && stateKey === "") {
|
||||
return powerLevels;
|
||||
} else {
|
||||
throw new Error("Accessed unexpected state event type or key");
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
tree = new MSC3089TreeSpace(client, roomId);
|
||||
makePowerLevels(DEFAULT_TREE_POWER_LEVELS_TEMPLATE);
|
||||
});
|
||||
|
||||
function makePowerLevels(content: any) {
|
||||
powerLevels = new MatrixEvent({
|
||||
type: EventType.RoomPowerLevels,
|
||||
state_key: "",
|
||||
sender: "@creator:localhost",
|
||||
event_id: "$powerlevels",
|
||||
room_id: roomId,
|
||||
content: content,
|
||||
});
|
||||
}
|
||||
|
||||
it('should populate the room reference', () => {
|
||||
expect(tree.room).toBe(room);
|
||||
});
|
||||
|
||||
it('should proxy the ID member to room ID', () => {
|
||||
expect(tree.id).toEqual(tree.roomId);
|
||||
expect(tree.id).toEqual(roomId);
|
||||
});
|
||||
|
||||
it('should support setting the name of the space', async () => {
|
||||
const newName = "NEW NAME";
|
||||
const fn = jest.fn()
|
||||
.mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
expect(stateRoomId).toEqual(roomId);
|
||||
expect(eventType).toEqual(EventType.RoomName);
|
||||
expect(stateKey).toEqual("");
|
||||
expect(content).toMatchObject({ name: newName });
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.sendStateEvent = fn;
|
||||
await tree.setName(newName);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should support inviting users to the space', async () => {
|
||||
const target = targetUser;
|
||||
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||
expect(inviteRoomId).toEqual(roomId);
|
||||
expect(userId).toEqual(target);
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.invite = fn;
|
||||
await tree.invite(target, false, false);
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should retry invites to the space', async () => {
|
||||
const target = targetUser;
|
||||
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||
expect(inviteRoomId).toEqual(roomId);
|
||||
expect(userId).toEqual(target);
|
||||
if (fn.mock.calls.length === 1) return Promise.reject(new Error("Sample Failure"));
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.invite = fn;
|
||||
await tree.invite(target, false, false);
|
||||
expect(fn).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should not retry invite permission errors', async () => {
|
||||
const target = targetUser;
|
||||
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||
expect(inviteRoomId).toEqual(roomId);
|
||||
expect(userId).toEqual(target);
|
||||
return Promise.reject(new MatrixError({ errcode: "M_FORBIDDEN", error: "Sample Failure" }));
|
||||
});
|
||||
client.invite = fn;
|
||||
try {
|
||||
await tree.invite(target, false, false);
|
||||
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.errcode).toEqual("M_FORBIDDEN");
|
||||
}
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should invite to subspaces', async () => {
|
||||
const target = targetUser;
|
||||
const fn = jest.fn().mockImplementation((inviteRoomId: string, userId: string) => {
|
||||
expect(inviteRoomId).toEqual(roomId);
|
||||
expect(userId).toEqual(target);
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.invite = fn;
|
||||
tree.getDirectories = () => [
|
||||
// Bare minimum overrides. We proxy to our mock function manually so we can
|
||||
// count the calls, not to ensure accuracy. The invite function behaving correctly
|
||||
// is covered by another test.
|
||||
{ invite: (userId) => fn(tree.roomId, userId) } as MSC3089TreeSpace,
|
||||
{ invite: (userId) => fn(tree.roomId, userId) } as MSC3089TreeSpace,
|
||||
{ invite: (userId) => fn(tree.roomId, userId) } as MSC3089TreeSpace,
|
||||
];
|
||||
|
||||
await tree.invite(target, true, false);
|
||||
expect(fn).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
it('should share keys with invitees', async () => {
|
||||
const target = targetUser;
|
||||
const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => {
|
||||
expect(inviteRoomId).toEqual(roomId);
|
||||
expect(userIds).toMatchObject([target]);
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.invite = () => Promise.resolve(); // we're not testing this here - see other tests
|
||||
client.sendSharedHistoryKeys = sendKeysFn;
|
||||
|
||||
// Mock the history check as best as possible
|
||||
const historyVis = "shared";
|
||||
const historyFn = jest.fn().mockImplementation((eventType: string, stateKey?: string) => {
|
||||
// We're not expecting a super rigid test: the function that calls this internally isn't
|
||||
// really being tested here.
|
||||
expect(eventType).toEqual(EventType.RoomHistoryVisibility);
|
||||
expect(stateKey).toEqual("");
|
||||
return { getContent: () => ({ history_visibility: historyVis }) }; // eslint-disable-line camelcase
|
||||
});
|
||||
room.currentState.getStateEvents = historyFn;
|
||||
|
||||
// Note: inverse test is implicit from other tests, which disable the call stack of this
|
||||
// test in order to pass.
|
||||
await tree.invite(target, false, true);
|
||||
expect(sendKeysFn).toHaveBeenCalledTimes(1);
|
||||
expect(historyFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should not share keys with invitees if inappropriate history visibility', async () => {
|
||||
const target = targetUser;
|
||||
const sendKeysFn = jest.fn().mockImplementation((inviteRoomId: string, userIds: string[]) => {
|
||||
expect(inviteRoomId).toEqual(roomId);
|
||||
expect(userIds).toMatchObject([target]);
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.invite = () => Promise.resolve(); // we're not testing this here - see other tests
|
||||
client.sendSharedHistoryKeys = sendKeysFn;
|
||||
|
||||
const historyVis = "joined"; // NOTE: Changed.
|
||||
const historyFn = jest.fn().mockImplementation((eventType: string, stateKey?: string) => {
|
||||
expect(eventType).toEqual(EventType.RoomHistoryVisibility);
|
||||
expect(stateKey).toEqual("");
|
||||
return { getContent: () => ({ history_visibility: historyVis }) }; // eslint-disable-line camelcase
|
||||
});
|
||||
room.currentState.getStateEvents = historyFn;
|
||||
|
||||
await tree.invite(target, false, true);
|
||||
expect(sendKeysFn).toHaveBeenCalledTimes(0);
|
||||
expect(historyFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
async function evaluatePowerLevels(pls: any, role: TreePermissions, expectedPl: number) {
|
||||
makePowerLevels(pls);
|
||||
const fn = jest.fn()
|
||||
.mockImplementation((stateRoomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
expect(stateRoomId).toEqual(roomId);
|
||||
expect(eventType).toEqual(EventType.RoomPowerLevels);
|
||||
expect(stateKey).toEqual("");
|
||||
expect(content).toMatchObject({
|
||||
...pls,
|
||||
users: {
|
||||
[targetUser]: expectedPl,
|
||||
},
|
||||
});
|
||||
return Promise.resolve();
|
||||
});
|
||||
client.sendStateEvent = fn;
|
||||
await tree.setPermissions(targetUser, role);
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
}
|
||||
|
||||
it('should support setting Viewer permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users_default: 1024,
|
||||
}, TreePermissions.Viewer, 1024);
|
||||
});
|
||||
|
||||
it('should support setting Editor permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
events_default: 1024,
|
||||
}, TreePermissions.Editor, 1024);
|
||||
});
|
||||
|
||||
it('should support setting Owner permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
events: {
|
||||
[EventType.RoomPowerLevels]: 1024,
|
||||
},
|
||||
}, TreePermissions.Owner, 1024);
|
||||
});
|
||||
|
||||
it('should support demoting permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
users_default: 1024,
|
||||
users: {
|
||||
[targetUser]: 2222,
|
||||
},
|
||||
}, TreePermissions.Viewer, 1024);
|
||||
});
|
||||
|
||||
it('should support promoting permissions', () => {
|
||||
return evaluatePowerLevels({
|
||||
...DEFAULT_TREE_POWER_LEVELS_TEMPLATE,
|
||||
events_default: 1024,
|
||||
users: {
|
||||
[targetUser]: 5,
|
||||
},
|
||||
}, TreePermissions.Editor, 1024);
|
||||
});
|
||||
|
||||
it('should support defaults: Viewer', () => {
|
||||
return evaluatePowerLevels({}, TreePermissions.Viewer, 0);
|
||||
});
|
||||
|
||||
it('should support defaults: Editor', () => {
|
||||
return evaluatePowerLevels({}, TreePermissions.Editor, 50);
|
||||
});
|
||||
|
||||
it('should support defaults: Owner', () => {
|
||||
return evaluatePowerLevels({}, TreePermissions.Owner, 100);
|
||||
});
|
||||
|
||||
it('should create subdirectories', async () => {
|
||||
const subspaceName = "subdirectory";
|
||||
const subspaceId = "!subspace:localhost";
|
||||
const domain = "domain.example.com";
|
||||
client.getRoom = (roomId: string) => {
|
||||
if (roomId === tree.roomId) {
|
||||
return tree.room;
|
||||
} else if (roomId === subspaceId) {
|
||||
return {} as Room; // we don't need anything important off of this
|
||||
} else {
|
||||
throw new Error("Unexpected getRoom call");
|
||||
}
|
||||
};
|
||||
client.getDomain = () => domain;
|
||||
const createFn = jest.fn().mockImplementation(async (name: string) => {
|
||||
expect(name).toEqual(subspaceName);
|
||||
return new MSC3089TreeSpace(client, subspaceId);
|
||||
});
|
||||
const sendStateFn = jest.fn()
|
||||
.mockImplementation(async (roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
expect([tree.roomId, subspaceId]).toContain(roomId);
|
||||
if (roomId === subspaceId) {
|
||||
expect(eventType).toEqual(EventType.SpaceParent);
|
||||
expect(stateKey).toEqual(tree.roomId);
|
||||
} else {
|
||||
expect(eventType).toEqual(EventType.SpaceChild);
|
||||
expect(stateKey).toEqual(subspaceId);
|
||||
}
|
||||
expect(content).toMatchObject({ via: [domain] });
|
||||
|
||||
// return value not used
|
||||
});
|
||||
client.unstableCreateFileTree = createFn;
|
||||
client.sendStateEvent = sendStateFn;
|
||||
|
||||
const directory = await tree.createDirectory(subspaceName);
|
||||
expect(directory).toBeDefined();
|
||||
expect(directory).not.toBeNull();
|
||||
expect(directory).not.toBe(tree);
|
||||
expect(directory.roomId).toEqual(subspaceId);
|
||||
expect(createFn).toHaveBeenCalledTimes(1);
|
||||
expect(sendStateFn).toHaveBeenCalledTimes(2);
|
||||
|
||||
const content = expect.objectContaining({ via: [domain] });
|
||||
expect(sendStateFn).toHaveBeenCalledWith(subspaceId, EventType.SpaceParent, content, tree.roomId);
|
||||
expect(sendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, content, subspaceId);
|
||||
});
|
||||
|
||||
it('should find subdirectories', () => {
|
||||
const firstChildRoom = "!one:example.org";
|
||||
const secondChildRoom = "!two:example.org";
|
||||
const thirdChildRoom = "!three:example.org"; // to ensure it doesn't end up in the subdirectories
|
||||
room.currentState = {
|
||||
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
||||
expect(eventType).toEqual(EventType.SpaceChild);
|
||||
expect(stateKey).toBeUndefined();
|
||||
return [
|
||||
// Partial implementations of Room
|
||||
{ getStateKey: () => firstChildRoom },
|
||||
{ getStateKey: () => secondChildRoom },
|
||||
{ getStateKey: () => thirdChildRoom },
|
||||
];
|
||||
},
|
||||
};
|
||||
client.getRoom = () => ({} as Room); // to appease the TreeSpace constructor
|
||||
|
||||
const getFn = jest.fn().mockImplementation((roomId: string) => {
|
||||
if (roomId === thirdChildRoom) {
|
||||
throw new Error("Mock not-a-space room case called (expected)");
|
||||
}
|
||||
expect([firstChildRoom, secondChildRoom]).toContain(roomId);
|
||||
return new MSC3089TreeSpace(client, roomId);
|
||||
});
|
||||
client.unstableGetFileTreeSpace = getFn;
|
||||
|
||||
const subdirectories = tree.getDirectories();
|
||||
expect(subdirectories).toBeDefined();
|
||||
expect(subdirectories.length).toBe(2);
|
||||
expect(subdirectories[0].roomId).toBe(firstChildRoom);
|
||||
expect(subdirectories[1].roomId).toBe(secondChildRoom);
|
||||
expect(getFn).toHaveBeenCalledTimes(3);
|
||||
expect(getFn).toHaveBeenCalledWith(firstChildRoom);
|
||||
expect(getFn).toHaveBeenCalledWith(secondChildRoom);
|
||||
expect(getFn).toHaveBeenCalledWith(thirdChildRoom); // check to make sure it tried
|
||||
});
|
||||
|
||||
it('should find specific directories', () => {
|
||||
client.getRoom = () => ({} as Room); // to appease the TreeSpace constructor
|
||||
|
||||
// Only mocking used API
|
||||
const firstSubdirectory = { roomId: "!first:example.org" } as any as MSC3089TreeSpace;
|
||||
const searchedSubdirectory = { roomId: "!find_me:example.org" } as any as MSC3089TreeSpace;
|
||||
const thirdSubdirectory = { roomId: "!third:example.org" } as any as MSC3089TreeSpace;
|
||||
tree.getDirectories = () => [firstSubdirectory, searchedSubdirectory, thirdSubdirectory];
|
||||
|
||||
let result = tree.getDirectory(searchedSubdirectory.roomId);
|
||||
expect(result).toBe(searchedSubdirectory);
|
||||
|
||||
result = tree.getDirectory("not a subdirectory");
|
||||
expect(result).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should be able to delete itself', async () => {
|
||||
const delete1 = jest.fn().mockImplementation(() => Promise.resolve());
|
||||
const subdir1 = { delete: delete1 } as any as MSC3089TreeSpace; // mock tested bits
|
||||
|
||||
const delete2 = jest.fn().mockImplementation(() => Promise.resolve());
|
||||
const subdir2 = { delete: delete2 } as any as MSC3089TreeSpace; // mock tested bits
|
||||
|
||||
const joinMemberId = "@join:example.org";
|
||||
const knockMemberId = "@knock:example.org";
|
||||
const inviteMemberId = "@invite:example.org";
|
||||
const leaveMemberId = "@leave:example.org";
|
||||
const banMemberId = "@ban:example.org";
|
||||
const selfUserId = "@self:example.org";
|
||||
|
||||
tree.getDirectories = () => [subdir1, subdir2];
|
||||
room.currentState = {
|
||||
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
||||
expect(eventType).toEqual(EventType.RoomMember);
|
||||
expect(stateKey).toBeUndefined();
|
||||
return [
|
||||
// Partial implementations
|
||||
{ getContent: () => ({ membership: "join" }), getStateKey: () => joinMemberId },
|
||||
{ getContent: () => ({ membership: "knock" }), getStateKey: () => knockMemberId },
|
||||
{ getContent: () => ({ membership: "invite" }), getStateKey: () => inviteMemberId },
|
||||
{ getContent: () => ({ membership: "leave" }), getStateKey: () => leaveMemberId },
|
||||
{ getContent: () => ({ membership: "ban" }), getStateKey: () => banMemberId },
|
||||
|
||||
// ensure we don't kick ourselves
|
||||
{ getContent: () => ({ membership: "join" }), getStateKey: () => selfUserId },
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
// These two functions are tested by input expectations, so no expectations in the function bodies
|
||||
const kickFn = jest.fn().mockImplementation((userId) => Promise.resolve());
|
||||
const leaveFn = jest.fn().mockImplementation(() => Promise.resolve());
|
||||
client.kick = kickFn;
|
||||
client.leave = leaveFn;
|
||||
client.getUserId = () => selfUserId;
|
||||
|
||||
await tree.delete();
|
||||
|
||||
expect(delete1).toHaveBeenCalledTimes(1);
|
||||
expect(delete2).toHaveBeenCalledTimes(1);
|
||||
expect(kickFn).toHaveBeenCalledTimes(3);
|
||||
expect(kickFn).toHaveBeenCalledWith(tree.roomId, joinMemberId, expect.any(String));
|
||||
expect(kickFn).toHaveBeenCalledWith(tree.roomId, knockMemberId, expect.any(String));
|
||||
expect(kickFn).toHaveBeenCalledWith(tree.roomId, inviteMemberId, expect.any(String));
|
||||
expect(leaveFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('get and set order', () => {
|
||||
// Danger: these are partial implementations for testing purposes only
|
||||
|
||||
// @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important
|
||||
let childState: { [roomId: string]: MatrixEvent[] } = {};
|
||||
// @ts-ignore - "MatrixEvent is a value but used as a type", which is true but not important
|
||||
let parentState: MatrixEvent[] = [];
|
||||
let parentRoom: Room;
|
||||
let childTrees: MSC3089TreeSpace[];
|
||||
let rooms: { [roomId: string]: Room };
|
||||
let clientSendStateFn: jest.MockedFunction<typeof client.sendStateEvent>;
|
||||
const staticDomain = "static.example.org";
|
||||
|
||||
function addSubspace(roomId: string, createTs?: number, order?: string) {
|
||||
const content = {
|
||||
via: [staticDomain],
|
||||
};
|
||||
if (order) content['order'] = order;
|
||||
parentState.push({
|
||||
getType: () => EventType.SpaceChild,
|
||||
getStateKey: () => roomId,
|
||||
getContent: () => content,
|
||||
});
|
||||
childState[roomId] = [
|
||||
{
|
||||
getType: () => EventType.SpaceParent,
|
||||
getStateKey: () => tree.roomId,
|
||||
getContent: () => ({
|
||||
via: [staticDomain],
|
||||
}),
|
||||
},
|
||||
];
|
||||
if (createTs) {
|
||||
childState[roomId].push({
|
||||
getType: () => EventType.RoomCreate,
|
||||
getStateKey: () => "",
|
||||
getContent: () => ({}),
|
||||
getTs: () => createTs,
|
||||
});
|
||||
}
|
||||
rooms[roomId] = makeMockChildRoom(roomId);
|
||||
childTrees.push(new MSC3089TreeSpace(client, roomId));
|
||||
}
|
||||
|
||||
function expectOrder(childRoomId: string, order: number) {
|
||||
const child = childTrees.find(c => c.roomId === childRoomId);
|
||||
expect(child).toBeDefined();
|
||||
expect(child.getOrder()).toEqual(order);
|
||||
}
|
||||
|
||||
function makeMockChildRoom(roomId: string): Room {
|
||||
return {
|
||||
currentState: {
|
||||
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
||||
expect([EventType.SpaceParent, EventType.RoomCreate]).toContain(eventType);
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return childState[roomId].find(e => e.getType() === EventType.RoomCreate);
|
||||
} else {
|
||||
expect(stateKey).toBeUndefined();
|
||||
return childState[roomId].filter(e => e.getType() === eventType);
|
||||
}
|
||||
},
|
||||
},
|
||||
} as Room; // partial
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
childState = {};
|
||||
parentState = [];
|
||||
parentRoom = {
|
||||
...tree.room,
|
||||
roomId: tree.roomId,
|
||||
currentState: {
|
||||
getStateEvents: (eventType: EventType, stateKey?: string) => {
|
||||
expect([
|
||||
EventType.SpaceChild,
|
||||
EventType.RoomCreate,
|
||||
EventType.SpaceParent,
|
||||
]).toContain(eventType);
|
||||
|
||||
if (eventType === EventType.RoomCreate) {
|
||||
expect(stateKey).toEqual("");
|
||||
return parentState.filter(e => e.getType() === EventType.RoomCreate)[0];
|
||||
} else {
|
||||
if (stateKey !== undefined) {
|
||||
expect(Object.keys(rooms)).toContain(stateKey);
|
||||
expect(stateKey).not.toEqual(tree.roomId);
|
||||
return parentState.find(e => e.getType() === eventType && e.getStateKey() === stateKey);
|
||||
} // else fine
|
||||
return parentState.filter(e => e.getType() === eventType);
|
||||
}
|
||||
},
|
||||
},
|
||||
} as Room;
|
||||
childTrees = [];
|
||||
rooms = {};
|
||||
rooms[tree.roomId] = parentRoom;
|
||||
(<any>tree).room = parentRoom; // override readonly
|
||||
client.getRoom = (r) => rooms[r];
|
||||
|
||||
clientSendStateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: EventType, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(tree.roomId);
|
||||
expect(eventType).toEqual(EventType.SpaceChild);
|
||||
expect(content).toMatchObject(expect.objectContaining({
|
||||
via: expect.any(Array),
|
||||
order: expect.any(String),
|
||||
}));
|
||||
expect(Object.keys(rooms)).toContain(stateKey);
|
||||
expect(stateKey).not.toEqual(tree.roomId);
|
||||
|
||||
const stateEvent = parentState.find(e => e.getType() === eventType && e.getStateKey() === stateKey);
|
||||
expect(stateEvent).toBeDefined();
|
||||
stateEvent.getContent = () => content;
|
||||
|
||||
return Promise.resolve(); // return value not used
|
||||
});
|
||||
client.sendStateEvent = clientSendStateFn;
|
||||
});
|
||||
|
||||
it('should know when something is top level', () => {
|
||||
const a = "!a:example.org";
|
||||
addSubspace(a);
|
||||
|
||||
expect(tree.isTopLevel).toBe(true);
|
||||
expect(childTrees[0].isTopLevel).toBe(false); // a bit of a hack to get at this, but it's fine
|
||||
});
|
||||
|
||||
it('should return -1 for top level spaces', () => {
|
||||
// The tree is what we've defined as top level, so it should work
|
||||
expect(tree.getOrder()).toEqual(-1);
|
||||
});
|
||||
|
||||
it('should throw when setting an order at the top level space', async () => {
|
||||
try {
|
||||
// The tree is what we've defined as top level, so it should work
|
||||
await tree.setOrder(2);
|
||||
|
||||
// noinspection ExceptionCaughtLocallyJS
|
||||
throw new Error("Failed to fail");
|
||||
} catch (e) {
|
||||
expect(e.message).toEqual("Cannot set order of top level spaces currently");
|
||||
}
|
||||
});
|
||||
|
||||
it('should return a stable order for unordered children', () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(c, 3);
|
||||
addSubspace(b, 2);
|
||||
addSubspace(a, 1);
|
||||
|
||||
expectOrder(a, 0);
|
||||
expectOrder(b, 1);
|
||||
expectOrder(c, 2);
|
||||
});
|
||||
|
||||
it('should return a stable order for ordered children', () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(a, 1, "Z");
|
||||
addSubspace(b, 2, "Y");
|
||||
addSubspace(c, 3, "X");
|
||||
|
||||
expectOrder(c, 0);
|
||||
expectOrder(b, 1);
|
||||
expectOrder(a, 2);
|
||||
});
|
||||
|
||||
it('should return a stable order for partially ordered children', () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
const d = "!d:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(a, 1);
|
||||
addSubspace(b, 2);
|
||||
addSubspace(c, 3, "Y");
|
||||
addSubspace(d, 4, "X");
|
||||
|
||||
expectOrder(d, 0);
|
||||
expectOrder(c, 1);
|
||||
expectOrder(b, 3); // note order diff due to room ID comparison expectation
|
||||
expectOrder(a, 2);
|
||||
});
|
||||
|
||||
it('should return a stable order if the create event timestamps are the same', () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(c, 3);
|
||||
addSubspace(b, 3); // same as C
|
||||
addSubspace(a, 3); // same as C
|
||||
|
||||
expectOrder(a, 0);
|
||||
expectOrder(b, 1);
|
||||
expectOrder(c, 2);
|
||||
});
|
||||
|
||||
it('should return a stable order if there are no known create events', () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(c);
|
||||
addSubspace(b);
|
||||
addSubspace(a);
|
||||
|
||||
expectOrder(a, 0);
|
||||
expectOrder(b, 1);
|
||||
expectOrder(c, 2);
|
||||
});
|
||||
|
||||
// XXX: These tests rely on `getOrder()` re-calculating and not caching values.
|
||||
|
||||
it('should allow reordering within unordered children', async () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(c, 3);
|
||||
addSubspace(b, 2);
|
||||
addSubspace(a, 1);
|
||||
|
||||
// Order of this state is validated by other tests.
|
||||
|
||||
const treeA = childTrees.find(c => c.roomId === a);
|
||||
expect(treeA).toBeDefined();
|
||||
await treeA.setOrder(1);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(3);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
|
||||
// Because of how the reordering works (maintain stable ordering before moving), we end up calling this
|
||||
// function twice for the same room.
|
||||
order: DEFAULT_ALPHABET[0],
|
||||
}), a);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: DEFAULT_ALPHABET[1],
|
||||
}), b);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: DEFAULT_ALPHABET[2],
|
||||
}), a);
|
||||
expectOrder(a, 1);
|
||||
expectOrder(b, 0);
|
||||
expectOrder(c, 2);
|
||||
});
|
||||
|
||||
it('should allow reordering within ordered children', async () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(c, 3, "Z");
|
||||
addSubspace(b, 2, "X");
|
||||
addSubspace(a, 1, "V");
|
||||
|
||||
// Order of this state is validated by other tests.
|
||||
|
||||
const treeA = childTrees.find(c => c.roomId === a);
|
||||
expect(treeA).toBeDefined();
|
||||
await treeA.setOrder(1);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: 'Y',
|
||||
}), a);
|
||||
expectOrder(a, 1);
|
||||
expectOrder(b, 0);
|
||||
expectOrder(c, 2);
|
||||
});
|
||||
|
||||
it('should allow reordering within partially ordered children', async () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
const d = "!d:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(a, 1);
|
||||
addSubspace(b, 2);
|
||||
addSubspace(c, 3, "Y");
|
||||
addSubspace(d, 4, "W");
|
||||
|
||||
// Order of this state is validated by other tests.
|
||||
|
||||
const treeA = childTrees.find(c => c.roomId === a);
|
||||
expect(treeA).toBeDefined();
|
||||
await treeA.setOrder(2);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: 'Z',
|
||||
}), a);
|
||||
expectOrder(a, 2);
|
||||
expectOrder(b, 3);
|
||||
expectOrder(c, 1);
|
||||
expectOrder(d, 0);
|
||||
});
|
||||
|
||||
it('should support moving upwards', async () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
const d = "!d:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(d, 4, "Z");
|
||||
addSubspace(c, 3, "X");
|
||||
addSubspace(b, 2, "V");
|
||||
addSubspace(a, 1, "T");
|
||||
|
||||
// Order of this state is validated by other tests.
|
||||
|
||||
const treeB = childTrees.find(c => c.roomId === b);
|
||||
expect(treeB).toBeDefined();
|
||||
await treeB.setOrder(2);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: 'Y',
|
||||
}), b);
|
||||
expectOrder(a, 0);
|
||||
expectOrder(b, 2);
|
||||
expectOrder(c, 1);
|
||||
expectOrder(d, 3);
|
||||
});
|
||||
|
||||
it('should support moving downwards', async () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
const d = "!d:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(d, 4, "Z");
|
||||
addSubspace(c, 3, "X");
|
||||
addSubspace(b, 2, "V");
|
||||
addSubspace(a, 1, "T");
|
||||
|
||||
// Order of this state is validated by other tests.
|
||||
|
||||
const treeC = childTrees.find(ch => ch.roomId === c);
|
||||
expect(treeC).toBeDefined();
|
||||
await treeC.setOrder(1);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(1);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: 'U',
|
||||
}), c);
|
||||
expectOrder(a, 0);
|
||||
expectOrder(b, 2);
|
||||
expectOrder(c, 1);
|
||||
expectOrder(d, 3);
|
||||
});
|
||||
|
||||
it('should support moving over the partial ordering boundary', async () => {
|
||||
const a = "!a:example.org";
|
||||
const b = "!b:example.org";
|
||||
const c = "!c:example.org";
|
||||
const d = "!d:example.org";
|
||||
|
||||
// Add in reverse order to make sure it gets ordered correctly
|
||||
addSubspace(d, 4);
|
||||
addSubspace(c, 3);
|
||||
addSubspace(b, 2, "V");
|
||||
addSubspace(a, 1, "T");
|
||||
|
||||
// Order of this state is validated by other tests.
|
||||
|
||||
const treeB = childTrees.find(ch => ch.roomId === b);
|
||||
expect(treeB).toBeDefined();
|
||||
await treeB.setOrder(2);
|
||||
|
||||
expect(clientSendStateFn).toHaveBeenCalledTimes(2);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: 'W',
|
||||
}), c);
|
||||
expect(clientSendStateFn).toHaveBeenCalledWith(tree.roomId, EventType.SpaceChild, expect.objectContaining({
|
||||
via: [staticDomain], // should retain domain independent of client.getDomain()
|
||||
order: 'X',
|
||||
}), b);
|
||||
expectOrder(a, 0);
|
||||
expectOrder(b, 2);
|
||||
expectOrder(c, 1);
|
||||
expectOrder(d, 3);
|
||||
});
|
||||
});
|
||||
|
||||
it('should upload files', async () => {
|
||||
const mxc = "mxc://example.org/file";
|
||||
const fileInfo = {
|
||||
mimetype: "text/plain",
|
||||
// other fields as required by encryption, but ignored here
|
||||
};
|
||||
const fileEventId = "$file";
|
||||
const fileName = "My File.txt";
|
||||
const fileContents = "This is a test file";
|
||||
|
||||
// Mock out Blob for the test environment
|
||||
(<any>global).Blob = MockBlob;
|
||||
|
||||
const uploadFn = jest.fn().mockImplementation((contents: Blob, opts: any) => {
|
||||
expect(contents).toBeInstanceOf(Blob);
|
||||
expect(contents.size).toEqual(fileContents.length);
|
||||
expect(opts).toMatchObject({
|
||||
includeFilename: false,
|
||||
onlyContentUri: true, // because the tests rely on this - we shouldn't really be testing for this.
|
||||
});
|
||||
return Promise.resolve(mxc);
|
||||
});
|
||||
client.uploadContent = uploadFn;
|
||||
|
||||
const sendMsgFn = jest.fn().mockImplementation((roomId: string, contents: any) => {
|
||||
expect(roomId).toEqual(tree.roomId);
|
||||
expect(contents).toMatchObject({
|
||||
msgtype: MsgType.File,
|
||||
body: fileName,
|
||||
url: mxc,
|
||||
file: fileInfo,
|
||||
[UNSTABLE_MSC3089_LEAF.unstable]: {}, // test to ensure we're definitely using unstable
|
||||
});
|
||||
|
||||
return Promise.resolve({ event_id: fileEventId }); // eslint-disable-line camelcase
|
||||
});
|
||||
client.sendMessage = sendMsgFn;
|
||||
|
||||
const sendStateFn = jest.fn()
|
||||
.mockImplementation((roomId: string, eventType: string, content: any, stateKey: string) => {
|
||||
expect(roomId).toEqual(tree.roomId);
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
expect(content).toMatchObject({
|
||||
active: true,
|
||||
name: fileName,
|
||||
});
|
||||
|
||||
return Promise.resolve(); // return value not used.
|
||||
});
|
||||
client.sendStateEvent = sendStateFn;
|
||||
|
||||
const buf = Uint8Array.from(Array.from(fileContents).map((_, i) => fileContents.charCodeAt(i)));
|
||||
|
||||
// We clone the file info just to make sure it doesn't get mutated for the test.
|
||||
await tree.createFile(fileName, buf, Object.assign({}, fileInfo));
|
||||
|
||||
expect(uploadFn).toHaveBeenCalledTimes(1);
|
||||
expect(sendMsgFn).toHaveBeenCalledTimes(1);
|
||||
expect(sendStateFn).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should support getting files', () => {
|
||||
const fileEventId = "$file";
|
||||
const fileEvent = { forTest: true }; // MatrixEvent mock
|
||||
room.currentState = {
|
||||
getStateEvents: (eventType: string, stateKey?: string) => {
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
return fileEvent;
|
||||
},
|
||||
};
|
||||
|
||||
const file = tree.getFile(fileEventId);
|
||||
expect(file).toBeDefined();
|
||||
expect(file.indexEvent).toBe(fileEvent);
|
||||
});
|
||||
|
||||
it('should return falsy for unknown files', () => {
|
||||
const fileEventId = "$file";
|
||||
room.currentState = {
|
||||
getStateEvents: (eventType: string, stateKey?: string) => {
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
||||
expect(stateKey).toEqual(fileEventId);
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
const file = tree.getFile(fileEventId);
|
||||
expect(file).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should list files', () => {
|
||||
const firstFile = { getContent: () => ({ active: true }) };
|
||||
const secondFile = { getContent: () => ({ active: false }) }; // deliberately inactive
|
||||
room.currentState = {
|
||||
getStateEvents: (eventType: string, stateKey?: string) => {
|
||||
expect(eventType).toEqual(UNSTABLE_MSC3089_BRANCH.unstable); // test to ensure we're definitely using unstable
|
||||
expect(stateKey).toBeUndefined();
|
||||
return [firstFile, secondFile];
|
||||
},
|
||||
};
|
||||
|
||||
const files = tree.listFiles();
|
||||
expect(files).toBeDefined();
|
||||
expect(files.length).toEqual(1);
|
||||
expect(files[0].indexEvent).toBe(firstFile);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2021 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 { MatrixEvent } from "../../../src/models/event";
|
||||
|
||||
describe('MatrixEvent', () => {
|
||||
it('should create copies of itself', () => {
|
||||
const a = new MatrixEvent({
|
||||
type: "com.example.test",
|
||||
content: {
|
||||
isTest: true,
|
||||
num: 42,
|
||||
},
|
||||
});
|
||||
|
||||
const clone = a.toSnapshot();
|
||||
expect(clone).toBeDefined();
|
||||
expect(clone).not.toBe(a);
|
||||
expect(clone.event).not.toBe(a.event);
|
||||
expect(clone.event).toMatchObject(a.event);
|
||||
|
||||
// The other properties we're not super interested in, honestly.
|
||||
});
|
||||
|
||||
it('should compare itself to other events using json', () => {
|
||||
const a = new MatrixEvent({
|
||||
type: "com.example.test",
|
||||
content: {
|
||||
isTest: true,
|
||||
num: 42,
|
||||
},
|
||||
});
|
||||
const b = new MatrixEvent({
|
||||
type: "com.example.test______B",
|
||||
content: {
|
||||
isTest: true,
|
||||
num: 42,
|
||||
},
|
||||
});
|
||||
expect(a.isEquivalentTo(b)).toBe(false);
|
||||
expect(a.isEquivalentTo(a)).toBe(true);
|
||||
expect(b.isEquivalentTo(a)).toBe(false);
|
||||
expect(b.isEquivalentTo(b)).toBe(true);
|
||||
expect(a.toSnapshot().isEquivalentTo(a)).toBe(true);
|
||||
expect(a.toSnapshot().isEquivalentTo(b)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {PushProcessor} from "../../src/pushprocessor";
|
||||
import { PushProcessor } from "../../src/pushprocessor";
|
||||
|
||||
describe('NotificationService', function() {
|
||||
const testUserId = "@ali:matrix.org";
|
||||
|
||||
@@ -46,8 +46,8 @@ describe("realtime-callbacks", function() {
|
||||
it("should set 'this' to the global object", function() {
|
||||
let passed = false;
|
||||
const callback = function() {
|
||||
expect(this).toBe(global); // eslint-disable-line babel/no-invalid-this
|
||||
expect(this.console).toBeTruthy(); // eslint-disable-line babel/no-invalid-this
|
||||
expect(this).toBe(global); // eslint-disable-line @babel/no-invalid-this
|
||||
expect(this.console).toBeTruthy(); // eslint-disable-line @babel/no-invalid-this
|
||||
passed = true;
|
||||
};
|
||||
callbacks.setTimeout(callback);
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
Copyright 2021 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 { EventTimelineSet } from "../../src/models/event-timeline-set";
|
||||
import { MatrixEvent } from "../../src/models/event";
|
||||
import { Relations } from "../../src/models/relations";
|
||||
|
||||
describe("Relations", function() {
|
||||
it("should deduplicate annotations", function() {
|
||||
const relations = new Relations("m.annotation", "m.reaction");
|
||||
|
||||
// Create an instance of an annotation
|
||||
const eventData = {
|
||||
"sender": "@bob:example.com",
|
||||
"type": "m.reaction",
|
||||
"event_id": "$cZ1biX33ENJqIm00ks0W_hgiO_6CHrsAc3ZQrnLeNTw",
|
||||
"room_id": "!pzVjCQSoQPpXQeHpmK:example.com",
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
|
||||
"key": "👍️",
|
||||
"rel_type": "m.annotation",
|
||||
},
|
||||
},
|
||||
};
|
||||
const eventA = new MatrixEvent(eventData);
|
||||
|
||||
// Add the event once and check results
|
||||
{
|
||||
relations.addEvent(eventA);
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey();
|
||||
expect(annotationsByKey.length).toEqual(1);
|
||||
const [key, events] = annotationsByKey[0];
|
||||
expect(key).toEqual("👍️");
|
||||
expect(events.size).toEqual(1);
|
||||
}
|
||||
|
||||
// Add the event again and expect the same
|
||||
{
|
||||
relations.addEvent(eventA);
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey();
|
||||
expect(annotationsByKey.length).toEqual(1);
|
||||
const [key, events] = annotationsByKey[0];
|
||||
expect(key).toEqual("👍️");
|
||||
expect(events.size).toEqual(1);
|
||||
}
|
||||
|
||||
// Create a fresh object with the same event content
|
||||
const eventB = new MatrixEvent(eventData);
|
||||
|
||||
// Add the event again and expect the same
|
||||
{
|
||||
relations.addEvent(eventB);
|
||||
const annotationsByKey = relations.getSortedAnnotationsByKey();
|
||||
expect(annotationsByKey.length).toEqual(1);
|
||||
const [key, events] = annotationsByKey[0];
|
||||
expect(key).toEqual("👍️");
|
||||
expect(events.size).toEqual(1);
|
||||
}
|
||||
});
|
||||
|
||||
it("should emit created regardless of ordering", async function() {
|
||||
const targetEvent = new MatrixEvent({
|
||||
"sender": "@bob:example.com",
|
||||
"type": "m.room.message",
|
||||
"event_id": "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
|
||||
"room_id": "!pzVjCQSoQPpXQeHpmK:example.com",
|
||||
"content": {},
|
||||
});
|
||||
const relationEvent = new MatrixEvent({
|
||||
"sender": "@bob:example.com",
|
||||
"type": "m.reaction",
|
||||
"event_id": "$cZ1biX33ENJqIm00ks0W_hgiO_6CHrsAc3ZQrnLeNTw",
|
||||
"room_id": "!pzVjCQSoQPpXQeHpmK:example.com",
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
|
||||
"key": "👍️",
|
||||
"rel_type": "m.annotation",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Stub the room
|
||||
const room = {
|
||||
getPendingEvent() { return null; },
|
||||
getUnfilteredTimelineSet() { return null; },
|
||||
};
|
||||
|
||||
// Add the target event first, then the relation event
|
||||
{
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once("Event.relationsCreated", resolve);
|
||||
});
|
||||
|
||||
const timelineSet = new EventTimelineSet(room, {
|
||||
unstableClientRelationAggregation: true,
|
||||
});
|
||||
timelineSet.addLiveEvent(targetEvent);
|
||||
timelineSet.addLiveEvent(relationEvent);
|
||||
|
||||
await relationsCreated;
|
||||
}
|
||||
|
||||
// Add the relation event first, then the target event
|
||||
{
|
||||
const relationsCreated = new Promise(resolve => {
|
||||
targetEvent.once("Event.relationsCreated", resolve);
|
||||
});
|
||||
|
||||
const timelineSet = new EventTimelineSet(room, {
|
||||
unstableClientRelationAggregation: true,
|
||||
});
|
||||
timelineSet.addLiveEvent(relationEvent);
|
||||
timelineSet.addLiveEvent(targetEvent);
|
||||
|
||||
await relationsCreated;
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {RoomMember} from "../../src/models/room-member";
|
||||
import { RoomMember } from "../../src/models/room-member";
|
||||
|
||||
describe("RoomMember", function() {
|
||||
const roomId = "!foo:bar";
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {RoomState} from "../../src/models/room-state";
|
||||
import {RoomMember} from "../../src/models/room-member";
|
||||
import { RoomState } from "../../src/models/room-state";
|
||||
|
||||
describe("RoomState", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -193,12 +192,7 @@ describe("RoomState", function() {
|
||||
expect(emitCount).toEqual(2);
|
||||
});
|
||||
|
||||
it("should call setPowerLevelEvent on each RoomMember for m.room.power_levels",
|
||||
function() {
|
||||
// mock up the room members
|
||||
state.members[userA] = utils.mock(RoomMember);
|
||||
state.members[userB] = utils.mock(RoomMember);
|
||||
|
||||
it("should call setPowerLevelEvent on each RoomMember for m.room.power_levels", function() {
|
||||
const powerLevelEvent = utils.mkEvent({
|
||||
type: "m.room.power_levels", room: roomId, user: userA, event: true,
|
||||
content: {
|
||||
@@ -208,18 +202,16 @@ describe("RoomState", function() {
|
||||
},
|
||||
});
|
||||
|
||||
// spy on the room members
|
||||
jest.spyOn(state.members[userA], "setPowerLevelEvent");
|
||||
jest.spyOn(state.members[userB], "setPowerLevelEvent");
|
||||
state.setStateEvents([powerLevelEvent]);
|
||||
|
||||
expect(state.members[userA].setPowerLevelEvent).toHaveBeenCalledWith(
|
||||
powerLevelEvent,
|
||||
);
|
||||
expect(state.members[userB].setPowerLevelEvent).toHaveBeenCalledWith(
|
||||
powerLevelEvent,
|
||||
);
|
||||
expect(state.members[userA].setPowerLevelEvent).toHaveBeenCalledWith(powerLevelEvent);
|
||||
expect(state.members[userB].setPowerLevelEvent).toHaveBeenCalledWith(powerLevelEvent);
|
||||
});
|
||||
|
||||
it("should call setPowerLevelEvent on a new RoomMember if power levels exist",
|
||||
function() {
|
||||
it("should call setPowerLevelEvent on a new RoomMember if power levels exist", function() {
|
||||
const memberEvent = utils.mkMembership({
|
||||
mship: "join", user: userC, room: roomId, event: true,
|
||||
});
|
||||
@@ -243,13 +235,12 @@ describe("RoomState", function() {
|
||||
});
|
||||
|
||||
it("should call setMembershipEvent on the right RoomMember", function() {
|
||||
// mock up the room members
|
||||
state.members[userA] = utils.mock(RoomMember);
|
||||
state.members[userB] = utils.mock(RoomMember);
|
||||
|
||||
const memberEvent = utils.mkMembership({
|
||||
user: userB, mship: "leave", room: roomId, event: true,
|
||||
});
|
||||
// spy on the room members
|
||||
jest.spyOn(state.members[userA], "setMembershipEvent");
|
||||
jest.spyOn(state.members[userB], "setMembershipEvent");
|
||||
state.setStateEvents([memberEvent]);
|
||||
|
||||
expect(state.members[userA].setMembershipEvent).not.toHaveBeenCalled();
|
||||
@@ -374,17 +365,13 @@ describe("RoomState", function() {
|
||||
user_ids: [userA],
|
||||
},
|
||||
});
|
||||
// mock up the room members
|
||||
state.members[userA] = utils.mock(RoomMember);
|
||||
state.members[userB] = utils.mock(RoomMember);
|
||||
// spy on the room members
|
||||
jest.spyOn(state.members[userA], "setTypingEvent");
|
||||
jest.spyOn(state.members[userB], "setTypingEvent");
|
||||
state.setTypingEvent(typingEvent);
|
||||
|
||||
expect(state.members[userA].setTypingEvent).toHaveBeenCalledWith(
|
||||
typingEvent,
|
||||
);
|
||||
expect(state.members[userB].setTypingEvent).toHaveBeenCalledWith(
|
||||
typingEvent,
|
||||
);
|
||||
expect(state.members[userA].setTypingEvent).toHaveBeenCalledWith(typingEvent);
|
||||
expect(state.members[userB].setTypingEvent).toHaveBeenCalledWith(typingEvent);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -471,13 +458,13 @@ describe("RoomState", function() {
|
||||
|
||||
it("should update after adding joined member", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userA, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "join",
|
||||
user: userA, room: roomId }),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(1);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userC, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "join",
|
||||
user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(2);
|
||||
});
|
||||
@@ -490,13 +477,13 @@ describe("RoomState", function() {
|
||||
|
||||
it("should update after adding invited member", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userA, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "invite",
|
||||
user: userA, room: roomId }),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(1);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userC, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "invite",
|
||||
user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(2);
|
||||
});
|
||||
@@ -509,15 +496,15 @@ describe("RoomState", function() {
|
||||
|
||||
it("should, once used, override counting members from state", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userA, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "join",
|
||||
user: userA, room: roomId }),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(1);
|
||||
state.setJoinedMemberCount(100);
|
||||
expect(state.getJoinedMemberCount()).toEqual(100);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userC, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "join",
|
||||
user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(100);
|
||||
});
|
||||
@@ -525,14 +512,14 @@ describe("RoomState", function() {
|
||||
it("should, once used, override counting members from state, " +
|
||||
"also after clone", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userA, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "join",
|
||||
user: userA, room: roomId }),
|
||||
]);
|
||||
state.setJoinedMemberCount(100);
|
||||
const copy = state.clone();
|
||||
copy.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "join",
|
||||
user: userC, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "join",
|
||||
user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getJoinedMemberCount()).toEqual(100);
|
||||
});
|
||||
@@ -545,15 +532,15 @@ describe("RoomState", function() {
|
||||
|
||||
it("should, once used, override counting members from state", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userB, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "invite",
|
||||
user: userB, room: roomId }),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(1);
|
||||
state.setInvitedMemberCount(100);
|
||||
expect(state.getInvitedMemberCount()).toEqual(100);
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userC, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "invite",
|
||||
user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(100);
|
||||
});
|
||||
@@ -561,14 +548,14 @@ describe("RoomState", function() {
|
||||
it("should, once used, override counting members from state, " +
|
||||
"also after clone", function() {
|
||||
state.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userB, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "invite",
|
||||
user: userB, room: roomId }),
|
||||
]);
|
||||
state.setInvitedMemberCount(100);
|
||||
const copy = state.clone();
|
||||
copy.setStateEvents([
|
||||
utils.mkMembership({event: true, mship: "invite",
|
||||
user: userC, room: roomId}),
|
||||
utils.mkMembership({ event: true, mship: "invite",
|
||||
user: userC, room: roomId }),
|
||||
]);
|
||||
expect(state.getInvitedMemberCount()).toEqual(100);
|
||||
});
|
||||
|
||||
+45
-35
@@ -1,8 +1,9 @@
|
||||
import * as utils from "../test-utils";
|
||||
import {EventStatus, MatrixEvent} from "../../src/models/event";
|
||||
import {EventTimeline} from "../../src/models/event-timeline";
|
||||
import {RoomState} from "../../src/models/room-state";
|
||||
import {Room} from "../../src/models/room";
|
||||
import { EventStatus, MatrixEvent } from "../../src";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import { RoomState } from "../../src";
|
||||
import { Room } from "../../src";
|
||||
import { TestClient } from "../TestClient";
|
||||
|
||||
describe("Room", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -85,9 +86,11 @@ describe("Room", function() {
|
||||
];
|
||||
|
||||
it("should call RoomState.setTypingEvent on m.typing events", function() {
|
||||
room.currentState = utils.mock(RoomState);
|
||||
const typing = utils.mkEvent({
|
||||
room: roomId, type: "m.typing", event: true, content: {
|
||||
room: roomId,
|
||||
type: "m.typing",
|
||||
event: true,
|
||||
content: {
|
||||
user_ids: [userA],
|
||||
},
|
||||
});
|
||||
@@ -139,8 +142,8 @@ describe("Room", function() {
|
||||
expect(callCount).toEqual(2);
|
||||
});
|
||||
|
||||
it("should call setStateEvents on the right RoomState with the right " +
|
||||
"forwardLooking value for new events", function() {
|
||||
it("should call setStateEvents on the right RoomState with the right forwardLooking value for new events",
|
||||
function() {
|
||||
const events = [
|
||||
utils.mkMembership({
|
||||
room: roomId, mship: "invite", user: userB, skey: userA, event: true,
|
||||
@@ -190,7 +193,7 @@ describe("Room", function() {
|
||||
const remoteEvent = utils.mkMessage({
|
||||
room: roomId, user: userA, event: true,
|
||||
});
|
||||
remoteEvent.event.unsigned = {transaction_id: "TXN_ID"};
|
||||
remoteEvent.event.unsigned = { transaction_id: "TXN_ID" };
|
||||
const remoteEventId = remoteEvent.getId();
|
||||
|
||||
let callCount = 0;
|
||||
@@ -374,7 +377,7 @@ describe("Room", function() {
|
||||
let events = null;
|
||||
|
||||
beforeEach(function() {
|
||||
room = new Room(roomId, null, null, {timelineSupport: timelineSupport});
|
||||
room = new Room(roomId, null, null, { timelineSupport: timelineSupport });
|
||||
// set events each time to avoid resusing Event objects (which
|
||||
// doesn't work because they get frozen)
|
||||
events = [
|
||||
@@ -456,7 +459,7 @@ describe("Room", function() {
|
||||
|
||||
describe("compareEventOrdering", function() {
|
||||
beforeEach(function() {
|
||||
room = new Room(roomId, null, null, {timelineSupport: true});
|
||||
room = new Room(roomId, null, null, { timelineSupport: true });
|
||||
});
|
||||
|
||||
const events = [
|
||||
@@ -651,7 +654,8 @@ describe("Room", function() {
|
||||
const roomName = "flibble";
|
||||
|
||||
const event = addMember(userA, "invite");
|
||||
event.event.invite_room_state = [
|
||||
event.event.unsigned = {};
|
||||
event.event.unsigned.invite_room_state = [
|
||||
{
|
||||
type: "m.room.name",
|
||||
state_key: "",
|
||||
@@ -670,7 +674,8 @@ describe("Room", function() {
|
||||
const roomName = "flibble";
|
||||
setRoomName(roomName);
|
||||
const roomNameToIgnore = "ignoreme";
|
||||
event.event.invite_room_state = [
|
||||
event.event.unsigned = {};
|
||||
event.event.unsigned.invite_room_state = [
|
||||
{
|
||||
type: "m.room.name",
|
||||
state_key: "",
|
||||
@@ -712,7 +717,7 @@ describe("Room", function() {
|
||||
it("uses hero name from state", function() {
|
||||
const name = "Mr B";
|
||||
addMember(userA, "invite");
|
||||
addMember(userB, "join", {name});
|
||||
addMember(userB, "join", { name });
|
||||
room.setSummary({
|
||||
"m.heroes": [userB],
|
||||
});
|
||||
@@ -723,7 +728,7 @@ describe("Room", function() {
|
||||
|
||||
it("uses counts from summary", function() {
|
||||
const name = "Mr B";
|
||||
addMember(userB, "join", {name});
|
||||
addMember(userB, "join", { name });
|
||||
room.setSummary({
|
||||
"m.heroes": [userB],
|
||||
"m.joined_member_count": 50,
|
||||
@@ -736,8 +741,8 @@ describe("Room", function() {
|
||||
it("relies on heroes in case of absent counts", function() {
|
||||
const nameB = "Mr Bean";
|
||||
const nameC = "Mel C";
|
||||
addMember(userB, "join", {name: nameB});
|
||||
addMember(userC, "join", {name: nameC});
|
||||
addMember(userB, "join", { name: nameB });
|
||||
addMember(userC, "join", { name: nameC });
|
||||
room.setSummary({
|
||||
"m.heroes": [userB, userC],
|
||||
});
|
||||
@@ -747,7 +752,7 @@ describe("Room", function() {
|
||||
|
||||
it("uses only heroes", function() {
|
||||
const nameB = "Mr Bean";
|
||||
addMember(userB, "join", {name: nameB});
|
||||
addMember(userB, "join", { name: nameB });
|
||||
addMember(userC, "join");
|
||||
room.setSummary({
|
||||
"m.heroes": [userB],
|
||||
@@ -840,7 +845,7 @@ describe("Room", function() {
|
||||
it("should show the other user's name for private" +
|
||||
" (invite join_rules) rooms if you are invited to it.", function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userA, "invite", {user: userB});
|
||||
addMember(userA, "invite", { user: userB });
|
||||
addMember(userB);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
@@ -915,8 +920,8 @@ describe("Room", function() {
|
||||
"available",
|
||||
function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userB, 'join', {name: "Alice"});
|
||||
addMember(userA, "invite", {user: userA});
|
||||
addMember(userB, 'join', { name: "Alice" });
|
||||
addMember(userA, "invite", { user: userA });
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name).toEqual("Alice");
|
||||
@@ -926,7 +931,7 @@ describe("Room", function() {
|
||||
function() {
|
||||
setJoinRule("invite");
|
||||
addMember(userB);
|
||||
addMember(userA, "invite", {user: userA});
|
||||
addMember(userA, "invite", { user: userA });
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name).toEqual(userB);
|
||||
@@ -1176,7 +1181,10 @@ describe("Room", function() {
|
||||
describe("addPendingEvent", function() {
|
||||
it("should add pending events to the pendingEventList if " +
|
||||
"pendingEventOrdering == 'detached'", function() {
|
||||
const room = new Room(roomId, null, userA, {
|
||||
const client = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const room = new Room(roomId, client, userA, {
|
||||
pendingEventOrdering: "detached",
|
||||
});
|
||||
const eventA = utils.mkMessage({
|
||||
@@ -1226,7 +1234,10 @@ describe("Room", function() {
|
||||
|
||||
describe("updatePendingEvent", function() {
|
||||
it("should remove cancelled events from the pending list", function() {
|
||||
const room = new Room(roomId, null, userA, {
|
||||
const client = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const room = new Room(roomId, client, userA, {
|
||||
pendingEventOrdering: "detached",
|
||||
});
|
||||
const eventA = utils.mkMessage({
|
||||
@@ -1260,7 +1271,6 @@ describe("Room", function() {
|
||||
expect(callCount).toEqual(1);
|
||||
});
|
||||
|
||||
|
||||
it("should remove cancelled events from the timeline", function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
const eventA = utils.mkMessage({
|
||||
@@ -1308,13 +1318,13 @@ describe("Room", function() {
|
||||
isRoomEncrypted: function() {
|
||||
return false;
|
||||
},
|
||||
_http: {
|
||||
http: {
|
||||
serverResponse,
|
||||
authedRequest: function() {
|
||||
if (this.serverResponse instanceof Error) {
|
||||
return Promise.reject(this.serverResponse);
|
||||
} else {
|
||||
return Promise.resolve({chunk: this.serverResponse});
|
||||
return Promise.resolve({ chunk: this.serverResponse });
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -1344,7 +1354,7 @@ describe("Room", function() {
|
||||
|
||||
it("should load members from server on first call", async function() {
|
||||
const client = createClientMock([memberEvent]);
|
||||
const room = new Room(roomId, client, null, {lazyLoadMembers: true});
|
||||
const room = new Room(roomId, client, null, { lazyLoadMembers: true });
|
||||
await room.loadMembersIfNeeded();
|
||||
const memberA = room.getMember("@user_a:bar");
|
||||
expect(memberA.name).toEqual("User A");
|
||||
@@ -1359,7 +1369,7 @@ describe("Room", function() {
|
||||
room: roomId, event: true, name: "Ms A",
|
||||
});
|
||||
const client = createClientMock([memberEvent2], [memberEvent]);
|
||||
const room = new Room(roomId, client, null, {lazyLoadMembers: true});
|
||||
const room = new Room(roomId, client, null, { lazyLoadMembers: true });
|
||||
|
||||
await room.loadMembersIfNeeded();
|
||||
|
||||
@@ -1369,7 +1379,7 @@ describe("Room", function() {
|
||||
|
||||
it("should allow retry on error", async function() {
|
||||
const client = createClientMock(new Error("server says no"));
|
||||
const room = new Room(roomId, client, null, {lazyLoadMembers: true});
|
||||
const room = new Room(roomId, client, null, { lazyLoadMembers: true });
|
||||
let hasThrown = false;
|
||||
try {
|
||||
await room.loadMembersIfNeeded();
|
||||
@@ -1378,7 +1388,7 @@ describe("Room", function() {
|
||||
}
|
||||
expect(hasThrown).toEqual(true);
|
||||
|
||||
client._http.serverResponse = [memberEvent];
|
||||
client.http.serverResponse = [memberEvent];
|
||||
await room.loadMembersIfNeeded();
|
||||
const memberA = room.getMember("@user_a:bar");
|
||||
expect(memberA.name).toEqual("User A");
|
||||
@@ -1397,17 +1407,17 @@ describe("Room", function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
const events = [];
|
||||
room.on("Room.myMembership", (_room, membership, oldMembership) => {
|
||||
events.push({membership, oldMembership});
|
||||
events.push({ membership, oldMembership });
|
||||
});
|
||||
room.updateMyMembership("invite");
|
||||
expect(room.getMyMembership()).toEqual("invite");
|
||||
expect(events[0]).toEqual({membership: "invite", oldMembership: null});
|
||||
expect(events[0]).toEqual({ membership: "invite", oldMembership: null });
|
||||
events.splice(0); //clear
|
||||
room.updateMyMembership("invite");
|
||||
expect(events.length).toEqual(0);
|
||||
room.updateMyMembership("join");
|
||||
expect(room.getMyMembership()).toEqual("join");
|
||||
expect(events[0]).toEqual({membership: "join", oldMembership: "invite"});
|
||||
expect(events[0]).toEqual({ membership: "join", oldMembership: "invite" });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1415,7 +1425,7 @@ describe("Room", function() {
|
||||
it("should return first hero id",
|
||||
function() {
|
||||
const room = new Room(roomId, null, userA);
|
||||
room.setSummary({'m.heroes': [userB]});
|
||||
room.setSummary({ 'm.heroes': [userB] });
|
||||
expect(room.guessDMUserId()).toEqual(userB);
|
||||
});
|
||||
it("should return first member that isn't self",
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// This file had a function whose name is all caps, which displeases eslint
|
||||
/* eslint new-cap: "off" */
|
||||
|
||||
import {defer} from '../../src/utils';
|
||||
import {MatrixError} from "../../src/http-api";
|
||||
import {MatrixScheduler} from "../../src/scheduler";
|
||||
import { defer } from '../../src/utils';
|
||||
import { MatrixError } from "../../src/http-api";
|
||||
import { MatrixScheduler } from "../../src/scheduler";
|
||||
import * as utils from "../test-utils";
|
||||
|
||||
jest.useFakeTimers();
|
||||
@@ -62,8 +62,8 @@ describe("MatrixScheduler", function() {
|
||||
scheduler.queueEvent(eventA),
|
||||
scheduler.queueEvent(eventB),
|
||||
]);
|
||||
deferB.resolve({b: true});
|
||||
deferA.resolve({a: true});
|
||||
deferB.resolve({ b: true });
|
||||
deferA.resolve({ a: true });
|
||||
const [a, b] = await abPromise;
|
||||
expect(a.a).toEqual(true);
|
||||
expect(b.b).toEqual(true);
|
||||
@@ -156,8 +156,8 @@ describe("MatrixScheduler", function() {
|
||||
// Expect to have processFn invoked for A&B.
|
||||
// Resolve A.
|
||||
// Expect to have processFn invoked for D.
|
||||
const eventC = utils.mkMessage({user: "@a:bar", room: roomId, event: true});
|
||||
const eventD = utils.mkMessage({user: "@b:bar", room: roomId, event: true});
|
||||
const eventC = utils.mkMessage({ user: "@a:bar", room: roomId, event: true });
|
||||
const eventD = utils.mkMessage({ user: "@b:bar", room: roomId, event: true });
|
||||
|
||||
const buckets = {};
|
||||
buckets[eventA.getId()] = "queue_A";
|
||||
@@ -241,7 +241,7 @@ describe("MatrixScheduler", function() {
|
||||
expect(queue).toEqual([eventA, eventB]);
|
||||
// modify the queue
|
||||
const eventC = utils.mkMessage(
|
||||
{user: "@a:bar", room: roomId, event: true},
|
||||
{ user: "@a:bar", room: roomId, event: true },
|
||||
);
|
||||
queue.push(eventC);
|
||||
const queueAgain = scheduler.getQueueForEvent(eventA);
|
||||
|
||||
@@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {SyncAccumulator} from "../../src/sync-accumulator";
|
||||
import { SyncAccumulator } from "../../src/sync-accumulator";
|
||||
|
||||
// The event body & unsigned object get frozen to assert that they don't get altered
|
||||
// by the impl
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {EventTimeline} from "../../src/models/event-timeline";
|
||||
import {TimelineIndex, TimelineWindow} from "../../src/timeline-window";
|
||||
import { EventTimeline } from "../../src/models/event-timeline";
|
||||
import { TimelineIndex, TimelineWindow } from "../../src/timeline-window";
|
||||
import * as utils from "../test-utils";
|
||||
|
||||
const ROOM_ID = "roomId";
|
||||
@@ -18,7 +18,7 @@ function createTimeline(numEvents, baseIndex) {
|
||||
}
|
||||
|
||||
// XXX: this is a horrid hack
|
||||
const timelineSet = { room: { roomId: ROOM_ID }};
|
||||
const timelineSet = { room: { roomId: ROOM_ID } };
|
||||
timelineSet.room.getUnfilteredTimelineSet = function() {
|
||||
return timelineSet;
|
||||
};
|
||||
@@ -46,7 +46,6 @@ function addEventsToTimeline(timeline, numEvents, atStart) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* create a pair of linked timelines
|
||||
*/
|
||||
@@ -58,7 +57,6 @@ function createLinkedTimelines() {
|
||||
return [tl1, tl2];
|
||||
}
|
||||
|
||||
|
||||
describe("TimelineIndex", function() {
|
||||
describe("minIndex", function() {
|
||||
it("should return the min index relative to BaseIndex", function() {
|
||||
@@ -133,7 +131,6 @@ describe("TimelineIndex", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("TimelineWindow", function() {
|
||||
/**
|
||||
* create a dummy eventTimelineSet and client, and a TimelineWindow
|
||||
@@ -142,7 +139,7 @@ describe("TimelineWindow", function() {
|
||||
let timelineSet;
|
||||
let client;
|
||||
function createWindow(timeline, opts) {
|
||||
timelineSet = {getTimelineForEvent: () => null};
|
||||
timelineSet = { getTimelineForEvent: () => null };
|
||||
client = {};
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineSet0).toBe(timelineSet);
|
||||
@@ -171,7 +168,7 @@ describe("TimelineWindow", function() {
|
||||
const timeline = createTimeline();
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
const timelineSet = {getTimelineForEvent: () => null};
|
||||
const timelineSet = { getTimelineForEvent: () => null };
|
||||
const client = {};
|
||||
client.getEventTimeline = function(timelineSet0, eventId0) {
|
||||
expect(timelineSet0).toBe(timelineSet);
|
||||
@@ -193,7 +190,7 @@ describe("TimelineWindow", function() {
|
||||
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
const timelineSet = {getTimelineForEvent: () => null};
|
||||
const timelineSet = { getTimelineForEvent: () => null };
|
||||
const client = {};
|
||||
|
||||
const timelineWindow = new TimelineWindow(client, timelineSet);
|
||||
@@ -266,7 +263,7 @@ describe("TimelineWindow", function() {
|
||||
it("should advance into next timeline", function() {
|
||||
const tls = createLinkedTimelines();
|
||||
const eventId = tls[0].getEvents()[1].getId();
|
||||
const timelineWindow = createWindow(tls[0], {windowLimit: 5});
|
||||
const timelineWindow = createWindow(tls[0], { windowLimit: 5 });
|
||||
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = tls[0].getEvents();
|
||||
@@ -311,7 +308,7 @@ describe("TimelineWindow", function() {
|
||||
it("should retreat into previous timeline", function() {
|
||||
const tls = createLinkedTimelines();
|
||||
const eventId = tls[1].getEvents()[1].getId();
|
||||
const timelineWindow = createWindow(tls[1], {windowLimit: 5});
|
||||
const timelineWindow = createWindow(tls[1], { windowLimit: 5 });
|
||||
|
||||
return timelineWindow.load(eventId, 3).then(function() {
|
||||
const expectedEvents = tls[1].getEvents();
|
||||
@@ -357,7 +354,7 @@ describe("TimelineWindow", function() {
|
||||
const timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
|
||||
|
||||
const timelineWindow = createWindow(timeline, {windowLimit: 5});
|
||||
const timelineWindow = createWindow(timeline, { windowLimit: 5 });
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
client.paginateEventTimeline = function(timeline0, opts) {
|
||||
@@ -385,12 +382,11 @@ describe("TimelineWindow", function() {
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("should make backward pagination requests", function() {
|
||||
const timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok", EventTimeline.BACKWARDS);
|
||||
|
||||
const timelineWindow = createWindow(timeline, {windowLimit: 5});
|
||||
const timelineWindow = createWindow(timeline, { windowLimit: 5 });
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
client.paginateEventTimeline = function(timeline0, opts) {
|
||||
@@ -422,7 +418,7 @@ describe("TimelineWindow", function() {
|
||||
const timeline = createTimeline();
|
||||
timeline.setPaginationToken("toktok", EventTimeline.FORWARDS);
|
||||
|
||||
const timelineWindow = createWindow(timeline, {windowLimit: 5});
|
||||
const timelineWindow = createWindow(timeline, { windowLimit: 5 });
|
||||
const eventId = timeline.getEvents()[1].getId();
|
||||
|
||||
let paginateCount = 0;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {User} from "../../src/models/user";
|
||||
import { User } from "../../src/models/user";
|
||||
import * as utils from "../test-utils";
|
||||
|
||||
describe("User", function() {
|
||||
|
||||
@@ -1,285 +0,0 @@
|
||||
import * as utils from "../../src/utils";
|
||||
|
||||
describe("utils", function() {
|
||||
describe("encodeParams", function() {
|
||||
it("should url encode and concat with &s", function() {
|
||||
const params = {
|
||||
foo: "bar",
|
||||
baz: "beer@",
|
||||
};
|
||||
expect(utils.encodeParams(params)).toEqual(
|
||||
"foo=bar&baz=beer%40",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encodeUri", function() {
|
||||
it("should replace based on object keys and url encode", function() {
|
||||
const path = "foo/bar/%something/%here";
|
||||
const vals = {
|
||||
"%something": "baz",
|
||||
"%here": "beer@",
|
||||
};
|
||||
expect(utils.encodeUri(path, vals)).toEqual(
|
||||
"foo/bar/baz/beer%40",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("forEach", function() {
|
||||
it("should be invoked for each element", function() {
|
||||
const arr = [];
|
||||
utils.forEach([55, 66, 77], function(element) {
|
||||
arr.push(element);
|
||||
});
|
||||
expect(arr).toEqual([55, 66, 77]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("findElement", function() {
|
||||
it("should find only 1 element if there is a match", function() {
|
||||
const matchFn = function() {
|
||||
return true;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
expect(utils.findElement(arr, matchFn)).toEqual(55);
|
||||
});
|
||||
it("should be able to find in reverse order", function() {
|
||||
const matchFn = function() {
|
||||
return true;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
expect(utils.findElement(arr, matchFn, true)).toEqual(77);
|
||||
});
|
||||
it("should find nothing if the function never returns true", function() {
|
||||
const matchFn = function() {
|
||||
return false;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
expect(utils.findElement(arr, matchFn)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeElement", function() {
|
||||
it("should remove only 1 element if there is a match", function() {
|
||||
const matchFn = function() {
|
||||
return true;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
utils.removeElement(arr, matchFn);
|
||||
expect(arr).toEqual([66, 77]);
|
||||
});
|
||||
it("should be able to remove in reverse order", function() {
|
||||
const matchFn = function() {
|
||||
return true;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
utils.removeElement(arr, matchFn, true);
|
||||
expect(arr).toEqual([55, 66]);
|
||||
});
|
||||
it("should remove nothing if the function never returns true", function() {
|
||||
const matchFn = function() {
|
||||
return false;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
utils.removeElement(arr, matchFn);
|
||||
expect(arr).toEqual(arr);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isFunction", function() {
|
||||
it("should return true for functions", function() {
|
||||
expect(utils.isFunction([])).toBe(false);
|
||||
expect(utils.isFunction([5, 3, 7])).toBe(false);
|
||||
expect(utils.isFunction()).toBe(false);
|
||||
expect(utils.isFunction(null)).toBe(false);
|
||||
expect(utils.isFunction({})).toBe(false);
|
||||
expect(utils.isFunction("foo")).toBe(false);
|
||||
expect(utils.isFunction(555)).toBe(false);
|
||||
|
||||
expect(utils.isFunction(function() {})).toBe(true);
|
||||
const s = { foo: function() {} };
|
||||
expect(utils.isFunction(s.foo)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isArray", function() {
|
||||
it("should return true for arrays", function() {
|
||||
expect(utils.isArray([])).toBe(true);
|
||||
expect(utils.isArray([5, 3, 7])).toBe(true);
|
||||
|
||||
expect(utils.isArray()).toBe(false);
|
||||
expect(utils.isArray(null)).toBe(false);
|
||||
expect(utils.isArray({})).toBe(false);
|
||||
expect(utils.isArray("foo")).toBe(false);
|
||||
expect(utils.isArray(555)).toBe(false);
|
||||
expect(utils.isArray(function() {})).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkObjectHasKeys", function() {
|
||||
it("should throw for missing keys", function() {
|
||||
expect(function() {
|
||||
utils.checkObjectHasKeys({}, ["foo"]);
|
||||
}).toThrow();
|
||||
expect(function() {
|
||||
utils.checkObjectHasKeys({
|
||||
foo: "bar",
|
||||
}, ["foo"]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkObjectHasNoAdditionalKeys", function() {
|
||||
it("should throw for extra keys", function() {
|
||||
expect(function() {
|
||||
utils.checkObjectHasNoAdditionalKeys({
|
||||
foo: "bar",
|
||||
baz: 4,
|
||||
}, ["foo"]);
|
||||
}).toThrow();
|
||||
|
||||
expect(function() {
|
||||
utils.checkObjectHasNoAdditionalKeys({
|
||||
foo: "bar",
|
||||
}, ["foo"]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deepCompare", function() {
|
||||
const assert = {
|
||||
isTrue: function(x) {
|
||||
expect(x).toBe(true);
|
||||
},
|
||||
isFalse: function(x) {
|
||||
expect(x).toBe(false);
|
||||
},
|
||||
};
|
||||
|
||||
it("should handle primitives", function() {
|
||||
assert.isTrue(utils.deepCompare(null, null));
|
||||
assert.isFalse(utils.deepCompare(null, undefined));
|
||||
assert.isTrue(utils.deepCompare("hi", "hi"));
|
||||
assert.isTrue(utils.deepCompare(5, 5));
|
||||
assert.isFalse(utils.deepCompare(5, 10));
|
||||
});
|
||||
|
||||
it("should handle regexps", function() {
|
||||
assert.isTrue(utils.deepCompare(/abc/, /abc/));
|
||||
assert.isFalse(utils.deepCompare(/abc/, /123/));
|
||||
const r = /abc/;
|
||||
assert.isTrue(utils.deepCompare(r, r));
|
||||
});
|
||||
|
||||
it("should handle dates", function() {
|
||||
assert.isTrue(utils.deepCompare(new Date("2011-03-31"),
|
||||
new Date("2011-03-31")));
|
||||
assert.isFalse(utils.deepCompare(new Date("2011-03-31"),
|
||||
new Date("1970-01-01")));
|
||||
});
|
||||
|
||||
it("should handle arrays", function() {
|
||||
assert.isTrue(utils.deepCompare([], []));
|
||||
assert.isTrue(utils.deepCompare([1, 2], [1, 2]));
|
||||
assert.isFalse(utils.deepCompare([1, 2], [2, 1]));
|
||||
assert.isFalse(utils.deepCompare([1, 2], [1, 2, 3]));
|
||||
});
|
||||
|
||||
it("should handle simple objects", function() {
|
||||
assert.isTrue(utils.deepCompare({}, {}));
|
||||
assert.isTrue(utils.deepCompare({a: 1, b: 2}, {a: 1, b: 2}));
|
||||
assert.isTrue(utils.deepCompare({a: 1, b: 2}, {b: 2, a: 1}));
|
||||
assert.isFalse(utils.deepCompare({a: 1, b: 2}, {a: 1, b: 3}));
|
||||
|
||||
assert.isTrue(utils.deepCompare({1: {name: "mhc", age: 28},
|
||||
2: {name: "arb", age: 26}},
|
||||
{1: {name: "mhc", age: 28},
|
||||
2: {name: "arb", age: 26}}));
|
||||
|
||||
assert.isFalse(utils.deepCompare({1: {name: "mhc", age: 28},
|
||||
2: {name: "arb", age: 26}},
|
||||
{1: {name: "mhc", age: 28},
|
||||
2: {name: "arb", age: 27}}));
|
||||
|
||||
assert.isFalse(utils.deepCompare({}, null));
|
||||
assert.isFalse(utils.deepCompare({}, undefined));
|
||||
});
|
||||
|
||||
it("should handle functions", function() {
|
||||
// no two different function is equal really, they capture their
|
||||
// context variables so even if they have same toString(), they
|
||||
// won't have same functionality
|
||||
const func = function(x) {
|
||||
return true;
|
||||
};
|
||||
const func2 = function(x) {
|
||||
return true;
|
||||
};
|
||||
assert.isTrue(utils.deepCompare(func, func));
|
||||
assert.isFalse(utils.deepCompare(func, func2));
|
||||
assert.isTrue(utils.deepCompare({ a: { b: func } }, { a: { b: func } }));
|
||||
assert.isFalse(utils.deepCompare({ a: { b: func } }, { a: { b: func2 } }));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("extend", function() {
|
||||
const SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
|
||||
|
||||
it("should extend", function() {
|
||||
const target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
const merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
const sourceOrig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
|
||||
});
|
||||
|
||||
it("should ignore null", function() {
|
||||
const target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
const merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
const sourceOrig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, null, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
|
||||
});
|
||||
|
||||
it("should handle properties created with defineProperties", function() {
|
||||
const source = Object.defineProperties({}, {
|
||||
"enumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
"nonenumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const target = {};
|
||||
utils.extend(target, source);
|
||||
expect(target.enumerableProp).toBe(true);
|
||||
expect(target.nonenumerableProp).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,496 @@
|
||||
import * as utils from "../../src/utils";
|
||||
import {
|
||||
alphabetPad,
|
||||
averageBetweenStrings,
|
||||
baseToString,
|
||||
deepSortedObjectEntries,
|
||||
DEFAULT_ALPHABET,
|
||||
lexicographicCompare,
|
||||
nextString,
|
||||
prevString,
|
||||
simpleRetryOperation,
|
||||
stringToBase,
|
||||
} from "../../src/utils";
|
||||
import { logger } from "../../src/logger";
|
||||
|
||||
// TODO: Fix types throughout
|
||||
|
||||
describe("utils", function() {
|
||||
describe("encodeParams", function() {
|
||||
it("should url encode and concat with &s", function() {
|
||||
const params = {
|
||||
foo: "bar",
|
||||
baz: "beer@",
|
||||
};
|
||||
expect(utils.encodeParams(params)).toEqual(
|
||||
"foo=bar&baz=beer%40",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("encodeUri", function() {
|
||||
it("should replace based on object keys and url encode", function() {
|
||||
const path = "foo/bar/%something/%here";
|
||||
const vals = {
|
||||
"%something": "baz",
|
||||
"%here": "beer@",
|
||||
};
|
||||
expect(utils.encodeUri(path, vals)).toEqual(
|
||||
"foo/bar/baz/beer%40",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeElement", function() {
|
||||
it("should remove only 1 element if there is a match", function() {
|
||||
const matchFn = function() {
|
||||
return true;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
utils.removeElement(arr, matchFn);
|
||||
expect(arr).toEqual([66, 77]);
|
||||
});
|
||||
it("should be able to remove in reverse order", function() {
|
||||
const matchFn = function() {
|
||||
return true;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
utils.removeElement(arr, matchFn, true);
|
||||
expect(arr).toEqual([55, 66]);
|
||||
});
|
||||
it("should remove nothing if the function never returns true", function() {
|
||||
const matchFn = function() {
|
||||
return false;
|
||||
};
|
||||
const arr = [55, 66, 77];
|
||||
utils.removeElement(arr, matchFn);
|
||||
expect(arr).toEqual(arr);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isFunction", function() {
|
||||
it("should return true for functions", function() {
|
||||
expect(utils.isFunction([])).toBe(false);
|
||||
expect(utils.isFunction([5, 3, 7])).toBe(false);
|
||||
expect(utils.isFunction(undefined)).toBe(false);
|
||||
expect(utils.isFunction(null)).toBe(false);
|
||||
expect(utils.isFunction({})).toBe(false);
|
||||
expect(utils.isFunction("foo")).toBe(false);
|
||||
expect(utils.isFunction(555)).toBe(false);
|
||||
|
||||
expect(utils.isFunction(function() {})).toBe(true);
|
||||
const s = { foo: function() {} };
|
||||
expect(utils.isFunction(s.foo)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkObjectHasKeys", function() {
|
||||
it("should throw for missing keys", function() {
|
||||
expect(function() {
|
||||
utils.checkObjectHasKeys({}, ["foo"]);
|
||||
}).toThrow();
|
||||
expect(function() {
|
||||
utils.checkObjectHasKeys({
|
||||
foo: "bar",
|
||||
}, ["foo"]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("checkObjectHasNoAdditionalKeys", function() {
|
||||
it("should throw for extra keys", function() {
|
||||
expect(function() {
|
||||
utils.checkObjectHasNoAdditionalKeys({ foo: "bar", baz: 4 }, ["foo"]);
|
||||
}).toThrow();
|
||||
|
||||
expect(function() {
|
||||
utils.checkObjectHasNoAdditionalKeys({ foo: "bar" }, ["foo"]);
|
||||
}).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("deepCompare", function() {
|
||||
const assert = {
|
||||
isTrue: function(x) {
|
||||
expect(x).toBe(true);
|
||||
},
|
||||
isFalse: function(x) {
|
||||
expect(x).toBe(false);
|
||||
},
|
||||
};
|
||||
|
||||
it("should handle primitives", function() {
|
||||
assert.isTrue(utils.deepCompare(null, null));
|
||||
assert.isFalse(utils.deepCompare(null, undefined));
|
||||
assert.isTrue(utils.deepCompare("hi", "hi"));
|
||||
assert.isTrue(utils.deepCompare(5, 5));
|
||||
assert.isFalse(utils.deepCompare(5, 10));
|
||||
});
|
||||
|
||||
it("should handle regexps", function() {
|
||||
assert.isTrue(utils.deepCompare(/abc/, /abc/));
|
||||
assert.isFalse(utils.deepCompare(/abc/, /123/));
|
||||
const r = /abc/;
|
||||
assert.isTrue(utils.deepCompare(r, r));
|
||||
});
|
||||
|
||||
it("should handle dates", function() {
|
||||
assert.isTrue(utils.deepCompare(new Date("2011-03-31"), new Date("2011-03-31")));
|
||||
assert.isFalse(utils.deepCompare(new Date("2011-03-31"), new Date("1970-01-01")));
|
||||
});
|
||||
|
||||
it("should handle arrays", function() {
|
||||
assert.isTrue(utils.deepCompare([], []));
|
||||
assert.isTrue(utils.deepCompare([1, 2], [1, 2]));
|
||||
assert.isFalse(utils.deepCompare([1, 2], [2, 1]));
|
||||
assert.isFalse(utils.deepCompare([1, 2], [1, 2, 3]));
|
||||
});
|
||||
|
||||
it("should handle simple objects", function() {
|
||||
assert.isTrue(utils.deepCompare({}, {}));
|
||||
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 2 }));
|
||||
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { b: 2, a: 1 }));
|
||||
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 3 }));
|
||||
|
||||
assert.isTrue(utils.deepCompare({
|
||||
1: { name: "mhc", age: 28 },
|
||||
2: { name: "arb", age: 26 },
|
||||
}, {
|
||||
1: { name: "mhc", age: 28 },
|
||||
2: { name: "arb", age: 26 },
|
||||
}));
|
||||
|
||||
assert.isFalse(utils.deepCompare({
|
||||
1: { name: "mhc", age: 28 },
|
||||
2: { name: "arb", age: 26 },
|
||||
}, {
|
||||
1: { name: "mhc", age: 28 },
|
||||
2: { name: "arb", age: 27 },
|
||||
}));
|
||||
|
||||
assert.isFalse(utils.deepCompare({}, null));
|
||||
assert.isFalse(utils.deepCompare({}, undefined));
|
||||
});
|
||||
|
||||
it("should handle functions", function() {
|
||||
// no two different function is equal really, they capture their
|
||||
// context variables so even if they have same toString(), they
|
||||
// won't have same functionality
|
||||
const func = function(x) {
|
||||
return true;
|
||||
};
|
||||
const func2 = function(x) {
|
||||
return true;
|
||||
};
|
||||
assert.isTrue(utils.deepCompare(func, func));
|
||||
assert.isFalse(utils.deepCompare(func, func2));
|
||||
assert.isTrue(utils.deepCompare({ a: { b: func } }, { a: { b: func } }));
|
||||
assert.isFalse(utils.deepCompare({ a: { b: func } }, { a: { b: func2 } }));
|
||||
});
|
||||
});
|
||||
|
||||
describe("extend", function() {
|
||||
const SOURCE = { "prop2": 1, "string2": "x", "newprop": "new" };
|
||||
|
||||
it("should extend", function() {
|
||||
const target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
const merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
const sourceOrig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
|
||||
});
|
||||
|
||||
it("should ignore null", function() {
|
||||
const target = {
|
||||
"prop1": 5, "prop2": 7, "string1": "baz", "string2": "foo",
|
||||
};
|
||||
const merged = {
|
||||
"prop1": 5, "prop2": 1, "string1": "baz", "string2": "x",
|
||||
"newprop": "new",
|
||||
};
|
||||
const sourceOrig = JSON.stringify(SOURCE);
|
||||
|
||||
utils.extend(target, null, SOURCE);
|
||||
expect(JSON.stringify(target)).toEqual(JSON.stringify(merged));
|
||||
|
||||
// check the originial wasn't modified
|
||||
expect(JSON.stringify(SOURCE)).toEqual(sourceOrig);
|
||||
});
|
||||
|
||||
it("should handle properties created with defineProperties", function() {
|
||||
const source = Object.defineProperties({}, {
|
||||
"enumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
enumerable: true,
|
||||
},
|
||||
"nonenumerableProp": {
|
||||
get: function() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: Fix type
|
||||
const target: any = {};
|
||||
utils.extend(target, source);
|
||||
expect(target.enumerableProp).toBe(true);
|
||||
expect(target.nonenumerableProp).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chunkPromises", function() {
|
||||
it("should execute promises in chunks", async function() {
|
||||
let promiseCount = 0;
|
||||
|
||||
async function fn1() {
|
||||
await utils.sleep(1);
|
||||
expect(promiseCount).toEqual(0);
|
||||
++promiseCount;
|
||||
}
|
||||
|
||||
async function fn2() {
|
||||
expect(promiseCount).toEqual(1);
|
||||
++promiseCount;
|
||||
}
|
||||
|
||||
await utils.chunkPromises([fn1, fn2], 1);
|
||||
expect(promiseCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('simpleRetryOperation', () => {
|
||||
it('should retry', async () => {
|
||||
let count = 0;
|
||||
const val = {};
|
||||
const fn = (attempt) => {
|
||||
count++;
|
||||
|
||||
// If this expectation fails then it can appear as a Jest Timeout due to
|
||||
// the retry running beyond the test limit.
|
||||
expect(attempt).toEqual(count);
|
||||
|
||||
if (count > 1) {
|
||||
return Promise.resolve(val);
|
||||
} else {
|
||||
return Promise.reject(new Error("Iterative failure"));
|
||||
}
|
||||
};
|
||||
|
||||
const ret = await simpleRetryOperation(fn);
|
||||
expect(ret).toBe(val);
|
||||
expect(count).toEqual(2);
|
||||
});
|
||||
|
||||
// We don't test much else of the function because then we're just testing that the
|
||||
// underlying library behaves, which should be tested on its own. Our API surface is
|
||||
// all that concerns us.
|
||||
});
|
||||
|
||||
describe('DEFAULT_ALPHABET', () => {
|
||||
it('should be usefully printable ASCII in order', () => {
|
||||
expect(DEFAULT_ALPHABET).toEqual(
|
||||
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('alphabetPad', () => {
|
||||
it('should pad to the alphabet length', () => {
|
||||
const len = 12;
|
||||
expect(alphabetPad("a", len)).toEqual("a" + ("".padEnd(len - 1, DEFAULT_ALPHABET[0])));
|
||||
expect(alphabetPad("a", len, "123")).toEqual("a" + ("".padEnd(len - 1, '1')));
|
||||
});
|
||||
});
|
||||
|
||||
describe('baseToString', () => {
|
||||
it('should calculate the appropriate string from numbers', () => {
|
||||
// Verify the whole alphabet
|
||||
for (let i = BigInt(1); i <= DEFAULT_ALPHABET.length; i++) {
|
||||
logger.log({ i }); // for debugging
|
||||
expect(baseToString(i)).toEqual(DEFAULT_ALPHABET[Number(i) - 1]);
|
||||
}
|
||||
|
||||
// Just quickly double check that repeated characters aren't treated as padding, particularly
|
||||
// at the beginning of the alphabet where they are most vulnerable to this behaviour.
|
||||
expect(baseToString(BigInt(1))).toEqual(DEFAULT_ALPHABET[0].repeat(1));
|
||||
expect(baseToString(BigInt(96))).toEqual(DEFAULT_ALPHABET[0].repeat(2));
|
||||
expect(baseToString(BigInt(9121))).toEqual(DEFAULT_ALPHABET[0].repeat(3));
|
||||
expect(baseToString(BigInt(866496))).toEqual(DEFAULT_ALPHABET[0].repeat(4));
|
||||
expect(baseToString(BigInt(82317121))).toEqual(DEFAULT_ALPHABET[0].repeat(5));
|
||||
expect(baseToString(BigInt(7820126496))).toEqual(DEFAULT_ALPHABET[0].repeat(6));
|
||||
|
||||
expect(baseToString(BigInt(10))).toEqual(DEFAULT_ALPHABET[9]);
|
||||
expect(baseToString(BigInt(10), "abcdefghijklmnopqrstuvwxyz")).toEqual('j');
|
||||
expect(baseToString(BigInt(6337))).toEqual("ab");
|
||||
expect(baseToString(BigInt(80), "abcdefghijklmnopqrstuvwxyz")).toEqual('cb');
|
||||
});
|
||||
});
|
||||
|
||||
describe('stringToBase', () => {
|
||||
it('should calculate the appropriate number for a string', () => {
|
||||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(1))).toEqual(BigInt(1));
|
||||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(2))).toEqual(BigInt(96));
|
||||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(3))).toEqual(BigInt(9121));
|
||||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(4))).toEqual(BigInt(866496));
|
||||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(5))).toEqual(BigInt(82317121));
|
||||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(6))).toEqual(BigInt(7820126496));
|
||||
expect(stringToBase("a", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(1));
|
||||
expect(stringToBase("a")).toEqual(BigInt(66));
|
||||
expect(stringToBase("c", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(3));
|
||||
expect(stringToBase("ab")).toEqual(BigInt(6337));
|
||||
expect(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(80));
|
||||
});
|
||||
});
|
||||
|
||||
describe('averageBetweenStrings', () => {
|
||||
it('should average appropriately', () => {
|
||||
expect(averageBetweenStrings(" ", "!!")).toEqual(" P");
|
||||
expect(averageBetweenStrings(" ", "!")).toEqual(" ");
|
||||
expect(averageBetweenStrings('A', 'B')).toEqual('A ');
|
||||
expect(averageBetweenStrings('AA', 'BB')).toEqual('Aq');
|
||||
expect(averageBetweenStrings('A', 'z')).toEqual(']');
|
||||
expect(averageBetweenStrings('a', 'z', "abcdefghijklmnopqrstuvwxyz")).toEqual('m');
|
||||
expect(averageBetweenStrings('AA', 'zz')).toEqual('^.');
|
||||
expect(averageBetweenStrings('aa', 'zz', "abcdefghijklmnopqrstuvwxyz")).toEqual('mz');
|
||||
expect(averageBetweenStrings('cat', 'doggo')).toEqual("d9>Cw");
|
||||
expect(averageBetweenStrings('cat', 'doggo', "abcdefghijklmnopqrstuvwxyz")).toEqual("cumqh");
|
||||
});
|
||||
});
|
||||
|
||||
describe('nextString', () => {
|
||||
it('should find the next string appropriately', () => {
|
||||
expect(nextString('A')).toEqual('B');
|
||||
expect(nextString('b', 'abcdefghijklmnopqrstuvwxyz')).toEqual('c');
|
||||
expect(nextString('cat')).toEqual('cau');
|
||||
expect(nextString('cat', 'abcdefghijklmnopqrstuvwxyz')).toEqual('cau');
|
||||
});
|
||||
});
|
||||
|
||||
describe('prevString', () => {
|
||||
it('should find the next string appropriately', () => {
|
||||
expect(prevString('B')).toEqual('A');
|
||||
expect(prevString('c', 'abcdefghijklmnopqrstuvwxyz')).toEqual('b');
|
||||
expect(prevString('cau')).toEqual('cat');
|
||||
expect(prevString('cau', 'abcdefghijklmnopqrstuvwxyz')).toEqual('cat');
|
||||
});
|
||||
});
|
||||
|
||||
// Let's just ensure the ordering is sensible for lexicographic ordering
|
||||
describe('string averaging unified', () => {
|
||||
it('should be truly previous and next', () => {
|
||||
let midpoint = "cat";
|
||||
|
||||
// We run this test 100 times to ensure we end up with a sane sequence.
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const next = nextString(midpoint);
|
||||
const prev = prevString(midpoint);
|
||||
logger.log({ i, midpoint, next, prev }); // for test debugging
|
||||
|
||||
expect(lexicographicCompare(midpoint, next) < 0).toBe(true);
|
||||
expect(lexicographicCompare(midpoint, prev) > 0).toBe(true);
|
||||
expect(averageBetweenStrings(prev, next)).toBe(midpoint);
|
||||
|
||||
midpoint = next;
|
||||
}
|
||||
});
|
||||
|
||||
it('should roll over', () => {
|
||||
const lastAlpha = DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1];
|
||||
const firstAlpha = DEFAULT_ALPHABET[0];
|
||||
|
||||
const highRoll = firstAlpha + firstAlpha;
|
||||
const lowRoll = lastAlpha;
|
||||
|
||||
expect(nextString(lowRoll)).toEqual(highRoll);
|
||||
expect(prevString(highRoll)).toEqual(lowRoll);
|
||||
});
|
||||
|
||||
it('should be reversible on small strings', () => {
|
||||
// Large scale reversibility is tested for max space order value
|
||||
const input = "cats";
|
||||
expect(prevString(nextString(input))).toEqual(input);
|
||||
});
|
||||
|
||||
// We want to explicitly make sure that Space order values are supported and roll appropriately
|
||||
it('should properly handle rolling over at 50 characters', () => {
|
||||
// Note: we also test reversibility of large strings here.
|
||||
|
||||
const maxSpaceValue = DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1].repeat(50);
|
||||
const fiftyFirstChar = DEFAULT_ALPHABET[0].repeat(51);
|
||||
|
||||
expect(nextString(maxSpaceValue)).toBe(fiftyFirstChar);
|
||||
expect(prevString(fiftyFirstChar)).toBe(maxSpaceValue);
|
||||
|
||||
// We're testing that the rollover happened, which means that the next string come before
|
||||
// the maximum space order value lexicographically.
|
||||
expect(lexicographicCompare(maxSpaceValue, fiftyFirstChar) > 0).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('lexicographicCompare', () => {
|
||||
it('should work', () => {
|
||||
// Simple tests
|
||||
expect(lexicographicCompare('a', 'b') < 0).toBe(true);
|
||||
expect(lexicographicCompare('ab', 'b') < 0).toBe(true);
|
||||
expect(lexicographicCompare('cat', 'dog') < 0).toBe(true);
|
||||
|
||||
// Simple tests (reversed)
|
||||
expect(lexicographicCompare('b', 'a') > 0).toBe(true);
|
||||
expect(lexicographicCompare('b', 'ab') > 0).toBe(true);
|
||||
expect(lexicographicCompare('dog', 'cat') > 0).toBe(true);
|
||||
|
||||
// Simple equality tests
|
||||
expect(lexicographicCompare('a', 'a') === 0).toBe(true);
|
||||
expect(lexicographicCompare('A', 'A') === 0).toBe(true);
|
||||
|
||||
// ASCII rule testing
|
||||
expect(lexicographicCompare('A', 'a') < 0).toBe(true);
|
||||
expect(lexicographicCompare('a', 'A') > 0).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deepSortedObjectEntries', () => {
|
||||
it('should auto-return non-objects', () => {
|
||||
expect(deepSortedObjectEntries(42)).toEqual(42);
|
||||
expect(deepSortedObjectEntries("not object")).toEqual("not object");
|
||||
expect(deepSortedObjectEntries(true)).toEqual(true);
|
||||
expect(deepSortedObjectEntries([42])).toEqual([42]);
|
||||
expect(deepSortedObjectEntries(null)).toEqual(null);
|
||||
expect(deepSortedObjectEntries(undefined)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('should sort objects appropriately', () => {
|
||||
const input = {
|
||||
a: 42,
|
||||
b: {
|
||||
d: {},
|
||||
a: "test",
|
||||
b: "alpha",
|
||||
},
|
||||
[72]: "test",
|
||||
};
|
||||
const output = [
|
||||
["72", "test"],
|
||||
["a", 42],
|
||||
["b", [
|
||||
["a", "test"],
|
||||
["b", "alpha"],
|
||||
["d", []],
|
||||
]],
|
||||
];
|
||||
|
||||
expect(deepSortedObjectEntries(input)).toMatchObject(output);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {MatrixCall, CallErrorCode} from '../../../src/webrtc/call';
|
||||
import { TestClient } from '../../TestClient';
|
||||
import { MatrixCall, CallErrorCode, CallEvent } from '../../../src/webrtc/call';
|
||||
|
||||
const DUMMY_SDP = (
|
||||
"v=0\r\n" +
|
||||
@@ -79,6 +79,7 @@ class MockRTCPeerConnection {
|
||||
return Promise.resolve();
|
||||
}
|
||||
close() {}
|
||||
getStats() { return []; }
|
||||
}
|
||||
|
||||
describe('Call', function() {
|
||||
@@ -122,6 +123,7 @@ describe('Call', function() {
|
||||
// We just stub out sendEvent: we're not interested in testing the client's
|
||||
// event sending code here
|
||||
client.client.sendEvent = () => {};
|
||||
client.httpBackend.when("GET", "/voip/turnServer").respond(200, {});
|
||||
call = new MatrixCall({
|
||||
client: client.client,
|
||||
roomId: '!foo:bar',
|
||||
@@ -138,7 +140,9 @@ describe('Call', function() {
|
||||
});
|
||||
|
||||
it('should ignore candidate events from non-matching party ID', async function() {
|
||||
await call.placeVoiceCall();
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
@@ -190,4 +194,108 @@ describe('Call', function() {
|
||||
// Hangup to stop timers
|
||||
call.hangup(CallErrorCode.UserHangup, true);
|
||||
});
|
||||
|
||||
it('should add candidates received before answer if party ID is correct', async function() {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
call.peerConn.addIceCandidate = jest.fn();
|
||||
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'the_correct_party_id',
|
||||
candidates: [
|
||||
{
|
||||
candidate: 'the_correct_candidate',
|
||||
sdpMid: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'some_other_party_id',
|
||||
candidates: [
|
||||
{
|
||||
candidate: 'the_wrong_candidate',
|
||||
sdpMid: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(0);
|
||||
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'the_correct_party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
|
||||
expect(call.peerConn.addIceCandidate).toHaveBeenCalledWith({
|
||||
candidate: 'the_correct_candidate',
|
||||
sdpMid: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('should map asserted identity messages to remoteAssertedIdentity', async function() {
|
||||
const callPromise = call.placeVoiceCall();
|
||||
await client.httpBackend.flush();
|
||||
await callPromise;
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const identChangedCallback = jest.fn();
|
||||
call.on(CallEvent.AssertedIdentityChanged, identChangedCallback);
|
||||
|
||||
await call.onAssertedIdentityReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'party_id',
|
||||
asserted_identity: {
|
||||
id: "@steve:example.com",
|
||||
display_name: "Steve Gibbons",
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
expect(identChangedCallback).toHaveBeenCalled();
|
||||
|
||||
const ident = call.getRemoteAssertedIdentity();
|
||||
expect(ident.id).toEqual("@steve:example.com");
|
||||
expect(ident.displayName).toEqual("Steve Gibbons");
|
||||
|
||||
// Hangup to stop timers
|
||||
call.hangup(CallErrorCode.UserHangup, true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export interface IIdentityServerProvider {
|
||||
/**
|
||||
* Gets an access token for use against the identity server,
|
||||
* for the associated client.
|
||||
* @returns {Promise<string>} Resolves to the access token.
|
||||
*/
|
||||
getAccessToken(): Promise<string>;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
|
||||
declare module "another-json" {
|
||||
export function stringify(o: object): string;
|
||||
}
|
||||
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { UnstableValue } from "../NamespacedValue";
|
||||
|
||||
export enum EventType {
|
||||
// Room state events
|
||||
RoomCanonicalAlias = "m.room.canonical_alias",
|
||||
@@ -36,6 +38,9 @@ export enum EventType {
|
||||
*/
|
||||
RoomAliases = "m.room.aliases", // deprecated https://matrix.org/docs/spec/client_server/r0.6.1#historical-events
|
||||
|
||||
SpaceChild = "m.space.child",
|
||||
SpaceParent = "m.space.parent",
|
||||
|
||||
// Room timeline events
|
||||
RoomRedaction = "m.room.redaction",
|
||||
RoomMessage = "m.room.message",
|
||||
@@ -49,12 +54,16 @@ export enum EventType {
|
||||
CallSelectAnswer = "m.call.select_answer",
|
||||
CallNegotiate = "m.call.negotiate",
|
||||
CallReplaces = "m.call.replaces",
|
||||
CallAssertedIdentity = "m.call.asserted_identity",
|
||||
CallAssertedIdentityPrefix = "org.matrix.call.asserted_identity",
|
||||
KeyVerificationRequest = "m.key.verification.request",
|
||||
KeyVerificationStart = "m.key.verification.start",
|
||||
KeyVerificationCancel = "m.key.verification.cancel",
|
||||
KeyVerificationMac = "m.key.verification.mac",
|
||||
KeyVerificationDone = "m.key.verification.done",
|
||||
// use of this is discouraged https://matrix.org/docs/spec/client_server/r0.6.1#m-room-message-feedback
|
||||
RoomMessageFeedback = "m.room.message.feedback",
|
||||
Reaction = "m.reaction",
|
||||
|
||||
// Room ephemeral events
|
||||
Typing = "m.typing",
|
||||
@@ -64,6 +73,7 @@ export enum EventType {
|
||||
// Room account_data events
|
||||
FullyRead = "m.fully_read",
|
||||
Tag = "m.tag",
|
||||
SpaceOrder = "org.matrix.msc3230.space_order", // MSC3230
|
||||
|
||||
// User account_data events
|
||||
PushRules = "m.push_rules",
|
||||
@@ -77,6 +87,11 @@ export enum EventType {
|
||||
Dummy = "m.dummy",
|
||||
}
|
||||
|
||||
export enum RelationType {
|
||||
Annotation = "m.annotation",
|
||||
Replace = "m.replace",
|
||||
}
|
||||
|
||||
export enum MsgType {
|
||||
Text = "m.text",
|
||||
Emote = "m.emote",
|
||||
@@ -87,3 +102,59 @@ export enum MsgType {
|
||||
Location = "m.location",
|
||||
Video = "m.video",
|
||||
}
|
||||
|
||||
export const RoomCreateTypeField = "type";
|
||||
|
||||
export enum RoomType {
|
||||
Space = "m.space",
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifier for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088)
|
||||
* room purpose. Note that this reference is UNSTABLE and subject to breaking changes,
|
||||
* including its eventual removal.
|
||||
*/
|
||||
export const UNSTABLE_MSC3088_PURPOSE = new UnstableValue("m.room.purpose", "org.matrix.msc3088.purpose");
|
||||
|
||||
/**
|
||||
* Enabled flag for an [MSC3088](https://github.com/matrix-org/matrix-doc/pull/3088)
|
||||
* room purpose. Note that this reference is UNSTABLE and subject to breaking changes,
|
||||
* including its eventual removal.
|
||||
*/
|
||||
export const UNSTABLE_MSC3088_ENABLED = new UnstableValue("m.enabled", "org.matrix.msc3088.enabled");
|
||||
|
||||
/**
|
||||
* Subtype for an [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room.
|
||||
* Note that this reference is UNSTABLE and subject to breaking changes, including its
|
||||
* eventual removal.
|
||||
*/
|
||||
export const UNSTABLE_MSC3089_TREE_SUBTYPE = new UnstableValue("m.data_tree", "org.matrix.msc3089.data_tree");
|
||||
|
||||
/**
|
||||
* Leaf type for an event in a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room.
|
||||
* Note that this reference is UNSTABLE and subject to breaking changes, including its
|
||||
* eventual removal.
|
||||
*/
|
||||
export const UNSTABLE_MSC3089_LEAF = new UnstableValue("m.leaf", "org.matrix.msc3089.leaf");
|
||||
|
||||
/**
|
||||
* Branch (Leaf Reference) type for the index approach in a
|
||||
* [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) space-room. Note that this reference is
|
||||
* UNSTABLE and subject to breaking changes, including its eventual removal.
|
||||
*/
|
||||
export const UNSTABLE_MSC3089_BRANCH = new UnstableValue("m.branch", "org.matrix.msc3089.branch");
|
||||
|
||||
export interface IEncryptedFile {
|
||||
url: string;
|
||||
mimetype?: string;
|
||||
key: {
|
||||
alg: string;
|
||||
key_ops: string[]; // eslint-disable-line camelcase
|
||||
kty: string;
|
||||
k: string;
|
||||
ext: boolean;
|
||||
};
|
||||
iv: string;
|
||||
hashes: {[alg: string]: string};
|
||||
v: string;
|
||||
}
|
||||
|
||||
Vendored
+54
-2
@@ -15,7 +15,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
// this is needed to tell TS about global.Olm
|
||||
import * as Olm from "olm"; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
import * as Olm from "@matrix-org/olm"; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
|
||||
export {};
|
||||
|
||||
@@ -26,10 +26,49 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
interface Window {
|
||||
electron?: Electron;
|
||||
}
|
||||
|
||||
interface Electron {
|
||||
getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>>;
|
||||
}
|
||||
|
||||
interface MediaDevices {
|
||||
// This is experimental and types don't know about it yet
|
||||
// https://github.com/microsoft/TypeScript/issues/33232
|
||||
getDisplayMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
|
||||
getDisplayMedia(constraints: MediaStreamConstraints | DesktopCapturerConstraints): Promise<MediaStream>;
|
||||
getUserMedia(constraints: MediaStreamConstraints | DesktopCapturerConstraints): Promise<MediaStream>;
|
||||
}
|
||||
|
||||
interface DesktopCapturerConstraints {
|
||||
audio: boolean | {
|
||||
mandatory: {
|
||||
chromeMediaSource: string;
|
||||
chromeMediaSourceId: string;
|
||||
};
|
||||
};
|
||||
video: boolean | {
|
||||
mandatory: {
|
||||
chromeMediaSource: string;
|
||||
chromeMediaSourceId: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface DesktopCapturerSource {
|
||||
id: string;
|
||||
name: string;
|
||||
thumbnailURL: string;
|
||||
}
|
||||
|
||||
interface GetSourcesOptions {
|
||||
types: Array<string>;
|
||||
thumbnailSize?: {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
fetchWindowIcons?: boolean;
|
||||
}
|
||||
|
||||
interface HTMLAudioElement {
|
||||
@@ -45,4 +84,17 @@ declare global {
|
||||
// on webkit: we should check if we still need to do this
|
||||
webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
|
||||
}
|
||||
|
||||
export interface ISettledFulfilled<T> {
|
||||
status: "fulfilled";
|
||||
value: T;
|
||||
}
|
||||
export interface ISettledRejected {
|
||||
status: "rejected";
|
||||
reason: any;
|
||||
}
|
||||
|
||||
interface PromiseConstructor {
|
||||
allSettled<T>(promises: Promise<T>[]): Promise<Array<ISettledFulfilled<T> | ISettledRejected>>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export interface IImageInfo {
|
||||
size?: number;
|
||||
mimetype?: string;
|
||||
thumbnail_info?: { // eslint-disable-line camelcase
|
||||
w?: number;
|
||||
h?: number;
|
||||
size?: number;
|
||||
mimetype?: string;
|
||||
};
|
||||
w?: number;
|
||||
h?: number;
|
||||
}
|
||||
|
||||
export enum Visibility {
|
||||
Public = "public",
|
||||
Private = "private",
|
||||
}
|
||||
|
||||
export enum Preset {
|
||||
PrivateChat = "private_chat",
|
||||
TrustedPrivateChat = "trusted_private_chat",
|
||||
PublicChat = "public_chat",
|
||||
}
|
||||
|
||||
export type ResizeMethod = "crop" | "scale";
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
Copyright 2021 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 { Callback } from "../client";
|
||||
import { Preset, Visibility } from "./partials";
|
||||
|
||||
// allow camelcase as these are things go onto the wire
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
export interface IJoinRoomOpts {
|
||||
/**
|
||||
* True to do a room initial sync on the resulting
|
||||
* room. If false, the <strong>returned Room object will have no current state.
|
||||
* </strong> Default: true.
|
||||
*/
|
||||
syncRoom?: boolean;
|
||||
|
||||
/**
|
||||
* If the caller has a keypair 3pid invite, the signing URL is passed in this parameter.
|
||||
*/
|
||||
inviteSignUrl?: string;
|
||||
|
||||
/**
|
||||
* The server names to try and join through in addition to those that are automatically chosen.
|
||||
*/
|
||||
viaServers?: string[];
|
||||
}
|
||||
|
||||
export interface IRedactOpts {
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export interface ISendEventResponse {
|
||||
event_id: string;
|
||||
}
|
||||
|
||||
export interface IPresenceOpts {
|
||||
presence: "online" | "offline" | "unavailable";
|
||||
status_msg?: string;
|
||||
}
|
||||
|
||||
export interface IPaginateOpts {
|
||||
backwards?: boolean;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface IGuestAccessOpts {
|
||||
allowJoin: boolean;
|
||||
allowRead: boolean;
|
||||
}
|
||||
|
||||
export interface ISearchOpts {
|
||||
keys?: string[];
|
||||
query: string;
|
||||
}
|
||||
|
||||
export interface IEventSearchOpts {
|
||||
filter: any; // TODO: Types
|
||||
term: string;
|
||||
}
|
||||
|
||||
export interface IInvite3PID {
|
||||
id_server: string;
|
||||
id_access_token?: string; // this gets injected by the js-sdk
|
||||
medium: string;
|
||||
address: string;
|
||||
}
|
||||
|
||||
export interface ICreateRoomStateEvent {
|
||||
type: string;
|
||||
state_key?: string; // defaults to an empty string
|
||||
content: object;
|
||||
}
|
||||
|
||||
export interface ICreateRoomOpts {
|
||||
room_alias_name?: string;
|
||||
visibility?: Visibility;
|
||||
name?: string;
|
||||
topic?: string;
|
||||
preset?: Preset;
|
||||
power_level_content_override?: object;
|
||||
creation_content?: object;
|
||||
initial_state?: ICreateRoomStateEvent[];
|
||||
invite?: string[];
|
||||
invite_3pid?: IInvite3PID[];
|
||||
is_direct?: boolean;
|
||||
room_version?: string;
|
||||
}
|
||||
|
||||
export interface IRoomDirectoryOptions {
|
||||
server?: string;
|
||||
limit?: number;
|
||||
since?: string;
|
||||
|
||||
// TODO: Proper types
|
||||
filter?: any & {generic_search_term: string};
|
||||
}
|
||||
|
||||
export interface IUploadOpts {
|
||||
name?: string;
|
||||
includeFilename?: boolean;
|
||||
type?: string;
|
||||
rawResponse?: boolean;
|
||||
onlyContentUri?: boolean;
|
||||
callback?: Callback;
|
||||
progressHandler?: (state: {loaded: number, total: number}) => void;
|
||||
}
|
||||
|
||||
/* eslint-enable camelcase */
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export interface ISignatures {
|
||||
[entity: string]: {
|
||||
[keyId: string]: string;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a simple Matrix namespaced value. This will assume that if a stable prefix
|
||||
* is provided that the stable prefix should be used when representing the identifier.
|
||||
*/
|
||||
export class NamespacedValue<S extends string, U extends string> {
|
||||
// Stable is optional, but one of the two parameters is required, hence the weird-looking types.
|
||||
// Goal is to to have developers explicitly say there is no stable value (if applicable).
|
||||
public constructor(public readonly stable: S | null | undefined, public readonly unstable?: U) {
|
||||
if (!this.unstable && !this.stable) {
|
||||
throw new Error("One of stable or unstable values must be supplied");
|
||||
}
|
||||
}
|
||||
|
||||
public get name(): U | S {
|
||||
if (this.stable) {
|
||||
return this.stable;
|
||||
}
|
||||
return this.unstable;
|
||||
}
|
||||
|
||||
public get altName(): U | S | null {
|
||||
if (!this.stable) {
|
||||
return null;
|
||||
}
|
||||
return this.unstable;
|
||||
}
|
||||
|
||||
public matches(val: string): boolean {
|
||||
return this.name === val || this.altName === val;
|
||||
}
|
||||
|
||||
// this desperately wants https://github.com/microsoft/TypeScript/pull/26349 at the top level of the class
|
||||
// so we can instantiate `NamespacedValue<string, _, _>` as a default type for that namespace.
|
||||
public findIn<T>(obj: any): T {
|
||||
let val: T;
|
||||
if (this.name) {
|
||||
val = obj?.[this.name];
|
||||
}
|
||||
if (!val && this.altName) {
|
||||
val = obj?.[this.altName];
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
public includedIn(arr: any[]): boolean {
|
||||
let included = false;
|
||||
if (this.name) {
|
||||
included = arr.includes(this.name);
|
||||
}
|
||||
if (!included && this.altName) {
|
||||
included = arr.includes(this.altName);
|
||||
}
|
||||
return included;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a namespaced value which prioritizes the unstable value over the stable
|
||||
* value.
|
||||
*/
|
||||
export class UnstableValue<S extends string, U extends string> extends NamespacedValue<S, U> {
|
||||
// Note: Constructor difference is that `unstable` is *required*.
|
||||
public constructor(stable: S, unstable: U) {
|
||||
super(stable, unstable);
|
||||
if (!this.unstable) {
|
||||
throw new Error("Unstable value must be supplied");
|
||||
}
|
||||
}
|
||||
|
||||
public get name(): U {
|
||||
return this.unstable;
|
||||
}
|
||||
|
||||
public get altName(): S {
|
||||
return this.stable;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module
|
||||
*/
|
||||
|
||||
export class ReEmitter {
|
||||
constructor(target) {
|
||||
this.target = target;
|
||||
|
||||
// We keep one bound event handler for each event name so we know
|
||||
// what event is arriving
|
||||
this.boundHandlers = {};
|
||||
}
|
||||
|
||||
_handleEvent(eventName, ...args) {
|
||||
this.target.emit(eventName, ...args);
|
||||
}
|
||||
|
||||
reEmit(source, eventNames) {
|
||||
// We include the source as the last argument for event handlers which may need it,
|
||||
// such as read receipt listeners on the client class which won't have the context
|
||||
// of the room.
|
||||
const forSource = (handler, ...args) => {
|
||||
handler(...args, source);
|
||||
};
|
||||
for (const eventName of eventNames) {
|
||||
if (this.boundHandlers[eventName] === undefined) {
|
||||
this.boundHandlers[eventName] = this._handleEvent.bind(this, eventName);
|
||||
}
|
||||
|
||||
const boundHandler = forSource.bind(this, this.boundHandlers[eventName]);
|
||||
source.on(eventName, boundHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2017 New Vector Ltd
|
||||
|
||||
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";
|
||||
|
||||
export class ReEmitter {
|
||||
private target: EventEmitter;
|
||||
|
||||
constructor(target: EventEmitter) {
|
||||
this.target = target;
|
||||
}
|
||||
|
||||
reEmit(source: EventEmitter, eventNames: string[]) {
|
||||
for (const eventName of eventNames) {
|
||||
// We include the source as the last argument for event handlers which may need it,
|
||||
// such as read receipt listeners on the client class which won't have the context
|
||||
// of the room.
|
||||
const forSource = (...args) => {
|
||||
// EventEmitter special cases 'error' to make the emit function throw if no
|
||||
// handler is attached, which sort of makes sense for making sure that something
|
||||
// handles an error, but for re-emitting, there could be a listener on the original
|
||||
// source object so the test doesn't really work. We *could* try to replicate the
|
||||
// same logic and throw if there is no listener on either the source or the target,
|
||||
// but this behaviour is fairly undesireable for us anyway: the main place we throw
|
||||
// 'error' events is for calls, where error events are usually emitted some time
|
||||
// later by a different part of the code where 'emit' throwing because the app hasn't
|
||||
// added an error handler isn't terribly helpful. (A better fix in retrospect may
|
||||
// have been to just avoid using the event name 'error', but backwards compat...)
|
||||
if (eventName === 'error' && this.target.listenerCount('error') === 0) return;
|
||||
this.target.emit(eventName, ...args, source);
|
||||
};
|
||||
source.on(eventName, forSource);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,8 @@ limitations under the License.
|
||||
|
||||
/** @module auto-discovery */
|
||||
|
||||
import {logger} from './logger';
|
||||
import {URL as NodeURL} from "url";
|
||||
import { logger } from './logger';
|
||||
import { URL as NodeURL } from "url";
|
||||
|
||||
// Dev note: Auto discovery is part of the spec.
|
||||
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
||||
@@ -511,12 +511,12 @@ export class AutoDiscovery {
|
||||
action = "IGNORE";
|
||||
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
|
||||
}
|
||||
resolve({raw: {}, action: action, reason: reason, error: err});
|
||||
resolve({ raw: {}, action: action, reason: reason, error: err });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
resolve({raw: JSON.parse(body), action: "SUCCESS"});
|
||||
resolve({ raw: JSON.parse(body), action: "SUCCESS" });
|
||||
} catch (e) {
|
||||
let reason = AutoDiscovery.ERROR_INVALID;
|
||||
if (e.name === "SyntaxError") {
|
||||
|
||||
-2376
File diff suppressed because it is too large
Load Diff
-5923
File diff suppressed because it is too large
Load Diff
+8494
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018 - 2021 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,7 +23,7 @@ limitations under the License.
|
||||
* @param {string} htmlBody the HTML representation of the message
|
||||
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
||||
*/
|
||||
export function makeHtmlMessage(body, htmlBody) {
|
||||
export function makeHtmlMessage(body: string, htmlBody: string) {
|
||||
return {
|
||||
msgtype: "m.text",
|
||||
format: "org.matrix.custom.html",
|
||||
@@ -38,7 +38,7 @@ export function makeHtmlMessage(body, htmlBody) {
|
||||
* @param {string} htmlBody the HTML representation of the notice
|
||||
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
||||
*/
|
||||
export function makeHtmlNotice(body, htmlBody) {
|
||||
export function makeHtmlNotice(body: string, htmlBody: string) {
|
||||
return {
|
||||
msgtype: "m.notice",
|
||||
format: "org.matrix.custom.html",
|
||||
@@ -53,7 +53,7 @@ export function makeHtmlNotice(body, htmlBody) {
|
||||
* @param {string} htmlBody the HTML representation of the emote
|
||||
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
||||
*/
|
||||
export function makeHtmlEmote(body, htmlBody) {
|
||||
export function makeHtmlEmote(body: string, htmlBody: string) {
|
||||
return {
|
||||
msgtype: "m.emote",
|
||||
format: "org.matrix.custom.html",
|
||||
@@ -67,7 +67,7 @@ export function makeHtmlEmote(body, htmlBody) {
|
||||
* @param {string} body the plaintext body of the emote
|
||||
* @returns {{msgtype: string, body: string}}
|
||||
*/
|
||||
export function makeTextMessage(body) {
|
||||
export function makeTextMessage(body: string) {
|
||||
return {
|
||||
msgtype: "m.text",
|
||||
body: body,
|
||||
@@ -79,7 +79,7 @@ export function makeTextMessage(body) {
|
||||
* @param {string} body the plaintext body of the notice
|
||||
* @returns {{msgtype: string, body: string}}
|
||||
*/
|
||||
export function makeNotice(body) {
|
||||
export function makeNotice(body: string) {
|
||||
return {
|
||||
msgtype: "m.notice",
|
||||
body: body,
|
||||
@@ -91,7 +91,7 @@ export function makeNotice(body) {
|
||||
* @param {string} body the plaintext body of the emote
|
||||
* @returns {{msgtype: string, body: string}}
|
||||
*/
|
||||
export function makeEmoteMessage(body) {
|
||||
export function makeEmoteMessage(body: string) {
|
||||
return {
|
||||
msgtype: "m.emote",
|
||||
body: body,
|
||||
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2015 - 2021 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.
|
||||
@@ -34,8 +33,14 @@ import * as utils from "./utils";
|
||||
* for such URLs.
|
||||
* @return {string} The complete URL to the content.
|
||||
*/
|
||||
export function getHttpUriForMxc(baseUrl, mxc, width, height,
|
||||
resizeMethod, allowDirectLinks) {
|
||||
export function getHttpUriForMxc(
|
||||
baseUrl: string,
|
||||
mxc: string,
|
||||
width: number,
|
||||
height: number,
|
||||
resizeMethod: string,
|
||||
allowDirectLinks = false,
|
||||
): string {
|
||||
if (typeof mxc !== "string" || !mxc) {
|
||||
return '';
|
||||
}
|
||||
@@ -51,15 +56,15 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height,
|
||||
const params = {};
|
||||
|
||||
if (width) {
|
||||
params.width = Math.round(width);
|
||||
params["width"] = Math.round(width);
|
||||
}
|
||||
if (height) {
|
||||
params.height = Math.round(height);
|
||||
params["height"] = Math.round(height);
|
||||
}
|
||||
if (resizeMethod) {
|
||||
params.method = resizeMethod;
|
||||
params["method"] = resizeMethod;
|
||||
}
|
||||
if (utils.keys(params).length > 0) {
|
||||
if (Object.keys(params).length > 0) {
|
||||
// these are thumbnailing params so they probably want the
|
||||
// thumbnailing API...
|
||||
prefix = "/_matrix/media/r0/thumbnail/";
|
||||
@@ -71,7 +76,7 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height,
|
||||
fragment = serverAndMediaId.substr(fragmentOffset);
|
||||
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
|
||||
}
|
||||
return baseUrl + prefix + serverAndMediaId +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params))) + fragment;
|
||||
|
||||
const urlParams = (Object.keys(params).length === 0 ? "" : ("?" + utils.encodeParams(params)));
|
||||
return baseUrl + prefix + serverAndMediaId + urlParams + fragment;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019 - 2021 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,22 +19,43 @@ limitations under the License.
|
||||
* @module crypto/CrossSigning
|
||||
*/
|
||||
|
||||
import {decodeBase64, encodeBase64, pkSign, pkVerify} from './olmlib';
|
||||
import {EventEmitter} from 'events';
|
||||
import {logger} from '../logger';
|
||||
import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store';
|
||||
import {decryptAES, encryptAES} from './aes';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { decodeBase64, encodeBase64, pkSign, pkVerify } from './olmlib';
|
||||
import { logger } from '../logger';
|
||||
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
|
||||
import { decryptAES, encryptAES } from './aes';
|
||||
import { PkSigning } from "@matrix-org/olm";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { SecretStorage } from "./SecretStorage";
|
||||
import { CryptoStore, MatrixClient } from "../client";
|
||||
import { OlmDevice } from "./OlmDevice";
|
||||
import { ICryptoCallbacks } from "../matrix";
|
||||
|
||||
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
|
||||
|
||||
function publicKeyFromKeyInfo(keyInfo) {
|
||||
function publicKeyFromKeyInfo(keyInfo: any): any { // TODO types
|
||||
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
|
||||
// We assume only a single key, and we want the bare form without type
|
||||
// prefix, so we select the values.
|
||||
return Object.values(keyInfo.keys)[0];
|
||||
}
|
||||
|
||||
interface ICacheCallbacks {
|
||||
getCrossSigningKeyCache?(type: string, expectedPublicKey?: string): Promise<Uint8Array>;
|
||||
storeCrossSigningKeyCache?(type: string, key: Uint8Array): Promise<void>;
|
||||
}
|
||||
|
||||
export class CrossSigningInfo extends EventEmitter {
|
||||
public keys: Record<string, any> = {}; // TODO types
|
||||
public firstUse = true;
|
||||
// This tracks whether we've ever verified this user with any identity.
|
||||
// When you verify a user, any devices online at the time that receive
|
||||
// the verifying signature via the homeserver will latch this to true
|
||||
// and can use it in the future to detect cases where the user has
|
||||
// become unverified later for any reason.
|
||||
private crossSigningVerifiedBefore = false;
|
||||
|
||||
/**
|
||||
* Information about a user's cross-signing keys
|
||||
*
|
||||
@@ -46,27 +66,15 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
* Requires getCrossSigningKey and saveCrossSigningKeys
|
||||
* @param {object} cacheCallbacks Callbacks used to interact with the cache
|
||||
*/
|
||||
constructor(userId, callbacks, cacheCallbacks) {
|
||||
constructor(
|
||||
public readonly userId: string,
|
||||
private callbacks: ICryptoCallbacks = {},
|
||||
private cacheCallbacks: ICacheCallbacks = {},
|
||||
) {
|
||||
super();
|
||||
|
||||
// you can't change the userId
|
||||
Object.defineProperty(this, 'userId', {
|
||||
enumerable: true,
|
||||
value: userId,
|
||||
});
|
||||
this._callbacks = callbacks || {};
|
||||
this._cacheCallbacks = cacheCallbacks || {};
|
||||
this.keys = {};
|
||||
this.firstUse = true;
|
||||
// This tracks whether we've ever verified this user with any identity.
|
||||
// When you verify a user, any devices online at the time that receive
|
||||
// the verifying signature via the homeserver will latch this to true
|
||||
// and can use it in the future to detect cases where the user has
|
||||
// become unverifed later for any reason.
|
||||
this.crossSigningVerifiedBefore = false;
|
||||
}
|
||||
|
||||
static fromStorage(obj, userId) {
|
||||
public static fromStorage(obj: object, userId: string): CrossSigningInfo {
|
||||
const res = new CrossSigningInfo(userId);
|
||||
for (const prop in obj) {
|
||||
if (obj.hasOwnProperty(prop)) {
|
||||
@@ -76,7 +84,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
return res;
|
||||
}
|
||||
|
||||
toStorage() {
|
||||
public toStorage(): object {
|
||||
return {
|
||||
keys: this.keys,
|
||||
firstUse: this.firstUse,
|
||||
@@ -92,10 +100,10 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
* the stored public key for the given key type.
|
||||
* @returns {Array} An array with [ public key, Olm.PkSigning ]
|
||||
*/
|
||||
async getCrossSigningKey(type, expectedPubkey) {
|
||||
public async getCrossSigningKey(type: string, expectedPubkey?: string): Promise<[string, PkSigning]> {
|
||||
const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0;
|
||||
|
||||
if (!this._callbacks.getCrossSigningKey) {
|
||||
if (!this.callbacks.getCrossSigningKey) {
|
||||
throw new Error("No getCrossSigningKey callback supplied");
|
||||
}
|
||||
|
||||
@@ -103,7 +111,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
expectedPubkey = this.getId(type);
|
||||
}
|
||||
|
||||
function validateKey(key) {
|
||||
function validateKey(key: Uint8Array): [string, PkSigning] {
|
||||
if (!key) return;
|
||||
const signing = new global.Olm.PkSigning();
|
||||
const gotPubkey = signing.init_with_seed(key);
|
||||
@@ -114,9 +122,8 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
}
|
||||
|
||||
let privkey;
|
||||
if (this._cacheCallbacks.getCrossSigningKeyCache && shouldCache) {
|
||||
privkey = await this._cacheCallbacks
|
||||
.getCrossSigningKeyCache(type, expectedPubkey);
|
||||
if (this.cacheCallbacks.getCrossSigningKeyCache && shouldCache) {
|
||||
privkey = await this.cacheCallbacks.getCrossSigningKeyCache(type, expectedPubkey);
|
||||
}
|
||||
|
||||
const cacheresult = validateKey(privkey);
|
||||
@@ -124,11 +131,11 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
return cacheresult;
|
||||
}
|
||||
|
||||
privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey);
|
||||
privkey = await this.callbacks.getCrossSigningKey(type, expectedPubkey);
|
||||
const result = validateKey(privkey);
|
||||
if (result) {
|
||||
if (this._cacheCallbacks.storeCrossSigningKeyCache && shouldCache) {
|
||||
await this._cacheCallbacks.storeCrossSigningKeyCache(type, privkey);
|
||||
if (this.cacheCallbacks.storeCrossSigningKeyCache && shouldCache) {
|
||||
await this.cacheCallbacks.storeCrossSigningKeyCache(type, privkey);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -156,10 +163,9 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
* with, or null if it is not present or not encrypted with a trusted
|
||||
* key
|
||||
*/
|
||||
async isStoredInSecretStorage(secretStorage) {
|
||||
public async isStoredInSecretStorage(secretStorage: SecretStorage): Promise<Record<string, object>> {
|
||||
// check what SSSS keys have encrypted the master key (if any)
|
||||
const stored =
|
||||
await secretStorage.isStored("m.cross_signing.master", false) || {};
|
||||
const stored = await secretStorage.isStored("m.cross_signing.master", false) || {};
|
||||
// then check which of those SSSS keys have also encrypted the SSK and USK
|
||||
function intersect(s) {
|
||||
for (const k of Object.keys(stored)) {
|
||||
@@ -169,9 +175,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
}
|
||||
}
|
||||
for (const type of ["self_signing", "user_signing"]) {
|
||||
intersect(
|
||||
await secretStorage.isStored(`m.cross_signing.${type}`, false) || {},
|
||||
);
|
||||
intersect(await secretStorage.isStored(`m.cross_signing.${type}`, false) || {});
|
||||
}
|
||||
return Object.keys(stored).length ? stored : null;
|
||||
}
|
||||
@@ -184,7 +188,10 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
* @param {Map} keys The keys to store
|
||||
* @param {SecretStorage} secretStorage The secret store using account data
|
||||
*/
|
||||
static async storeInSecretStorage(keys, secretStorage) {
|
||||
public static async storeInSecretStorage(
|
||||
keys: Map<string, Uint8Array>,
|
||||
secretStorage: SecretStorage,
|
||||
): Promise<void> {
|
||||
for (const [type, privateKey] of keys) {
|
||||
const encodedKey = encodeBase64(privateKey);
|
||||
await secretStorage.store(`m.cross_signing.${type}`, encodedKey);
|
||||
@@ -200,7 +207,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
* @param {SecretStorage} secretStorage The secret store using account data
|
||||
* @return {Uint8Array} The private key
|
||||
*/
|
||||
static async getFromSecretStorage(type, secretStorage) {
|
||||
public static async getFromSecretStorage(type: string, secretStorage: SecretStorage): Promise<Uint8Array> {
|
||||
const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
|
||||
if (!encodedKey) {
|
||||
return null;
|
||||
@@ -215,8 +222,8 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
* "self_signing", or "user_signing". Optional, will check all by default.
|
||||
* @returns {boolean} True if all keys are stored in the local cache.
|
||||
*/
|
||||
async isStoredInKeyCache(type) {
|
||||
const cacheCallbacks = this._cacheCallbacks;
|
||||
public async isStoredInKeyCache(type?: string): Promise<boolean> {
|
||||
const cacheCallbacks = this.cacheCallbacks;
|
||||
if (!cacheCallbacks) return false;
|
||||
const types = type ? [type] : ["master", "self_signing", "user_signing"];
|
||||
for (const t of types) {
|
||||
@@ -232,9 +239,9 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
*
|
||||
* @returns {Map} A map from key type (string) to private key (Uint8Array)
|
||||
*/
|
||||
async getCrossSigningKeysFromCache() {
|
||||
public async getCrossSigningKeysFromCache(): Promise<Map<string, Uint8Array>> {
|
||||
const keys = new Map();
|
||||
const cacheCallbacks = this._cacheCallbacks;
|
||||
const cacheCallbacks = this.cacheCallbacks;
|
||||
if (!cacheCallbacks) return keys;
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
const privKey = await cacheCallbacks.getCrossSigningKeyCache(type);
|
||||
@@ -255,8 +262,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
*
|
||||
* @return {string} the ID
|
||||
*/
|
||||
getId(type) {
|
||||
type = type || "master";
|
||||
public getId(type = "master"): string {
|
||||
if (!this.keys[type]) return null;
|
||||
const keyInfo = this.keys[type];
|
||||
return publicKeyFromKeyInfo(keyInfo);
|
||||
@@ -269,8 +275,8 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
*
|
||||
* @param {CrossSigningLevel} level The key types to reset
|
||||
*/
|
||||
async resetKeys(level) {
|
||||
if (!this._callbacks.saveCrossSigningKeys) {
|
||||
public async resetKeys(level?: CrossSigningLevel): Promise<void> {
|
||||
if (!this.callbacks.saveCrossSigningKeys) {
|
||||
throw new Error("No saveCrossSigningKeys callback supplied");
|
||||
}
|
||||
|
||||
@@ -289,8 +295,8 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
const privateKeys = {};
|
||||
const keys = {};
|
||||
const privateKeys: Record<string, Uint8Array> = {};
|
||||
const keys: Record<string, object> = {};
|
||||
let masterSigning;
|
||||
let masterPub;
|
||||
|
||||
@@ -347,7 +353,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
}
|
||||
|
||||
Object.assign(this.keys, keys);
|
||||
this._callbacks.saveCrossSigningKeys(privateKeys);
|
||||
this.callbacks.saveCrossSigningKeys(privateKeys);
|
||||
} finally {
|
||||
if (masterSigning) {
|
||||
masterSigning.free();
|
||||
@@ -358,12 +364,12 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
/**
|
||||
* unsets the keys, used when another session has reset the keys, to disable cross-signing
|
||||
*/
|
||||
clearKeys() {
|
||||
public clearKeys(): void {
|
||||
this.keys = {};
|
||||
}
|
||||
|
||||
setKeys(keys) {
|
||||
const signingKeys = {};
|
||||
public setKeys(keys: Record<string, any>): void {
|
||||
const signingKeys: Record<string, object> = {};
|
||||
if (keys.master) {
|
||||
if (keys.master.user_id !== this.userId) {
|
||||
const error = "Mismatched user ID " + keys.master.user_id +
|
||||
@@ -434,7 +440,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
updateCrossSigningVerifiedBefore(isCrossSigningVerified) {
|
||||
public updateCrossSigningVerifiedBefore(isCrossSigningVerified: boolean): void {
|
||||
// It is critical that this value latches forward from false to true but
|
||||
// never back to false to avoid a downgrade attack.
|
||||
if (!this.crossSigningVerifiedBefore && isCrossSigningVerified) {
|
||||
@@ -442,7 +448,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async signObject(data, type) {
|
||||
public async signObject<T extends object>(data: T, type: string): Promise<T> {
|
||||
if (!this.keys[type]) {
|
||||
throw new Error(
|
||||
"Attempted to sign with " + type + " key but no such key present",
|
||||
@@ -457,7 +463,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async signUser(key) {
|
||||
public async signUser(key: CrossSigningInfo): Promise<object> {
|
||||
if (!this.keys.user_signing) {
|
||||
logger.info("No user signing key: not signing user");
|
||||
return;
|
||||
@@ -465,7 +471,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
return this.signObject(key.keys.master, "user_signing");
|
||||
}
|
||||
|
||||
async signDevice(userId, device) {
|
||||
public async signDevice(userId: string, device: DeviceInfo): Promise<object> {
|
||||
if (userId !== this.userId) {
|
||||
throw new Error(
|
||||
`Trying to sign ${userId}'s device; can only sign our own device`,
|
||||
@@ -492,7 +498,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
*
|
||||
* @returns {UserTrustLevel}
|
||||
*/
|
||||
checkUserTrust(userCrossSigning) {
|
||||
public checkUserTrust(userCrossSigning: CrossSigningInfo): UserTrustLevel {
|
||||
// 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
|
||||
@@ -530,12 +536,17 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
*
|
||||
* @param {CrossSigningInfo} userCrossSigning Cross signing info for user
|
||||
* @param {module:crypto/deviceinfo} device The device to check
|
||||
* @param {bool} localTrust Whether the device is trusted locally
|
||||
* @param {bool} trustCrossSignedDevices Whether we trust cross signed devices
|
||||
* @param {boolean} localTrust Whether the device is trusted locally
|
||||
* @param {boolean} trustCrossSignedDevices Whether we trust cross signed devices
|
||||
*
|
||||
* @returns {DeviceTrustLevel}
|
||||
*/
|
||||
checkDeviceTrust(userCrossSigning, device, localTrust, trustCrossSignedDevices) {
|
||||
public checkDeviceTrust(
|
||||
userCrossSigning: CrossSigningInfo,
|
||||
device: DeviceInfo,
|
||||
localTrust: boolean,
|
||||
trustCrossSignedDevices: boolean,
|
||||
): DeviceTrustLevel {
|
||||
const userTrust = this.checkUserTrust(userCrossSigning);
|
||||
|
||||
const userSSK = userCrossSigning.keys.self_signing;
|
||||
@@ -552,29 +563,23 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
// 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), userCrossSigning.userId,
|
||||
);
|
||||
pkVerify(deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId);
|
||||
// ...then we trust this device as much as far as we trust the user
|
||||
return DeviceTrustLevel.fromUserTrustLevel(
|
||||
userTrust, localTrust, trustCrossSignedDevices,
|
||||
);
|
||||
return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust, trustCrossSignedDevices);
|
||||
} catch (e) {
|
||||
return new DeviceTrustLevel(
|
||||
false, false, localTrust, trustCrossSignedDevices,
|
||||
);
|
||||
return new DeviceTrustLevel(false, false, localTrust, trustCrossSignedDevices);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {object} Cache callbacks
|
||||
*/
|
||||
getCacheCallbacks() {
|
||||
return this._cacheCallbacks;
|
||||
public getCacheCallbacks(): ICacheCallbacks {
|
||||
return this.cacheCallbacks;
|
||||
}
|
||||
}
|
||||
|
||||
function deviceToObject(device, userId) {
|
||||
function deviceToObject(device: DeviceInfo, userId: string) {
|
||||
return {
|
||||
algorithms: device.algorithms,
|
||||
keys: device.keys,
|
||||
@@ -584,49 +589,49 @@ function deviceToObject(device, userId) {
|
||||
};
|
||||
}
|
||||
|
||||
export const CrossSigningLevel = {
|
||||
MASTER: 4,
|
||||
USER_SIGNING: 2,
|
||||
SELF_SIGNING: 1,
|
||||
};
|
||||
export enum CrossSigningLevel {
|
||||
MASTER = 4,
|
||||
USER_SIGNING = 2,
|
||||
SELF_SIGNING = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents the ways in which we trust a user
|
||||
*/
|
||||
export class UserTrustLevel {
|
||||
constructor(crossSigningVerified, crossSigningVerifiedBefore, tofu) {
|
||||
this._crossSigningVerified = crossSigningVerified;
|
||||
this._crossSigningVerifiedBefore = crossSigningVerifiedBefore;
|
||||
this._tofu = tofu;
|
||||
}
|
||||
constructor(
|
||||
private readonly crossSigningVerified: boolean,
|
||||
private readonly crossSigningVerifiedBefore: boolean,
|
||||
private readonly tofu: boolean,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if this user is verified via any means
|
||||
* @returns {boolean} true if this user is verified via any means
|
||||
*/
|
||||
isVerified() {
|
||||
public isVerified(): boolean {
|
||||
return this.isCrossSigningVerified();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if this user is verified via cross signing
|
||||
* @returns {boolean} true if this user is verified via cross signing
|
||||
*/
|
||||
isCrossSigningVerified() {
|
||||
return this._crossSigningVerified;
|
||||
public isCrossSigningVerified(): boolean {
|
||||
return this.crossSigningVerified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if we ever verified this user before (at least for
|
||||
* @returns {boolean} true if we ever verified this user before (at least for
|
||||
* the history of verifications observed by this device).
|
||||
*/
|
||||
wasCrossSigningVerified() {
|
||||
return this._crossSigningVerifiedBefore;
|
||||
public wasCrossSigningVerified(): boolean {
|
||||
return this.crossSigningVerifiedBefore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if this user's key is trusted on first use
|
||||
* @returns {boolean} true if this user's key is trusted on first use
|
||||
*/
|
||||
isTofu() {
|
||||
return this._tofu;
|
||||
public isTofu(): boolean {
|
||||
return this.tofu;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,58 +639,62 @@ export class UserTrustLevel {
|
||||
* Represents the ways in which we trust a device
|
||||
*/
|
||||
export class DeviceTrustLevel {
|
||||
constructor(crossSigningVerified, tofu, localVerified, trustCrossSignedDevices) {
|
||||
this._crossSigningVerified = crossSigningVerified;
|
||||
this._tofu = tofu;
|
||||
this._localVerified = localVerified;
|
||||
this._trustCrossSignedDevices = trustCrossSignedDevices;
|
||||
}
|
||||
constructor(
|
||||
public readonly crossSigningVerified: boolean,
|
||||
public readonly tofu: boolean,
|
||||
private readonly localVerified: boolean,
|
||||
private readonly trustCrossSignedDevices: boolean,
|
||||
) {}
|
||||
|
||||
static fromUserTrustLevel(userTrustLevel, localVerified, trustCrossSignedDevices) {
|
||||
public static fromUserTrustLevel(
|
||||
userTrustLevel: UserTrustLevel,
|
||||
localVerified: boolean,
|
||||
trustCrossSignedDevices: boolean,
|
||||
): DeviceTrustLevel {
|
||||
return new DeviceTrustLevel(
|
||||
userTrustLevel._crossSigningVerified,
|
||||
userTrustLevel._tofu,
|
||||
userTrustLevel.isCrossSigningVerified(),
|
||||
userTrustLevel.isTofu(),
|
||||
localVerified,
|
||||
trustCrossSignedDevices,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if this device is verified via any means
|
||||
* @returns {boolean} true if this device is verified via any means
|
||||
*/
|
||||
isVerified() {
|
||||
public isVerified(): boolean {
|
||||
return Boolean(this.isLocallyVerified() || (
|
||||
this._trustCrossSignedDevices && this.isCrossSigningVerified()
|
||||
this.trustCrossSignedDevices && this.isCrossSigningVerified()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if this device is verified via cross signing
|
||||
* @returns {boolean} true if this device is verified via cross signing
|
||||
*/
|
||||
isCrossSigningVerified() {
|
||||
return this._crossSigningVerified;
|
||||
public isCrossSigningVerified(): boolean {
|
||||
return this.crossSigningVerified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if this device is verified locally
|
||||
* @returns {boolean} true if this device is verified locally
|
||||
*/
|
||||
isLocallyVerified() {
|
||||
return this._localVerified;
|
||||
public isLocallyVerified(): boolean {
|
||||
return this.localVerified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if this device is trusted from a user's key
|
||||
* @returns {boolean} true if this device is trusted from a user's key
|
||||
* that is trusted on first use
|
||||
*/
|
||||
isTofu() {
|
||||
return this._tofu;
|
||||
public isTofu(): boolean {
|
||||
return this.tofu;
|
||||
}
|
||||
}
|
||||
|
||||
export function createCryptoStoreCacheCallbacks(store, olmdevice) {
|
||||
export function createCryptoStoreCacheCallbacks(store: CryptoStore, olmDevice: OlmDevice): ICacheCallbacks {
|
||||
return {
|
||||
getCrossSigningKeyCache: async function(type, _expectedPublicKey) {
|
||||
const key = await new Promise((resolve) => {
|
||||
getCrossSigningKeyCache: async function(type: string, _expectedPublicKey: string): Promise<Uint8Array> {
|
||||
const key = await new Promise<any>((resolve) => {
|
||||
return store.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
@@ -696,20 +705,20 @@ export function createCryptoStoreCacheCallbacks(store, olmdevice) {
|
||||
});
|
||||
|
||||
if (key && key.ciphertext) {
|
||||
const pickleKey = Buffer.from(olmdevice._pickleKey);
|
||||
const pickleKey = Buffer.from(olmDevice._pickleKey);
|
||||
const decrypted = await decryptAES(key, pickleKey, type);
|
||||
return decodeBase64(decrypted);
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
},
|
||||
storeCrossSigningKeyCache: async function(type, key) {
|
||||
storeCrossSigningKeyCache: async function(type: string, key: Uint8Array): Promise<void> {
|
||||
if (!(key instanceof Uint8Array)) {
|
||||
throw new Error(
|
||||
`storeCrossSigningKeyCache expects Uint8Array, got ${key}`,
|
||||
);
|
||||
}
|
||||
const pickleKey = Buffer.from(olmdevice._pickleKey);
|
||||
const pickleKey = Buffer.from(olmDevice._pickleKey);
|
||||
key = await encryptAES(encodeBase64(key), pickleKey, type);
|
||||
return store.doTxn(
|
||||
'readwrite',
|
||||
@@ -725,11 +734,11 @@ export function createCryptoStoreCacheCallbacks(store, olmdevice) {
|
||||
/**
|
||||
* Request cross-signing keys from another device during verification.
|
||||
*
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis base Matrix API interface
|
||||
* @param {MatrixClient} baseApis base Matrix API interface
|
||||
* @param {string} userId The user ID being verified
|
||||
* @param {string} deviceId The device ID being verified
|
||||
*/
|
||||
export async function requestKeysDuringVerification(baseApis, userId, deviceId) {
|
||||
export async function requestKeysDuringVerification(baseApis: MatrixClient, userId: string, deviceId: string) {
|
||||
// If this is a self-verification, ask the other party for keys
|
||||
if (baseApis.getUserId() !== userId) {
|
||||
return;
|
||||
@@ -739,7 +748,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
|
||||
// it. We return here in order to test.
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = baseApis;
|
||||
const original = client._crypto._crossSigningInfo;
|
||||
const original = client.crypto.crossSigningInfo;
|
||||
|
||||
// We already have all of the infrastructure we need to validate and
|
||||
// cache cross-signing keys, so instead of replicating that, here we set
|
||||
@@ -748,8 +757,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
|
||||
const crossSigning = new CrossSigningInfo(
|
||||
original.userId,
|
||||
{ getCrossSigningKey: async (type) => {
|
||||
logger.debug("Cross-signing: requesting secret",
|
||||
type, deviceId);
|
||||
logger.debug("Cross-signing: requesting secret", type, deviceId);
|
||||
const { promise } = client.requestSecret(
|
||||
`m.cross_signing.${type}`, [deviceId],
|
||||
);
|
||||
@@ -757,7 +765,7 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
|
||||
const decoded = decodeBase64(result);
|
||||
return Uint8Array.from(decoded);
|
||||
} },
|
||||
original._cacheCallbacks,
|
||||
original.getCacheCallbacks(),
|
||||
);
|
||||
crossSigning.keys = original.keys;
|
||||
|
||||
@@ -774,8 +782,9 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
|
||||
});
|
||||
|
||||
// also request and cache the key backup key
|
||||
const backupKeyPromise = new Promise(async resolve => {
|
||||
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
const backupKeyPromise = new Promise<void>(async resolve => {
|
||||
const cachedKey = await client.crypto.getSessionBackupPrivateKey();
|
||||
if (!cachedKey) {
|
||||
logger.info("No cached backup key found. Requesting...");
|
||||
const secretReq = client.requestSecret(
|
||||
@@ -785,15 +794,13 @@ export async function requestKeysDuringVerification(baseApis, userId, deviceId)
|
||||
logger.info("Got key backup key, decoding...");
|
||||
const decodedKey = decodeBase64(base64Key);
|
||||
logger.info("Decoded backup key, storing...");
|
||||
client._crypto.storeSessionBackupPrivateKey(
|
||||
client.crypto.storeSessionBackupPrivateKey(
|
||||
Uint8Array.from(decodedKey),
|
||||
);
|
||||
logger.info("Backup key stored. Starting backup restore...");
|
||||
const backupInfo = await client.getKeyBackupVersion();
|
||||
// no need to await for this - just let it go in the bg
|
||||
client.restoreKeyBackupWithCache(
|
||||
undefined, undefined, backupInfo,
|
||||
).then(() => {
|
||||
client.restoreKeyBackupWithCache(undefined, undefined, backupInfo).then(() => {
|
||||
logger.info("Backup restored.");
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
import { logger } from "../logger";
|
||||
import {MatrixEvent} from "../models/event";
|
||||
import {EventEmitter} from "events";
|
||||
import {createCryptoStoreCacheCallbacks} from "./CrossSigning";
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
import { MatrixEvent } from "../models/event";
|
||||
import { EventEmitter } from "events";
|
||||
import { createCryptoStoreCacheCallbacks } from "./CrossSigning";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import {
|
||||
PREFIX_UNSTABLE,
|
||||
} from "../http-api";
|
||||
@@ -43,7 +43,7 @@ export class EncryptionSetupBuilder {
|
||||
* @param {Object} keys the new keys
|
||||
*/
|
||||
addCrossSigningKeys(authUpload, keys) {
|
||||
this._crossSigningKeys = {authUpload, keys};
|
||||
this._crossSigningKeys = { authUpload, keys };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -86,7 +86,6 @@ export class EncryptionSetupBuilder {
|
||||
userSignatures[deviceId] = signature;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @param {Object} content
|
||||
@@ -122,18 +121,17 @@ export class EncryptionSetupBuilder {
|
||||
async persist(crypto) {
|
||||
// store private keys in cache
|
||||
if (this._crossSigningKeys) {
|
||||
const cacheCallbacks = createCryptoStoreCacheCallbacks(
|
||||
crypto._cryptoStore, crypto._olmDevice);
|
||||
const cacheCallbacks = createCryptoStoreCacheCallbacks(crypto.cryptoStore, crypto.olmDevice);
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
logger.log(`Cache ${type} cross-signing private key locally`);
|
||||
const privateKey = this.crossSigningCallbacks.privateKeys.get(type);
|
||||
await cacheCallbacks.storeCrossSigningKeyCache(type, privateKey);
|
||||
}
|
||||
// store own cross-sign pubkeys as trusted
|
||||
await crypto._cryptoStore.doTxn(
|
||||
await crypto.cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
crypto._cryptoStore.storeCrossSigningKeys(
|
||||
crypto.cryptoStore.storeCrossSigningKeys(
|
||||
txn, this._crossSigningKeys.keys);
|
||||
},
|
||||
);
|
||||
@@ -170,7 +168,7 @@ export class EncryptionSetupOperation {
|
||||
* @param {Crypto} crypto
|
||||
*/
|
||||
async apply(crypto) {
|
||||
const baseApis = crypto._baseApis;
|
||||
const baseApis = crypto.baseApis;
|
||||
// upload cross-signing keys
|
||||
if (this._crossSigningKeys) {
|
||||
const keys = {};
|
||||
@@ -185,7 +183,7 @@ export class EncryptionSetupOperation {
|
||||
});
|
||||
|
||||
// pass the new keys to the main instance of our own CrossSigningInfo.
|
||||
crypto._crossSigningInfo.setKeys(this._crossSigningKeys.keys);
|
||||
crypto.crossSigningInfo.setKeys(this._crossSigningKeys.keys);
|
||||
}
|
||||
// set account data
|
||||
if (this._accountData) {
|
||||
@@ -205,27 +203,26 @@ export class EncryptionSetupOperation {
|
||||
// The backup is trusted because the user provided the private key.
|
||||
// Sign the backup with the cross signing key so the key backup can
|
||||
// be trusted via cross-signing.
|
||||
await baseApis._http.authedRequest(
|
||||
await baseApis.http.authedRequest(
|
||||
undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version,
|
||||
undefined, {
|
||||
algorithm: this._keyBackupInfo.algorithm,
|
||||
auth_data: this._keyBackupInfo.auth_data,
|
||||
},
|
||||
{prefix: PREFIX_UNSTABLE},
|
||||
{ prefix: PREFIX_UNSTABLE },
|
||||
);
|
||||
} else {
|
||||
// add new key backup
|
||||
await baseApis._http.authedRequest(
|
||||
await baseApis.http.authedRequest(
|
||||
undefined, "POST", "/room_keys/version",
|
||||
undefined, this._keyBackupInfo,
|
||||
{prefix: PREFIX_UNSTABLE},
|
||||
{ prefix: PREFIX_UNSTABLE },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Catches account data set by SecretStorage during bootstrapping by
|
||||
* implementing the methods related to account data in MatrixClient
|
||||
@@ -276,7 +273,7 @@ class AccountDataClientAdapter extends EventEmitter {
|
||||
// as SecretStorage listens for it while calling this method
|
||||
// and it seems to rely on this.
|
||||
return Promise.resolve().then(() => {
|
||||
const event = new MatrixEvent({type, content});
|
||||
const event = new MatrixEvent({ type, content });
|
||||
this.emit("accountData", event, lastEvent);
|
||||
});
|
||||
}
|
||||
@@ -335,7 +332,7 @@ class SSSSCryptoCallbacks {
|
||||
// for it to the general crypto callbacks and cache it
|
||||
if (this._delegateCryptoCallbacks) {
|
||||
const result = await this._delegateCryptoCallbacks.
|
||||
getSecretStorageKey({keys}, name);
|
||||
getSecretStorageKey({ keys }, name);
|
||||
if (result) {
|
||||
const [keyId, privateKey] = result;
|
||||
this._privateKeys.set(keyId, privateKey);
|
||||
|
||||
+49
-20
@@ -16,8 +16,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from '../logger';
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
import { logger } from '../logger';
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import * as algorithms from './algorithms';
|
||||
|
||||
// The maximum size of an event is 65K, and we base64 the content, so this is a
|
||||
@@ -48,7 +48,6 @@ function checkPayloadLength(payloadString) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The type of object we use for importing and exporting megolm session data.
|
||||
*
|
||||
@@ -62,7 +61,6 @@ function checkPayloadLength(payloadString) {
|
||||
* @property {String} session_key Base64'ed key data
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Manages the olm cryptography functions. Each OlmDevice has a single
|
||||
* OlmAccount and a number of OlmSessions.
|
||||
@@ -350,7 +348,7 @@ OlmDevice.prototype._unpickleSession = function(sessionInfo, func) {
|
||||
const session = new global.Olm.Session();
|
||||
try {
|
||||
session.unpickle(this._pickleKey, sessionInfo.session);
|
||||
const unpickledSessInfo = Object.assign({}, sessionInfo, {session});
|
||||
const unpickledSessInfo = Object.assign({}, sessionInfo, { session });
|
||||
|
||||
func(unpickledSessInfo);
|
||||
} finally {
|
||||
@@ -376,7 +374,6 @@ OlmDevice.prototype._saveSession = function(deviceKey, sessionInfo, txn) {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* get an OlmUtility and call the given function
|
||||
*
|
||||
@@ -393,7 +390,6 @@ OlmDevice.prototype._getUtility = function(func) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Signs a message with the ed25519 key for this account.
|
||||
*
|
||||
@@ -434,7 +430,6 @@ OlmDevice.prototype.getOneTimeKeys = async function() {
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the maximum number of one-time keys we can store.
|
||||
*
|
||||
@@ -545,11 +540,11 @@ OlmDevice.prototype.createOutboundSession = async function(
|
||||
}
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[createOutboundSession]"),
|
||||
);
|
||||
return newSessionId;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Generate a new inbound session, given an incoming message
|
||||
*
|
||||
@@ -605,12 +600,12 @@ OlmDevice.prototype.createInboundSession = async function(
|
||||
}
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[createInboundSession]"),
|
||||
);
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get a list of known session IDs for the given device
|
||||
*
|
||||
@@ -619,8 +614,10 @@ OlmDevice.prototype.createInboundSession = async function(
|
||||
* @return {Promise<string[]>} a list of known session ids for the device
|
||||
*/
|
||||
OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityKey) {
|
||||
const log = logger.withPrefix("[getSessionIdsForDevice]");
|
||||
|
||||
if (this._sessionsInProgress[theirDeviceIdentityKey]) {
|
||||
logger.log("waiting for olm session to be created");
|
||||
log.debug(`Waiting for Olm session for ${theirDeviceIdentityKey} to be created`);
|
||||
try {
|
||||
await this._sessionsInProgress[theirDeviceIdentityKey];
|
||||
} catch (e) {
|
||||
@@ -638,6 +635,7 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
|
||||
},
|
||||
);
|
||||
},
|
||||
log,
|
||||
);
|
||||
|
||||
return sessionIds;
|
||||
@@ -651,13 +649,14 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
|
||||
* @param {boolean} nowait Don't wait for an in-progress session to complete.
|
||||
* This should only be set to true of the calling function is the function
|
||||
* that marked the session as being in-progress.
|
||||
* @param {Logger} [log] A possibly customised log
|
||||
* @return {Promise<?string>} session id, or null if no established session
|
||||
*/
|
||||
OlmDevice.prototype.getSessionIdForDevice = async function(
|
||||
theirDeviceIdentityKey, nowait,
|
||||
theirDeviceIdentityKey, nowait, log,
|
||||
) {
|
||||
const sessionInfos = await this.getSessionInfoForDevice(
|
||||
theirDeviceIdentityKey, nowait,
|
||||
theirDeviceIdentityKey, nowait, log,
|
||||
);
|
||||
|
||||
if (sessionInfos.length === 0) {
|
||||
@@ -697,11 +696,16 @@ OlmDevice.prototype.getSessionIdForDevice = async function(
|
||||
* @param {boolean} nowait Don't wait for an in-progress session to complete.
|
||||
* This should only be set to true of the calling function is the function
|
||||
* that marked the session as being in-progress.
|
||||
* @param {Logger} [log] A possibly customised log
|
||||
* @return {Array.<{sessionId: string, hasReceivedMessage: Boolean}>}
|
||||
*/
|
||||
OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey, nowait) {
|
||||
OlmDevice.prototype.getSessionInfoForDevice = async function(
|
||||
deviceIdentityKey, nowait, log = logger,
|
||||
) {
|
||||
log = log.withPrefix("[getSessionInfoForDevice]");
|
||||
|
||||
if (this._sessionsInProgress[deviceIdentityKey] && !nowait) {
|
||||
logger.log("waiting for olm session to be created");
|
||||
log.debug(`Waiting for Olm session for ${deviceIdentityKey} to be created`);
|
||||
try {
|
||||
await this._sessionsInProgress[deviceIdentityKey];
|
||||
} catch (e) {
|
||||
@@ -727,6 +731,7 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey,
|
||||
}
|
||||
});
|
||||
},
|
||||
log,
|
||||
);
|
||||
|
||||
return info;
|
||||
@@ -761,6 +766,7 @@ OlmDevice.prototype.encryptMessage = async function(
|
||||
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[encryptMessage]"),
|
||||
);
|
||||
return res;
|
||||
};
|
||||
@@ -794,6 +800,7 @@ OlmDevice.prototype.decryptMessage = async function(
|
||||
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[decryptMessage]"),
|
||||
);
|
||||
return payloadString;
|
||||
};
|
||||
@@ -825,6 +832,7 @@ OlmDevice.prototype.matchesSession = async function(
|
||||
matches = sessionInfo.session.matches_inbound(ciphertext);
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[matchesSession]"),
|
||||
);
|
||||
return matches;
|
||||
};
|
||||
@@ -841,7 +849,6 @@ OlmDevice.prototype.filterOutNotifiedErrorDevices = async function(devices) {
|
||||
return await this._cryptoStore.filterOutNotifiedErrorDevices(devices);
|
||||
};
|
||||
|
||||
|
||||
// Outbound group session
|
||||
// ======================
|
||||
|
||||
@@ -856,7 +863,6 @@ OlmDevice.prototype._saveOutboundGroupSession = function(session) {
|
||||
this._outboundGroupSessionStore[session.session_id()] = pickledSession;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* extract an OutboundGroupSession from _outboundGroupSessionStore and call the
|
||||
* given function
|
||||
@@ -881,7 +887,6 @@ OlmDevice.prototype._getOutboundGroupSession = function(sessionId, func) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Generate a new outbound group session
|
||||
*
|
||||
@@ -898,7 +903,6 @@ OlmDevice.prototype.createOutboundGroupSession = function() {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Encrypt an outgoing message with an outbound group session
|
||||
*
|
||||
@@ -938,7 +942,6 @@ OlmDevice.prototype.getOutboundGroupSessionKey = function(sessionId) {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Inbound group session
|
||||
// =====================
|
||||
|
||||
@@ -1033,6 +1036,7 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
'readwrite', [
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
|
||||
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
|
||||
], (txn) => {
|
||||
/* if we already have this session, consider updating it */
|
||||
this._getInboundGroupSession(
|
||||
@@ -1089,12 +1093,19 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
this._cryptoStore.storeEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, sessionData, txn,
|
||||
);
|
||||
|
||||
if (!existingSession && extraSessionData.sharedHistory) {
|
||||
this._cryptoStore.addSharedHistoryInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
logger.withPrefix("[addInboundGroupSession]"),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1265,6 +1276,7 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
},
|
||||
);
|
||||
},
|
||||
logger.withPrefix("[decryptGroupMessage]"),
|
||||
);
|
||||
|
||||
if (error) {
|
||||
@@ -1310,6 +1322,7 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
|
||||
},
|
||||
);
|
||||
},
|
||||
logger.withPrefix("[hasInboundSessionKeys]"),
|
||||
);
|
||||
|
||||
return result;
|
||||
@@ -1365,10 +1378,12 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
|
||||
"forwarding_curve25519_key_chain":
|
||||
sessionData.forwardingCurve25519KeyChain || [],
|
||||
"sender_claimed_ed25519_key": senderEd25519Key,
|
||||
"shared_history": sessionData.sharedHistory || false,
|
||||
};
|
||||
},
|
||||
);
|
||||
},
|
||||
logger.withPrefix("[getInboundGroupSessionKey]"),
|
||||
);
|
||||
|
||||
return result;
|
||||
@@ -1396,10 +1411,24 @@ OlmDevice.prototype.exportInboundGroupSession = function(
|
||||
"session_key": session.export_session(messageIndex),
|
||||
"forwarding_curve25519_key_chain": session.forwardingCurve25519KeyChain || [],
|
||||
"first_known_index": session.first_known_index(),
|
||||
"org.matrix.msc3061.shared_history": sessionData.sharedHistory || false,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
OlmDevice.prototype.getSharedHistoryInboundGroupSessions = async function(roomId) {
|
||||
let result;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [
|
||||
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS,
|
||||
], (txn) => {
|
||||
result = this._cryptoStore.getSharedHistoryInboundGroupSessions(roomId, txn);
|
||||
},
|
||||
logger.withPrefix("[getSharedHistoryInboundGroupSessionsForRoom]"),
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
// Utilities
|
||||
// =========
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from '../logger';
|
||||
import * as utils from '../utils';
|
||||
import { logger } from '../logger';
|
||||
|
||||
/**
|
||||
* Internal module. Management of outgoing room key requests.
|
||||
@@ -496,7 +495,7 @@ function stringifyRequestBody(requestBody) {
|
||||
|
||||
function stringifyRecipientList(recipients) {
|
||||
return '['
|
||||
+ utils.map(recipients, (r) => `${r.userId}:${r.deviceId}`).join(",")
|
||||
+ recipients.map((r) => `${r.userId}:${r.deviceId}`).join(",")
|
||||
+ ']';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
/*
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module crypto/RoomList
|
||||
*
|
||||
* Manages the list of encrypted rooms
|
||||
*/
|
||||
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
|
||||
/**
|
||||
* @alias module:crypto/RoomList
|
||||
*/
|
||||
export class RoomList {
|
||||
constructor(cryptoStore) {
|
||||
this._cryptoStore = cryptoStore;
|
||||
|
||||
// Object of roomId -> room e2e info object (body of the m.room.encryption event)
|
||||
this._roomEncryption = {};
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
|
||||
this._cryptoStore.getEndToEndRooms(txn, (result) => {
|
||||
this._roomEncryption = result;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
getRoomEncryption(roomId) {
|
||||
return this._roomEncryption[roomId] || null;
|
||||
}
|
||||
|
||||
isRoomEncrypted(roomId) {
|
||||
return Boolean(this.getRoomEncryption(roomId));
|
||||
}
|
||||
|
||||
async setRoomEncryption(roomId, roomInfo) {
|
||||
// important that this happens before calling into the store
|
||||
// as it prevents the Crypto::setRoomEncryption from calling
|
||||
// this twice for consecutive m.room.encryption events
|
||||
this._roomEncryption[roomId] = roomInfo;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
|
||||
this._cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2018 - 2021 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module crypto/RoomList
|
||||
*
|
||||
* Manages the list of encrypted rooms
|
||||
*/
|
||||
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { CryptoStore } from "../client";
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
interface IRoomEncryption {
|
||||
algorithm: string;
|
||||
rotation_period_ms: number;
|
||||
rotation_period_msgs: number;
|
||||
}
|
||||
/* eslint-enable camelcase */
|
||||
|
||||
/**
|
||||
* @alias module:crypto/RoomList
|
||||
*/
|
||||
export class RoomList {
|
||||
// Object of roomId -> room e2e info object (body of the m.room.encryption event)
|
||||
private roomEncryption: Record<string, IRoomEncryption> = {};
|
||||
|
||||
constructor(private readonly cryptoStore: CryptoStore) {}
|
||||
|
||||
public async init(): Promise<void> {
|
||||
await this.cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
|
||||
this.cryptoStore.getEndToEndRooms(txn, (result) => {
|
||||
this.roomEncryption = result;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
public getRoomEncryption(roomId: string): IRoomEncryption {
|
||||
return this.roomEncryption[roomId] || null;
|
||||
}
|
||||
|
||||
public isRoomEncrypted(roomId: string): boolean {
|
||||
return Boolean(this.getRoomEncryption(roomId));
|
||||
}
|
||||
|
||||
public async setRoomEncryption(roomId: string, roomInfo: IRoomEncryption): Promise<void> {
|
||||
// important that this happens before calling into the store
|
||||
// as it prevents the Crypto::setRoomEncryption from calling
|
||||
// this twice for consecutive m.room.encryption events
|
||||
this.roomEncryption[roomId] = roomInfo;
|
||||
await this.cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ROOMS], (txn) => {
|
||||
this.cryptoStore.storeEndToEndRoom(roomId, roomInfo, txn);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+15
-15
@@ -14,12 +14,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
import {logger} from '../logger';
|
||||
import { EventEmitter } from 'events';
|
||||
import { logger } from '../logger';
|
||||
import * as olmlib from './olmlib';
|
||||
import {randomString} from '../randomstring';
|
||||
import {encryptAES, decryptAES} from './aes';
|
||||
import {encodeBase64} from "./olmlib";
|
||||
import { randomString } from '../randomstring';
|
||||
import { encryptAES, decryptAES } from './aes';
|
||||
import { encodeBase64 } from "./olmlib";
|
||||
|
||||
export const SECRET_STORAGE_ALGORITHM_V1_AES
|
||||
= "m.secret_storage.v1.aes-hmac-sha2";
|
||||
@@ -86,7 +86,7 @@ export class SecretStorage extends EventEmitter {
|
||||
* keyInfo: {object} details about the key (iv, mac, passphrase)
|
||||
*/
|
||||
async addKey(algorithm, opts, keyId) {
|
||||
const keyInfo = {algorithm};
|
||||
const keyInfo = { algorithm };
|
||||
|
||||
if (!opts) opts = {};
|
||||
|
||||
@@ -99,7 +99,7 @@ export class SecretStorage extends EventEmitter {
|
||||
keyInfo.passphrase = opts.passphrase;
|
||||
}
|
||||
if (opts.key) {
|
||||
const {iv, mac} = await SecretStorage._calculateKeyCheck(opts.key);
|
||||
const { iv, mac } = await SecretStorage._calculateKeyCheck(opts.key);
|
||||
keyInfo.iv = iv;
|
||||
keyInfo.mac = mac;
|
||||
}
|
||||
@@ -171,7 +171,7 @@ export class SecretStorage extends EventEmitter {
|
||||
async checkKey(key, info) {
|
||||
if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
if (info.mac) {
|
||||
const {mac} = await SecretStorage._calculateKeyCheck(key, info.iv);
|
||||
const { mac } = await SecretStorage._calculateKeyCheck(key, info.iv);
|
||||
return info.mac.replace(/=+$/g, '') === mac.replace(/=+$/g, '');
|
||||
} else {
|
||||
// if we have no information, we have to assume the key is right
|
||||
@@ -220,7 +220,7 @@ export class SecretStorage extends EventEmitter {
|
||||
|
||||
// encrypt secret, based on the algorithm
|
||||
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
const keys = {[keyId]: keyInfo};
|
||||
const keys = { [keyId]: keyInfo };
|
||||
const [, encryption] = await this._getSecretStorageKey(keys, name);
|
||||
encrypted[keyId] = await encryption.encrypt(secret);
|
||||
} else {
|
||||
@@ -231,7 +231,7 @@ export class SecretStorage extends EventEmitter {
|
||||
}
|
||||
|
||||
// save encrypted secret
|
||||
await this._baseApis.setAccountData(name, {encrypted});
|
||||
await this._baseApis.setAccountData(name, { encrypted });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -444,7 +444,7 @@ export class SecretStorage extends EventEmitter {
|
||||
&& this._incomingRequests[deviceId][content.request_id]) {
|
||||
logger.info("received request cancellation for secret (" + sender
|
||||
+ ", " + deviceId + ", " + content.request_id + ")");
|
||||
this.baseApis.emit("crypto.secrets.requestCancelled", {
|
||||
this._baseApis.emit("crypto.secrets.requestCancelled", {
|
||||
user_id: sender,
|
||||
device_id: deviceId,
|
||||
request_id: content.request_id,
|
||||
@@ -480,11 +480,11 @@ export class SecretStorage extends EventEmitter {
|
||||
};
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this._baseApis._crypto._olmDevice.deviceCurve25519Key,
|
||||
sender_key: this._baseApis.crypto.olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this._baseApis._crypto._olmDevice,
|
||||
this._baseApis.crypto.olmDevice,
|
||||
this._baseApis,
|
||||
{
|
||||
[sender]: [
|
||||
@@ -496,7 +496,7 @@ export class SecretStorage extends EventEmitter {
|
||||
encryptedContent.ciphertext,
|
||||
this._baseApis.getUserId(),
|
||||
this._baseApis.deviceId,
|
||||
this._baseApis._crypto._olmDevice,
|
||||
this._baseApis.crypto.olmDevice,
|
||||
sender,
|
||||
this._baseApis.getStoredDevice(sender, deviceId),
|
||||
payload,
|
||||
@@ -527,7 +527,7 @@ export class SecretStorage extends EventEmitter {
|
||||
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(
|
||||
const deviceInfo = this._baseApis.crypto.deviceList.getDeviceByIdentityKey(
|
||||
olmlib.OLM_ALGORITHM,
|
||||
event.getSenderKey(),
|
||||
);
|
||||
|
||||
+7
-7
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {getCrypto} from '../utils';
|
||||
import {decodeBase64, encodeBase64} from './olmlib';
|
||||
import { getCrypto } from '../utils';
|
||||
import { decodeBase64, encodeBase64 } from './olmlib';
|
||||
|
||||
const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
|
||||
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
|
||||
@@ -148,7 +148,7 @@ async function encryptBrowser(data, key, name, ivStr) {
|
||||
);
|
||||
|
||||
const hmac = await subtleCrypto.sign(
|
||||
{name: 'HMAC'},
|
||||
{ name: 'HMAC' },
|
||||
hmacKey,
|
||||
ciphertext,
|
||||
);
|
||||
@@ -176,7 +176,7 @@ async function decryptBrowser(data, key, name) {
|
||||
const ciphertext = decodeBase64(data.ciphertext);
|
||||
|
||||
if (!await subtleCrypto.verify(
|
||||
{name: "HMAC"},
|
||||
{ name: "HMAC" },
|
||||
hmacKey,
|
||||
decodeBase64(data.mac),
|
||||
ciphertext,
|
||||
@@ -201,7 +201,7 @@ async function deriveKeysBrowser(key, name) {
|
||||
const hkdfkey = await subtleCrypto.importKey(
|
||||
'raw',
|
||||
key,
|
||||
{name: "HKDF"},
|
||||
{ name: "HKDF" },
|
||||
false,
|
||||
["deriveBits"],
|
||||
);
|
||||
@@ -222,7 +222,7 @@ async function deriveKeysBrowser(key, name) {
|
||||
const aesProm = subtleCrypto.importKey(
|
||||
'raw',
|
||||
aesKey,
|
||||
{name: 'AES-CTR'},
|
||||
{ name: 'AES-CTR' },
|
||||
false,
|
||||
['encrypt', 'decrypt'],
|
||||
);
|
||||
@@ -232,7 +232,7 @@ async function deriveKeysBrowser(key, name) {
|
||||
hmacKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: {name: 'SHA-256'},
|
||||
hash: { name: 'SHA-256' },
|
||||
},
|
||||
false,
|
||||
['sign', 'verify'],
|
||||
|
||||
@@ -46,7 +46,7 @@ export const DECRYPTION_CLASSES = {};
|
||||
* @param {string} params.deviceId The identifier for this device.
|
||||
* @param {module:crypto} params.crypto crypto core
|
||||
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
||||
* @param {MatrixClient} baseApis base matrix api interface
|
||||
* @param {string} params.roomId The ID of the room we will be sending to
|
||||
* @param {object} params.config The body of the m.room.encryption event
|
||||
*/
|
||||
@@ -102,7 +102,7 @@ export class EncryptionAlgorithm {
|
||||
* @param {string} params.userId The UserID for the local user
|
||||
* @param {module:crypto} params.crypto crypto core
|
||||
* @param {module:crypto/OlmDevice} params.olmDevice olm.js wrapper
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis base matrix api interface
|
||||
* @param {MatrixClient} baseApis base matrix api interface
|
||||
* @param {string=} params.roomId The ID of the room we will be receiving
|
||||
* from. Null for to-device events.
|
||||
*/
|
||||
|
||||
+238
-96
@@ -22,9 +22,9 @@ limitations under the License.
|
||||
* @module crypto/algorithms/megolm
|
||||
*/
|
||||
|
||||
import {logger} from '../../logger';
|
||||
import { logger } from '../../logger';
|
||||
import * as utils from "../../utils";
|
||||
import {polyfillSuper} from "../../utils";
|
||||
import { polyfillSuper } from "../../utils";
|
||||
import * as olmlib from "../olmlib";
|
||||
import {
|
||||
DecryptionAlgorithm,
|
||||
@@ -34,13 +34,29 @@ import {
|
||||
UnknownDeviceError,
|
||||
} from "./base";
|
||||
|
||||
import {WITHHELD_MESSAGES} from '../OlmDevice';
|
||||
import { WITHHELD_MESSAGES } from '../OlmDevice';
|
||||
|
||||
// determine whether the key can be shared with invitees
|
||||
export function isRoomSharedHistory(room) {
|
||||
const visibilityEvent = room.currentState &&
|
||||
room.currentState.getStateEvents("m.room.history_visibility", "");
|
||||
// NOTE: if the room visibility is unset, it would normally default to
|
||||
// "world_readable".
|
||||
// (https://spec.matrix.org/unstable/client-server-api/#server-behaviour-5)
|
||||
// But we will be paranoid here, and treat it as a situation where the room
|
||||
// is not shared-history
|
||||
const visibility = visibilityEvent && visibilityEvent.getContent() &&
|
||||
visibilityEvent.getContent().history_visibility;
|
||||
return ["world_readable", "shared"].includes(visibility);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @constructor
|
||||
*
|
||||
* @param {string} sessionId
|
||||
* @param {boolean} sharedHistory whether the session can be freely shared with
|
||||
* other group members, according to the room history visibility settings
|
||||
*
|
||||
* @property {string} sessionId
|
||||
* @property {Number} useCount number of times this session has been used
|
||||
@@ -50,15 +66,15 @@ import {WITHHELD_MESSAGES} from '../OlmDevice';
|
||||
* devices with which we have shared the session key
|
||||
* userId -> {deviceId -> msgindex}
|
||||
*/
|
||||
function OutboundSessionInfo(sessionId) {
|
||||
function OutboundSessionInfo(sessionId, sharedHistory = false) {
|
||||
this.sessionId = sessionId;
|
||||
this.useCount = 0;
|
||||
this.creationTime = new Date().getTime();
|
||||
this.sharedWithDevices = {};
|
||||
this.blockedDevicesNotified = {};
|
||||
this.sharedHistory = sharedHistory;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check if it's time to rotate the session
|
||||
*
|
||||
@@ -141,7 +157,6 @@ OutboundSessionInfo.prototype.sharedWithTooManyDevices = function(
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Megolm encryption implementation
|
||||
*
|
||||
@@ -183,6 +198,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {module:models/room} room
|
||||
* @param {Object} devicesInRoom The devices in this room, indexed by user ID
|
||||
* @param {Object} blocked The devices that are blocked, indexed by user ID
|
||||
* @param {boolean} [singleOlmCreationPhase] Only perform one round of olm
|
||||
@@ -192,7 +208,7 @@ utils.inherits(MegolmEncryption, EncryptionAlgorithm);
|
||||
* OutboundSessionInfo when setup is complete.
|
||||
*/
|
||||
MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
devicesInRoom, blocked, singleOlmCreationPhase,
|
||||
room, devicesInRoom, blocked, singleOlmCreationPhase,
|
||||
) {
|
||||
let session;
|
||||
|
||||
@@ -204,6 +220,13 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
const prepareSession = async (oldSession) => {
|
||||
session = oldSession;
|
||||
|
||||
const sharedHistory = isRoomSharedHistory(room);
|
||||
|
||||
// history visibility changed
|
||||
if (session && sharedHistory !== session.sharedHistory) {
|
||||
session = null;
|
||||
}
|
||||
|
||||
// need to make a brand new session?
|
||||
if (session && session.needsRotation(this._sessionRotationPeriodMsgs,
|
||||
this._sessionRotationPeriodMs)
|
||||
@@ -219,7 +242,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
|
||||
if (!session) {
|
||||
logger.log(`Starting new megolm session for room ${this._roomId}`);
|
||||
session = await this._prepareNewSession();
|
||||
session = await this._prepareNewSession(sharedHistory);
|
||||
logger.log(`Started new megolm session ${session.sessionId} ` +
|
||||
`for room ${this._roomId}`);
|
||||
this._outboundSessions[session.sessionId] = session;
|
||||
@@ -250,11 +273,12 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
const payload = {
|
||||
type: "m.room_key",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: this._roomId,
|
||||
session_id: session.sessionId,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||
"room_id": this._roomId,
|
||||
"session_id": session.sessionId,
|
||||
"session_key": key.key,
|
||||
"chain_index": key.chain_index,
|
||||
"org.matrix.msc3061.shared_history": sharedHistory,
|
||||
},
|
||||
};
|
||||
const [devicesWithoutSession, olmSessions] = await olmlib.getExistingOlmSessions(
|
||||
@@ -264,11 +288,14 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
// share keys with devices that we already have a session for
|
||||
logger.debug(`Sharing keys with existing Olm sessions in ${this._roomId}`);
|
||||
await this._shareKeyWithOlmSessions(
|
||||
session, key, payload, olmSessions,
|
||||
);
|
||||
logger.debug(`Shared keys with existing Olm sessions in ${this._roomId}`);
|
||||
})(),
|
||||
(async () => {
|
||||
logger.debug(`Sharing keys (start phase 1) with new Olm sessions in ${this._roomId}`);
|
||||
const errorDevices = [];
|
||||
|
||||
// meanwhile, establish olm sessions for devices that we don't
|
||||
@@ -282,6 +309,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
session, key, payload, devicesWithoutSession, errorDevices,
|
||||
singleOlmCreationPhase ? 10000 : 2000, failedServers,
|
||||
);
|
||||
logger.debug(`Shared keys (end phase 1) with new Olm sessions in ${this._roomId}`);
|
||||
|
||||
if (!singleOlmCreationPhase && (Date.now() - start < 10000)) {
|
||||
// perform the second phase of olm session creation if requested,
|
||||
@@ -298,7 +326,7 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
failedServerMap.add(server);
|
||||
}
|
||||
const failedDevices = [];
|
||||
for (const {userId, deviceInfo} of errorDevices) {
|
||||
for (const { userId, deviceInfo } of errorDevices) {
|
||||
const userHS = userId.slice(userId.indexOf(":") + 1);
|
||||
if (failedServerMap.has(userHS)) {
|
||||
retryDevices[userId] = retryDevices[userId] || [];
|
||||
@@ -306,23 +334,28 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
} else {
|
||||
// if we aren't going to retry, then handle it
|
||||
// as a failed device
|
||||
failedDevices.push({userId, deviceInfo});
|
||||
failedDevices.push({ userId, deviceInfo });
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`Sharing keys (start phase 2) with new Olm sessions in ${this._roomId}`);
|
||||
await this._shareKeyWithDevices(
|
||||
session, key, payload, retryDevices, failedDevices, 30000,
|
||||
);
|
||||
logger.debug(`Shared keys (end phase 2) with new Olm sessions in ${this._roomId}`);
|
||||
|
||||
await this._notifyFailedOlmDevices(session, key, failedDevices);
|
||||
})();
|
||||
} else {
|
||||
await this._notifyFailedOlmDevices(session, key, errorDevices);
|
||||
}
|
||||
logger.debug(`Shared keys (all phases done) with new Olm sessions in ${this._roomId}`);
|
||||
})(),
|
||||
(async () => {
|
||||
logger.debug(`Notifying blocked devices in ${this._roomId}`);
|
||||
// also, notify blocked devices that they're blocked
|
||||
const blockedMap = {};
|
||||
let blockedCount = 0;
|
||||
for (const [userId, userBlockedDevices] of Object.entries(blocked)) {
|
||||
for (const [deviceId, device] of Object.entries(userBlockedDevices)) {
|
||||
if (
|
||||
@@ -330,12 +363,14 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
session.blockedDevicesNotified[userId][deviceId] === undefined
|
||||
) {
|
||||
blockedMap[userId] = blockedMap[userId] || {};
|
||||
blockedMap[userId][deviceId] = {device};
|
||||
blockedMap[userId][deviceId] = { device };
|
||||
blockedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this._notifyBlockedDevices(session, blockedMap);
|
||||
logger.debug(`Notified ${blockedCount} blocked devices in ${this._roomId}`);
|
||||
})(),
|
||||
]);
|
||||
};
|
||||
@@ -348,6 +383,11 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
// first wait for the previous share to complete
|
||||
const prom = this._setupPromise.then(prepareSession);
|
||||
|
||||
// Ensure any failures are logged for debugging
|
||||
prom.catch(e => {
|
||||
logger.error(`Failed to ensure outbound session in ${this._roomId}`, e);
|
||||
});
|
||||
|
||||
// _setupPromise resolves to `session` whether or not the share succeeds
|
||||
this._setupPromise = prom.then(returnSession, returnSession);
|
||||
|
||||
@@ -358,30 +398,26 @@ MegolmEncryption.prototype._ensureOutboundSession = async function(
|
||||
/**
|
||||
* @private
|
||||
*
|
||||
* @param {boolean} sharedHistory
|
||||
*
|
||||
* @return {module:crypto/algorithms/megolm.OutboundSessionInfo} session
|
||||
*/
|
||||
MegolmEncryption.prototype._prepareNewSession = async function() {
|
||||
MegolmEncryption.prototype._prepareNewSession = async function(sharedHistory) {
|
||||
const sessionId = this._olmDevice.createOutboundGroupSession();
|
||||
const key = this._olmDevice.getOutboundGroupSessionKey(sessionId);
|
||||
|
||||
await this._olmDevice.addInboundGroupSession(
|
||||
this._roomId, this._olmDevice.deviceCurve25519Key, [], sessionId,
|
||||
key.key, {ed25519: this._olmDevice.deviceEd25519Key},
|
||||
key.key, { ed25519: this._olmDevice.deviceEd25519Key }, false,
|
||||
{ sharedHistory: sharedHistory },
|
||||
);
|
||||
|
||||
if (this._crypto.backupInfo) {
|
||||
// don't wait for it to complete
|
||||
this._crypto.backupGroupSession(
|
||||
this._roomId, this._olmDevice.deviceCurve25519Key, [],
|
||||
sessionId, key.key,
|
||||
).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 megolm session", e);
|
||||
});
|
||||
}
|
||||
// don't wait for it to complete
|
||||
this._crypto.backupManager.backupGroupSession(
|
||||
this._olmDevice.deviceCurve25519Key, sessionId,
|
||||
);
|
||||
|
||||
return new OutboundSessionInfo(sessionId);
|
||||
return new OutboundSessionInfo(sessionId, sharedHistory);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -415,7 +451,7 @@ MegolmEncryption.prototype._getDevicesWithoutSessions = function(
|
||||
// no session with this device, probably because there
|
||||
// were no one-time keys.
|
||||
|
||||
noOlmDevices.push({userId, deviceInfo});
|
||||
noOlmDevices.push({ userId, deviceInfo });
|
||||
delete sessionResults[deviceId];
|
||||
|
||||
// ensureOlmSessionsForUsers has already done the logging,
|
||||
@@ -662,14 +698,15 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
|
||||
const payload = {
|
||||
type: "m.forwarded_room_key",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: this._roomId,
|
||||
session_id: sessionId,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
sender_key: senderKey,
|
||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
|
||||
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||
"room_id": this._roomId,
|
||||
"session_id": sessionId,
|
||||
"session_key": key.key,
|
||||
"chain_index": key.chain_index,
|
||||
"sender_key": senderKey,
|
||||
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
|
||||
"forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain,
|
||||
"org.matrix.msc3061.shared_history": key.shared_history || false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -723,13 +760,18 @@ MegolmEncryption.prototype.reshareKeyWithDevice = async function(
|
||||
MegolmEncryption.prototype._shareKeyWithDevices = async function(
|
||||
session, key, payload, devicesByUser, errorDevices, otkTimeout, failedServers,
|
||||
) {
|
||||
logger.debug(`Ensuring Olm sessions for devices in ${this._roomId}`);
|
||||
const devicemap = await olmlib.ensureOlmSessionsForDevices(
|
||||
this._olmDevice, this._baseApis, devicesByUser, otkTimeout, failedServers,
|
||||
logger.withPrefix(`[${this._roomId}]`),
|
||||
);
|
||||
logger.debug(`Ensured Olm sessions for devices in ${this._roomId}`);
|
||||
|
||||
this._getDevicesWithoutSessions(devicemap, devicesByUser, errorDevices);
|
||||
|
||||
logger.debug(`Sharing keys with Olm sessions in ${this._roomId}`);
|
||||
await this._shareKeyWithOlmSessions(session, key, payload, devicemap);
|
||||
logger.debug(`Shared keys with Olm sessions in ${this._roomId}`);
|
||||
};
|
||||
|
||||
MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
|
||||
@@ -738,16 +780,17 @@ MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
|
||||
const userDeviceMaps = this._splitDevices(devicemap);
|
||||
|
||||
for (let i = 0; i < userDeviceMaps.length; i++) {
|
||||
const taskDetail =
|
||||
`megolm keys for ${session.sessionId} ` +
|
||||
`in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`;
|
||||
try {
|
||||
logger.debug(`Sharing ${taskDetail}`);
|
||||
await this._encryptAndSendKeysToDevices(
|
||||
session, key.chain_index, userDeviceMaps[i], payload,
|
||||
);
|
||||
logger.log(`Completed megolm keyshare for ${session.sessionId} `
|
||||
+ `in ${this._roomId} (slice ${i + 1}/${userDeviceMaps.length})`);
|
||||
logger.debug(`Shared ${taskDetail}`);
|
||||
} catch (e) {
|
||||
logger.log(`megolm keyshare for ${session.sessionId} in ${this._roomId} `
|
||||
+ `(slice ${i + 1}/${userDeviceMaps.length}) failed`);
|
||||
|
||||
logger.error(`Failed to share ${taskDetail}`);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
@@ -766,9 +809,14 @@ MegolmEncryption.prototype._shareKeyWithOlmSessions = async function(
|
||||
MegolmEncryption.prototype._notifyFailedOlmDevices = async function(
|
||||
session, key, failedDevices,
|
||||
) {
|
||||
logger.debug(
|
||||
`Notifying ${failedDevices.length} devices we failed to ` +
|
||||
`create Olm sessions in ${this._roomId}`,
|
||||
);
|
||||
|
||||
// mark the devices that failed as "handled" because we don't want to try
|
||||
// to claim a one-time-key for dead devices on every message.
|
||||
for (const {userId, deviceInfo} of failedDevices) {
|
||||
for (const { userId, deviceInfo } of failedDevices) {
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
|
||||
session.markSharedWithDevice(
|
||||
@@ -780,8 +828,12 @@ MegolmEncryption.prototype._notifyFailedOlmDevices = async function(
|
||||
await this._olmDevice.filterOutNotifiedErrorDevices(
|
||||
failedDevices,
|
||||
);
|
||||
logger.debug(
|
||||
`Filtered down to ${filteredFailedDevices.length} error devices ` +
|
||||
`in ${this._roomId}`,
|
||||
);
|
||||
const blockedMap = {};
|
||||
for (const {userId, deviceInfo} of filteredFailedDevices) {
|
||||
for (const { userId, deviceInfo } of filteredFailedDevices) {
|
||||
blockedMap[userId] = blockedMap[userId] || {};
|
||||
// we use a similar format to what
|
||||
// olmlib.ensureOlmSessionsForDevices returns, so that
|
||||
@@ -797,6 +849,10 @@ MegolmEncryption.prototype._notifyFailedOlmDevices = async function(
|
||||
|
||||
// send the notifications
|
||||
await this._notifyBlockedDevices(session, blockedMap);
|
||||
logger.debug(
|
||||
`Notified ${filteredFailedDevices.length} devices we failed to ` +
|
||||
`create Olm sessions in ${this._roomId}`,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -846,24 +902,41 @@ MegolmEncryption.prototype.prepareToEncrypt = function(room) {
|
||||
// We're already preparing something, so don't do anything else.
|
||||
// FIXME: check if we need to restart
|
||||
// (https://github.com/matrix-org/matrix-js-sdk/issues/1255)
|
||||
const elapsedTime = Date.now() - this.encryptionPreparationMetadata.startTime;
|
||||
logger.debug(
|
||||
`Already started preparing to encrypt for ${this._roomId} ` +
|
||||
`${elapsedTime} ms ago, skipping`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug(`Preparing to encrypt events for ${this._roomId}`);
|
||||
|
||||
this.encryptionPreparationMetadata = {
|
||||
startTime: Date.now(),
|
||||
};
|
||||
this.encryptionPreparation = (async () => {
|
||||
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
|
||||
try {
|
||||
logger.debug(`Getting devices in ${this._roomId}`);
|
||||
const [devicesInRoom, blocked] = await this._getDevicesInRoom(room);
|
||||
|
||||
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
|
||||
// Drop unknown devices for now. When the message gets sent, we'll
|
||||
// throw an error, but we'll still be prepared to send to the known
|
||||
// devices.
|
||||
this._removeUnknownDevices(devicesInRoom);
|
||||
if (this._crypto.getGlobalErrorOnUnknownDevices()) {
|
||||
// Drop unknown devices for now. When the message gets sent, we'll
|
||||
// throw an error, but we'll still be prepared to send to the known
|
||||
// devices.
|
||||
this._removeUnknownDevices(devicesInRoom);
|
||||
}
|
||||
|
||||
logger.debug(`Ensuring outbound session in ${this._roomId}`);
|
||||
await this._ensureOutboundSession(room, devicesInRoom, blocked, true);
|
||||
|
||||
logger.debug(`Ready to encrypt events for ${this._roomId}`);
|
||||
} catch (e) {
|
||||
logger.error(`Failed to prepare to encrypt events for ${this._roomId}`, e);
|
||||
} finally {
|
||||
delete this.encryptionPreparationMetadata;
|
||||
delete this.encryptionPreparation;
|
||||
}
|
||||
|
||||
await this._ensureOutboundSession(devicesInRoom, blocked, true);
|
||||
|
||||
delete this.encryptionPreparation;
|
||||
})();
|
||||
};
|
||||
|
||||
@@ -899,7 +972,7 @@ MegolmEncryption.prototype.encryptMessage = async function(room, eventType, cont
|
||||
this._checkForUnknownDevices(devicesInRoom);
|
||||
}
|
||||
|
||||
const session = await this._ensureOutboundSession(devicesInRoom, blocked);
|
||||
const session = await this._ensureOutboundSession(room, devicesInRoom, blocked);
|
||||
const payloadJson = {
|
||||
room_id: this._roomId,
|
||||
type: eventType,
|
||||
@@ -1000,7 +1073,7 @@ MegolmEncryption.prototype._removeUnknownDevices = function(devicesInRoom) {
|
||||
*/
|
||||
MegolmEncryption.prototype._getDevicesInRoom = async function(room) {
|
||||
const members = await room.getEncryptionTargetMembers();
|
||||
const roomMembers = utils.map(members, function(u) {
|
||||
const roomMembers = members.map(function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
|
||||
@@ -1265,7 +1338,6 @@ MegolmDecryption.prototype._removeEventFromPendingList = function(event) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
@@ -1295,7 +1367,7 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
if (event.getType() == "m.forwarded_room_key") {
|
||||
exportFormat = true;
|
||||
forwardingKeyChain = content.forwarding_curve25519_key_chain;
|
||||
if (!utils.isArray(forwardingKeyChain)) {
|
||||
if (!Array.isArray(forwardingKeyChain)) {
|
||||
forwardingKeyChain = [];
|
||||
}
|
||||
|
||||
@@ -1324,10 +1396,14 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
keysClaimed = event.getKeysClaimed();
|
||||
}
|
||||
|
||||
const extraSessionData = {};
|
||||
if (content["org.matrix.msc3061.shared_history"]) {
|
||||
extraSessionData.sharedHistory = true;
|
||||
}
|
||||
return this._olmDevice.addInboundGroupSession(
|
||||
content.room_id, senderKey, forwardingKeyChain, sessionId,
|
||||
content.session_key, keysClaimed,
|
||||
exportFormat,
|
||||
exportFormat, extraSessionData,
|
||||
).then(() => {
|
||||
// have another go at decrypting events sent with this session.
|
||||
this._retryDecryption(senderKey, sessionId)
|
||||
@@ -1347,18 +1423,8 @@ MegolmDecryption.prototype.onRoomKeyEvent = function(event) {
|
||||
}
|
||||
});
|
||||
}).then(() => {
|
||||
if (this._crypto.backupInfo) {
|
||||
// don't wait for the keys to be backed up for the server
|
||||
this._crypto.backupGroupSession(
|
||||
content.room_id, senderKey, forwardingKeyChain,
|
||||
content.session_id, content.session_key, keysClaimed,
|
||||
exportFormat,
|
||||
).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 megolm session", e);
|
||||
});
|
||||
}
|
||||
// don't wait for the keys to be backed up for the server
|
||||
this._crypto.backupManager.backupGroupSession(senderKey, content.session_id);
|
||||
}).catch((e) => {
|
||||
logger.error(`Error handling m.room_key_event: ${e}`);
|
||||
});
|
||||
@@ -1394,14 +1460,14 @@ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
|
||||
this.retryDecryptionFromSender(senderKey);
|
||||
return;
|
||||
}
|
||||
let device = this._crypto._deviceList.getDeviceByIdentityKey(
|
||||
let device = this._crypto.deviceList.getDeviceByIdentityKey(
|
||||
content.algorithm, senderKey,
|
||||
);
|
||||
if (!device) {
|
||||
// if we don't know about the device, fetch the user's devices again
|
||||
// and retry before giving up
|
||||
await this._crypto.downloadKeys([sender], false);
|
||||
device = this._crypto._deviceList.getDeviceByIdentityKey(
|
||||
device = this._crypto.deviceList.getDeviceByIdentityKey(
|
||||
content.algorithm, senderKey,
|
||||
);
|
||||
if (!device) {
|
||||
@@ -1415,7 +1481,7 @@ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
|
||||
}
|
||||
}
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this._olmDevice, this._baseApis, {[sender]: [device]}, false,
|
||||
this._olmDevice, this._baseApis, { [sender]: [device] }, false,
|
||||
);
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
@@ -1429,7 +1495,7 @@ MegolmDecryption.prototype.onRoomKeyWithheldEvent = async function(event) {
|
||||
this._olmDevice,
|
||||
sender,
|
||||
device,
|
||||
{type: "m.dummy"},
|
||||
{ type: "m.dummy" },
|
||||
);
|
||||
|
||||
await this._olmDevice.recordSessionProblem(senderKey, "no_olm", true);
|
||||
@@ -1533,14 +1599,15 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
|
||||
return {
|
||||
type: "m.forwarded_room_key",
|
||||
content: {
|
||||
algorithm: olmlib.MEGOLM_ALGORITHM,
|
||||
room_id: roomId,
|
||||
sender_key: senderKey,
|
||||
sender_claimed_ed25519_key: key.sender_claimed_ed25519_key,
|
||||
session_id: sessionId,
|
||||
session_key: key.key,
|
||||
chain_index: key.chain_index,
|
||||
forwarding_curve25519_key_chain: key.forwarding_curve25519_key_chain,
|
||||
"algorithm": olmlib.MEGOLM_ALGORITHM,
|
||||
"room_id": roomId,
|
||||
"sender_key": senderKey,
|
||||
"sender_claimed_ed25519_key": key.sender_claimed_ed25519_key,
|
||||
"session_id": sessionId,
|
||||
"session_key": key.key,
|
||||
"chain_index": key.chain_index,
|
||||
"forwarding_curve25519_key_chain": key.forwarding_curve25519_key_chain,
|
||||
"org.matrix.msc3061.shared_history": key.shared_history || false,
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -1554,6 +1621,13 @@ MegolmDecryption.prototype._buildKeyForwardingMessage = async function(
|
||||
* @param {string} [opts.source] where the key came from
|
||||
*/
|
||||
MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
|
||||
const extraSessionData = {};
|
||||
if (opts.untrusted) {
|
||||
extraSessionData.untrusted = true;
|
||||
}
|
||||
if (session["org.matrix.msc3061.shared_history"]) {
|
||||
extraSessionData.sharedHistory = true;
|
||||
}
|
||||
return this._olmDevice.addInboundGroupSession(
|
||||
session.room_id,
|
||||
session.sender_key,
|
||||
@@ -1562,18 +1636,12 @@ MegolmDecryption.prototype.importRoomKey = function(session, opts = {}) {
|
||||
session.session_key,
|
||||
session.sender_claimed_keys,
|
||||
true,
|
||||
opts.untrusted ? { untrusted: opts.untrusted } : {},
|
||||
extraSessionData,
|
||||
).then(() => {
|
||||
if (this._crypto.backupInfo && opts.source !== "backup") {
|
||||
if (opts.source !== "backup") {
|
||||
// don't wait for it to complete
|
||||
this._crypto.backupGroupSession(
|
||||
session.room_id,
|
||||
session.sender_key,
|
||||
session.forwarding_curve25519_key_chain,
|
||||
session.session_id,
|
||||
session.session_key,
|
||||
session.sender_claimed_keys,
|
||||
true,
|
||||
this._crypto.backupManager.backupGroupSession(
|
||||
session.sender_key, session.session_id,
|
||||
).catch((e) => {
|
||||
// This throws if the upload failed, but this is fine
|
||||
// since it will have written it to the db and will retry.
|
||||
@@ -1610,7 +1678,7 @@ MegolmDecryption.prototype._retryDecryption = async function(senderKey, sessionI
|
||||
|
||||
await Promise.all([...pending].map(async (ev) => {
|
||||
try {
|
||||
await ev.attemptDecryption(this._crypto, true);
|
||||
await ev.attemptDecryption(this._crypto, { isRetry: true });
|
||||
} catch (e) {
|
||||
// don't die if something goes wrong
|
||||
}
|
||||
@@ -1641,6 +1709,80 @@ MegolmDecryption.prototype.retryDecryptionFromSender = async function(senderKey)
|
||||
return !this._pendingEvents[senderKey];
|
||||
};
|
||||
|
||||
MegolmDecryption.prototype.sendSharedHistoryInboundSessions = async function(devicesByUser) {
|
||||
await olmlib.ensureOlmSessionsForDevices(
|
||||
this._olmDevice, this._baseApis, devicesByUser,
|
||||
);
|
||||
|
||||
logger.log("sendSharedHistoryInboundSessions to users", Object.keys(devicesByUser));
|
||||
|
||||
const sharedHistorySessions =
|
||||
await this._olmDevice.getSharedHistoryInboundGroupSessions(
|
||||
this._roomId,
|
||||
);
|
||||
logger.log("shared-history sessions", sharedHistorySessions);
|
||||
for (const [senderKey, sessionId] of sharedHistorySessions) {
|
||||
const payload = await this._buildKeyForwardingMessage(
|
||||
this._roomId, senderKey, sessionId,
|
||||
);
|
||||
|
||||
const promises = [];
|
||||
const contentMap = {};
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
contentMap[userId] = {};
|
||||
for (const deviceInfo of devices) {
|
||||
const encryptedContent = {
|
||||
algorithm: olmlib.OLM_ALGORITHM,
|
||||
sender_key: this._olmDevice.deviceCurve25519Key,
|
||||
ciphertext: {},
|
||||
};
|
||||
contentMap[userId][deviceInfo.deviceId] = encryptedContent;
|
||||
promises.push(
|
||||
olmlib.encryptMessageForDevice(
|
||||
encryptedContent.ciphertext,
|
||||
this._userId,
|
||||
this._deviceId,
|
||||
this._olmDevice,
|
||||
userId,
|
||||
deviceInfo,
|
||||
payload,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
// prune out any devices that encryptMessageForDevice could not encrypt for,
|
||||
// in which case it will have just not added anything to the ciphertext object.
|
||||
// There's no point sending messages to devices if we couldn't encrypt to them,
|
||||
// since that's effectively a blank message.
|
||||
for (const userId of Object.keys(contentMap)) {
|
||||
for (const deviceId of Object.keys(contentMap[userId])) {
|
||||
if (Object.keys(contentMap[userId][deviceId].ciphertext).length === 0) {
|
||||
logger.log(
|
||||
"No ciphertext for device " +
|
||||
userId + ":" + deviceId + ": pruning",
|
||||
);
|
||||
delete contentMap[userId][deviceId];
|
||||
}
|
||||
}
|
||||
// No devices left for that user? Strip that too.
|
||||
if (Object.keys(contentMap[userId]).length === 0) {
|
||||
logger.log("Pruned all devices for user " + userId);
|
||||
delete contentMap[userId];
|
||||
}
|
||||
}
|
||||
|
||||
// Is there anything left?
|
||||
if (Object.keys(contentMap).length === 0) {
|
||||
logger.log("No users left to send to: aborting");
|
||||
return;
|
||||
}
|
||||
|
||||
await this._baseApis.sendToDevice("m.room.encrypted", contentMap);
|
||||
}
|
||||
};
|
||||
|
||||
registerAlgorithm(
|
||||
olmlib.MEGOLM_ALGORITHM, MegolmEncryption, MegolmDecryption,
|
||||
);
|
||||
|
||||
@@ -20,11 +20,11 @@ limitations under the License.
|
||||
* @module crypto/algorithms/olm
|
||||
*/
|
||||
|
||||
import {logger} from '../../logger';
|
||||
import { logger } from '../../logger';
|
||||
import * as utils from "../../utils";
|
||||
import {polyfillSuper} from "../../utils";
|
||||
import { polyfillSuper } from "../../utils";
|
||||
import * as olmlib from "../olmlib";
|
||||
import {DeviceInfo} from "../deviceinfo";
|
||||
import { DeviceInfo } from "../deviceinfo";
|
||||
import {
|
||||
DecryptionAlgorithm,
|
||||
DecryptionError,
|
||||
@@ -95,7 +95,7 @@ OlmEncryption.prototype.encryptMessage = async function(room, eventType, content
|
||||
|
||||
const members = await room.getEncryptionTargetMembers();
|
||||
|
||||
const users = utils.map(members, function(u) {
|
||||
const users = members.map(function(u) {
|
||||
return u.userId;
|
||||
});
|
||||
|
||||
@@ -358,5 +358,4 @@ OlmDecryption.prototype._reallyDecryptMessage = async function(
|
||||
return res.payload;
|
||||
};
|
||||
|
||||
|
||||
registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/*
|
||||
Copyright 2021 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 { DeviceInfo } from "./deviceinfo";
|
||||
import { IKeyBackupVersion } from "./keybackup";
|
||||
import { ISecretStorageKeyInfo } from "../matrix";
|
||||
|
||||
// TODO: Merge this with crypto.js once converted
|
||||
|
||||
export enum CrossSigningKey {
|
||||
Master = "master",
|
||||
SelfSigning = "self_signing",
|
||||
UserSigning = "user_signing",
|
||||
}
|
||||
|
||||
export interface IEncryptedEventInfo {
|
||||
/**
|
||||
* whether the event is encrypted (if not encrypted, some of the other properties may not be set)
|
||||
*/
|
||||
encrypted: boolean;
|
||||
|
||||
/**
|
||||
* the sender's key
|
||||
*/
|
||||
senderKey: string;
|
||||
|
||||
/**
|
||||
* the algorithm used to encrypt the event
|
||||
*/
|
||||
algorithm: string;
|
||||
|
||||
/**
|
||||
* whether we can be sure that the owner of the senderKey sent the event
|
||||
*/
|
||||
authenticated: boolean;
|
||||
|
||||
/**
|
||||
* the sender's device information, if available
|
||||
*/
|
||||
sender?: DeviceInfo;
|
||||
|
||||
/**
|
||||
* if the event's ed25519 and curve25519 keys don't match (only meaningful if `sender` is set)
|
||||
*/
|
||||
mismatchedSender: boolean;
|
||||
}
|
||||
|
||||
export interface IRecoveryKey {
|
||||
keyInfo: {
|
||||
pubkey: string;
|
||||
passphrase?: {
|
||||
algorithm: string;
|
||||
iterations: number;
|
||||
salt: string;
|
||||
};
|
||||
};
|
||||
privateKey: Uint8Array;
|
||||
encodedPrivateKey: string;
|
||||
}
|
||||
|
||||
export interface ICreateSecretStorageOpts {
|
||||
/**
|
||||
* Function called to await a secret storage key creation flow.
|
||||
* Returns:
|
||||
* {Promise<Object>} Object with public key metadata, encoded private
|
||||
* recovery key which should be disposed of after displaying to the user,
|
||||
* and raw private key to avoid round tripping if needed.
|
||||
*/
|
||||
createSecretStorageKey?: () => Promise<IRecoveryKey>;
|
||||
|
||||
/**
|
||||
* The current key backup object. If passed,
|
||||
* the passphrase and recovery key from this backup will be used.
|
||||
*/
|
||||
keyBackupInfo?: IKeyBackupVersion;
|
||||
|
||||
/**
|
||||
* If true, a new key backup version will be
|
||||
* created and the private key stored in the new SSSS store. Ignored if keyBackupInfo
|
||||
* is supplied.
|
||||
*/
|
||||
setupNewKeyBackup?: boolean;
|
||||
|
||||
/**
|
||||
* Reset even if keys already exist.
|
||||
*/
|
||||
setupNewSecretStorage?: boolean;
|
||||
|
||||
/**
|
||||
* Function called to get the user's
|
||||
* current key backup passphrase. Should return a promise that resolves with a Uint8Array
|
||||
* containing the key, or rejects if the key cannot be obtained.
|
||||
*/
|
||||
getKeyBackupPassphrase?: () => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
export interface ISecretStorageKey {
|
||||
keyId: string;
|
||||
keyInfo: ISecretStorageKeyInfo;
|
||||
}
|
||||
|
||||
export interface IAddSecretStorageKeyOpts {
|
||||
// depends on algorithm
|
||||
// TODO: Types
|
||||
}
|
||||
|
||||
export interface IImportOpts {
|
||||
stage: string; // TODO: Enum
|
||||
successes: number;
|
||||
failures: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface IImportRoomKeysOpts {
|
||||
progressCallback: (stage: IImportOpts) => void;
|
||||
untrusted?: boolean;
|
||||
source?: string; // TODO: Enum
|
||||
}
|
||||
@@ -0,0 +1,656 @@
|
||||
/*
|
||||
Copyright 2021 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module crypto/backup
|
||||
*
|
||||
* Classes for dealing with key backup.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "../client";
|
||||
import { logger } from "../logger";
|
||||
import { MEGOLM_ALGORITHM, verifySignature } from "./olmlib";
|
||||
import { DeviceInfo } from "./deviceinfo";
|
||||
import { DeviceTrustLevel } from './CrossSigning';
|
||||
import { keyFromPassphrase } from './key_passphrase';
|
||||
import { sleep } from "../utils";
|
||||
import { IndexedDBCryptoStore } from './store/indexeddb-crypto-store';
|
||||
import { encodeRecoveryKey } from './recoverykey';
|
||||
|
||||
const KEY_BACKUP_KEYS_PER_REQUEST = 200;
|
||||
|
||||
type AuthData = Record<string, any>;
|
||||
|
||||
type BackupInfo = {
|
||||
algorithm: string,
|
||||
auth_data: AuthData, // eslint-disable-line camelcase
|
||||
[properties: string]: any,
|
||||
};
|
||||
|
||||
type SigInfo = {
|
||||
deviceId: string,
|
||||
valid?: boolean | null, // true: valid, false: invalid, null: cannot attempt validation
|
||||
device?: DeviceInfo | null,
|
||||
crossSigningId?: boolean,
|
||||
deviceTrust?: DeviceTrustLevel,
|
||||
};
|
||||
|
||||
export type TrustInfo = {
|
||||
usable: boolean, // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
||||
sigs: SigInfo[],
|
||||
};
|
||||
|
||||
export interface IKeyBackupCheck {
|
||||
backupInfo: BackupInfo;
|
||||
trustInfo: TrustInfo;
|
||||
}
|
||||
|
||||
/** A function used to get the secret key for a backup.
|
||||
*/
|
||||
type GetKey = () => Promise<Uint8Array>;
|
||||
|
||||
interface BackupAlgorithmClass {
|
||||
algorithmName: string;
|
||||
// initialize from an existing backup
|
||||
init(authData: AuthData, getKey: GetKey): Promise<BackupAlgorithm>;
|
||||
|
||||
// prepare a brand new backup
|
||||
prepare(
|
||||
key: string | Uint8Array | null,
|
||||
): Promise<[Uint8Array, AuthData]>;
|
||||
}
|
||||
|
||||
interface BackupAlgorithm {
|
||||
encryptSession(data: Record<string, any>): Promise<any>;
|
||||
decryptSessions(ciphertexts: Record<string, any>): Promise<Record<string, any>[]>;
|
||||
authData: AuthData;
|
||||
keyMatches(key: Uint8Array): Promise<boolean>;
|
||||
free(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the key backup.
|
||||
*/
|
||||
export class BackupManager {
|
||||
private algorithm: BackupAlgorithm | undefined;
|
||||
public backupInfo: BackupInfo | undefined; // The info dict from /room_keys/version
|
||||
public checkedForBackup: boolean; // Have we checked the server for a backup we can use?
|
||||
private sendingBackups: boolean; // Are we currently sending backups?
|
||||
constructor(private readonly baseApis: MatrixClient, public readonly getKey: GetKey) {
|
||||
this.checkedForBackup = false;
|
||||
this.sendingBackups = false;
|
||||
}
|
||||
|
||||
public get version(): string | undefined {
|
||||
return this.backupInfo && this.backupInfo.version;
|
||||
}
|
||||
|
||||
public static async makeAlgorithm(info: BackupInfo, getKey: GetKey): Promise<BackupAlgorithm> {
|
||||
const Algorithm = algorithmsByName[info.algorithm];
|
||||
if (!Algorithm) {
|
||||
throw new Error("Unknown backup algorithm");
|
||||
}
|
||||
return await Algorithm.init(info.auth_data, getKey);
|
||||
}
|
||||
|
||||
public async enableKeyBackup(info: BackupInfo): Promise<void> {
|
||||
this.backupInfo = info;
|
||||
if (this.algorithm) {
|
||||
this.algorithm.free();
|
||||
}
|
||||
|
||||
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
|
||||
|
||||
this.baseApis.emit('crypto.keyBackupStatus', true);
|
||||
|
||||
// There may be keys left over from a partially completed backup, so
|
||||
// schedule a send to check.
|
||||
this.scheduleKeyBackupSend();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable backing up of keys.
|
||||
*/
|
||||
public disableKeyBackup(): void {
|
||||
if (this.algorithm) {
|
||||
this.algorithm.free();
|
||||
}
|
||||
this.algorithm = undefined;
|
||||
|
||||
this.backupInfo = undefined;
|
||||
|
||||
this.baseApis.emit('crypto.keyBackupStatus', false);
|
||||
}
|
||||
|
||||
public getKeyBackupEnabled(): boolean | null {
|
||||
if (!this.checkedForBackup) {
|
||||
return null;
|
||||
}
|
||||
return Boolean(this.algorithm);
|
||||
}
|
||||
|
||||
public async prepareKeyBackupVersion(
|
||||
key?: string | Uint8Array | null,
|
||||
algorithm?: string | undefined,
|
||||
): Promise<BackupInfo> {
|
||||
const Algorithm = algorithm ? algorithmsByName[algorithm] : DefaultAlgorithm;
|
||||
if (!Algorithm) {
|
||||
throw new Error("Unknown backup algorithm");
|
||||
}
|
||||
|
||||
const [privateKey, authData] = await Algorithm.prepare(key);
|
||||
const recoveryKey = encodeRecoveryKey(privateKey);
|
||||
return {
|
||||
algorithm: Algorithm.algorithmName,
|
||||
auth_data: authData,
|
||||
recovery_key: recoveryKey,
|
||||
privateKey,
|
||||
};
|
||||
}
|
||||
|
||||
public async createKeyBackupVersion(info: BackupInfo): Promise<void> {
|
||||
this.algorithm = await BackupManager.makeAlgorithm(info, this.getKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the server for an active key backup and
|
||||
* if one is present and has a valid signature from
|
||||
* one of the user's verified devices, start backing up
|
||||
* to it.
|
||||
*/
|
||||
public async checkAndStart(): Promise<{backupInfo: BackupInfo, trustInfo: TrustInfo}> {
|
||||
logger.log("Checking key backup status...");
|
||||
if (this.baseApis.isGuest()) {
|
||||
logger.log("Skipping key backup check since user is guest");
|
||||
this.checkedForBackup = true;
|
||||
return null;
|
||||
}
|
||||
let backupInfo: BackupInfo;
|
||||
try {
|
||||
backupInfo = await this.baseApis.getKeyBackupVersion();
|
||||
} catch (e) {
|
||||
logger.log("Error checking for active key backup", e);
|
||||
if (e.httpStatus === 404) {
|
||||
// 404 is returned when the key backup does not exist, so that
|
||||
// counts as successfully checking.
|
||||
this.checkedForBackup = true;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
this.checkedForBackup = true;
|
||||
|
||||
const trustInfo = await this.isKeyBackupTrusted(backupInfo);
|
||||
|
||||
if (trustInfo.usable && !this.backupInfo) {
|
||||
logger.log(
|
||||
"Found usable key backup v" + backupInfo.version +
|
||||
": enabling key backups",
|
||||
);
|
||||
await this.enableKeyBackup(backupInfo);
|
||||
} else if (!trustInfo.usable && this.backupInfo) {
|
||||
logger.log("No usable key backup: disabling key backup");
|
||||
this.disableKeyBackup();
|
||||
} else if (!trustInfo.usable && !this.backupInfo) {
|
||||
logger.log("No usable key backup: not enabling key backup");
|
||||
} else if (trustInfo.usable && this.backupInfo) {
|
||||
// may not be the same version: if not, we should switch
|
||||
if (backupInfo.version !== this.backupInfo.version) {
|
||||
logger.log(
|
||||
"On backup version " + this.backupInfo.version + " but found " +
|
||||
"version " + backupInfo.version + ": switching.",
|
||||
);
|
||||
this.disableKeyBackup();
|
||||
await this.enableKeyBackup(backupInfo);
|
||||
// We're now using a new backup, so schedule all the keys we have to be
|
||||
// uploaded to the new backup. This is a bit of a workaround to upload
|
||||
// keys to a new backup in *most* cases, but it won't cover all cases
|
||||
// because we don't remember what backup version we uploaded keys to:
|
||||
// see https://github.com/vector-im/element-web/issues/14833
|
||||
await this.scheduleAllGroupSessionsForBackup();
|
||||
} else {
|
||||
logger.log("Backup version " + backupInfo.version + " still current");
|
||||
}
|
||||
}
|
||||
|
||||
return { backupInfo, trustInfo };
|
||||
}
|
||||
|
||||
/**
|
||||
* Forces a re-check of the key backup and enables/disables it
|
||||
* as appropriate.
|
||||
*
|
||||
* @return {Object} Object with backup info (as returned by
|
||||
* getKeyBackupVersion) in backupInfo and
|
||||
* trust information (as returned by isKeyBackupTrusted)
|
||||
* in trustInfo.
|
||||
*/
|
||||
public async checkKeyBackup(): Promise<IKeyBackupCheck> {
|
||||
this.checkedForBackup = false;
|
||||
return this.checkAndStart();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given backup info is trusted.
|
||||
*
|
||||
* @param {object} backupInfo key backup info dict from /room_keys/version
|
||||
* @return {object} {
|
||||
* usable: [bool], // is the backup trusted, true iff there is a sig that is valid & from a trusted device
|
||||
* sigs: [
|
||||
* valid: [bool || null], // true: valid, false: invalid, null: cannot attempt validation
|
||||
* deviceId: [string],
|
||||
* device: [DeviceInfo || null],
|
||||
* ]
|
||||
* }
|
||||
*/
|
||||
public async isKeyBackupTrusted(backupInfo: BackupInfo): Promise<TrustInfo> {
|
||||
const ret = {
|
||||
usable: false,
|
||||
trusted_locally: false,
|
||||
sigs: [],
|
||||
};
|
||||
|
||||
if (
|
||||
!backupInfo ||
|
||||
!backupInfo.algorithm ||
|
||||
!backupInfo.auth_data ||
|
||||
!backupInfo.auth_data.public_key ||
|
||||
!backupInfo.auth_data.signatures
|
||||
) {
|
||||
logger.info("Key backup is absent or missing required data");
|
||||
return ret;
|
||||
}
|
||||
|
||||
const trustedPubkey = this.baseApis.crypto.sessionStore.getLocalTrustedBackupPubKey();
|
||||
|
||||
if (backupInfo.auth_data.public_key === trustedPubkey) {
|
||||
logger.info("Backup public key " + trustedPubkey + " is trusted locally");
|
||||
ret.trusted_locally = true;
|
||||
}
|
||||
|
||||
const mySigs = backupInfo.auth_data.signatures[this.baseApis.getUserId()] || [];
|
||||
|
||||
for (const keyId of Object.keys(mySigs)) {
|
||||
const keyIdParts = keyId.split(':');
|
||||
if (keyIdParts[0] !== 'ed25519') {
|
||||
logger.log("Ignoring unknown signature type: " + keyIdParts[0]);
|
||||
continue;
|
||||
}
|
||||
// Could be a cross-signing master key, but just say this is the device
|
||||
// ID for backwards compat
|
||||
const sigInfo: SigInfo = { deviceId: keyIdParts[1] };
|
||||
|
||||
// first check to see if it's from our cross-signing key
|
||||
const crossSigningId = this.baseApis.crypto.crossSigningInfo.getId();
|
||||
if (crossSigningId === sigInfo.deviceId) {
|
||||
sigInfo.crossSigningId = true;
|
||||
try {
|
||||
await verifySignature(
|
||||
this.baseApis.crypto.olmDevice,
|
||||
backupInfo.auth_data,
|
||||
this.baseApis.getUserId(),
|
||||
sigInfo.deviceId,
|
||||
crossSigningId,
|
||||
);
|
||||
sigInfo.valid = true;
|
||||
} catch (e) {
|
||||
logger.warn(
|
||||
"Bad signature from cross signing key " + crossSigningId, e,
|
||||
);
|
||||
sigInfo.valid = false;
|
||||
}
|
||||
ret.sigs.push(sigInfo);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Now look for a sig from a device
|
||||
// At some point this can probably go away and we'll just support
|
||||
// it being signed by the cross-signing master key
|
||||
const device = this.baseApis.crypto.deviceList.getStoredDevice(
|
||||
this.baseApis.getUserId(), sigInfo.deviceId,
|
||||
);
|
||||
if (device) {
|
||||
sigInfo.device = device;
|
||||
sigInfo.deviceTrust = await this.baseApis.checkDeviceTrust(
|
||||
this.baseApis.getUserId(), sigInfo.deviceId,
|
||||
);
|
||||
try {
|
||||
await verifySignature(
|
||||
this.baseApis.crypto.olmDevice,
|
||||
backupInfo.auth_data,
|
||||
this.baseApis.getUserId(),
|
||||
device.deviceId,
|
||||
device.getFingerprint(),
|
||||
);
|
||||
sigInfo.valid = true;
|
||||
} catch (e) {
|
||||
logger.info(
|
||||
"Bad signature from key ID " + keyId + " userID " + this.baseApis.getUserId() +
|
||||
" device ID " + device.deviceId + " fingerprint: " +
|
||||
device.getFingerprint(), backupInfo.auth_data, e,
|
||||
);
|
||||
sigInfo.valid = false;
|
||||
}
|
||||
} else {
|
||||
sigInfo.valid = null; // Can't determine validity because we don't have the signing device
|
||||
logger.info("Ignoring signature from unknown key " + keyId);
|
||||
}
|
||||
ret.sigs.push(sigInfo);
|
||||
}
|
||||
|
||||
ret.usable = ret.sigs.some((s) => {
|
||||
return (
|
||||
s.valid && (
|
||||
(s.device && s.deviceTrust.isVerified()) ||
|
||||
(s.crossSigningId)
|
||||
)
|
||||
);
|
||||
});
|
||||
ret.usable = ret.usable || ret.trusted_locally;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedules sending all keys waiting to be sent to the backup, if not already
|
||||
* scheduled. Retries if necessary.
|
||||
*
|
||||
* @param maxDelay Maximum delay to wait in ms. 0 means no delay.
|
||||
*/
|
||||
public async scheduleKeyBackupSend(maxDelay = 10000): Promise<void> {
|
||||
if (this.sendingBackups) return;
|
||||
|
||||
this.sendingBackups = true;
|
||||
|
||||
try {
|
||||
// wait between 0 and `maxDelay` seconds, to avoid backup
|
||||
// requests from different clients hitting the server all at
|
||||
// the same time when a new key is sent
|
||||
const delay = Math.random() * maxDelay;
|
||||
await sleep(delay, undefined);
|
||||
let numFailures = 0; // number of consecutive failures
|
||||
for (;;) {
|
||||
if (!this.algorithm) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const numBackedUp =
|
||||
await this.backupPendingKeys(KEY_BACKUP_KEYS_PER_REQUEST);
|
||||
if (numBackedUp === 0) {
|
||||
// no sessions left needing backup: we're done
|
||||
return;
|
||||
}
|
||||
numFailures = 0;
|
||||
} catch (err) {
|
||||
numFailures++;
|
||||
logger.log("Key backup request failed", err);
|
||||
if (err.data) {
|
||||
if (
|
||||
err.data.errcode == 'M_NOT_FOUND' ||
|
||||
err.data.errcode == 'M_WRONG_ROOM_KEYS_VERSION'
|
||||
) {
|
||||
// Re-check key backup status on error, so we can be
|
||||
// sure to present the current situation when asked.
|
||||
await this.checkKeyBackup();
|
||||
// Backup version has changed or this backup version
|
||||
// has been deleted
|
||||
this.baseApis.crypto.emit("crypto.keyBackupFailed", err.data.errcode);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (numFailures) {
|
||||
// exponential backoff if we have failures
|
||||
await sleep(1000 * Math.pow(2, Math.min(numFailures - 1, 4)), undefined);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.sendingBackups = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take some e2e keys waiting to be backed up and send them
|
||||
* to the backup.
|
||||
*
|
||||
* @param {integer} limit Maximum number of keys to back up
|
||||
* @returns {integer} Number of sessions backed up
|
||||
*/
|
||||
private async backupPendingKeys(limit: number): Promise<number> {
|
||||
const sessions = await this.baseApis.crypto.cryptoStore.getSessionsNeedingBackup(limit);
|
||||
if (!sessions.length) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
|
||||
const data = {};
|
||||
for (const session of sessions) {
|
||||
const roomId = session.sessionData.room_id;
|
||||
if (data[roomId] === undefined) {
|
||||
data[roomId] = { sessions: {} };
|
||||
}
|
||||
|
||||
const sessionData = await this.baseApis.crypto.olmDevice.exportInboundGroupSession(
|
||||
session.senderKey, session.sessionId, session.sessionData,
|
||||
);
|
||||
sessionData.algorithm = MEGOLM_ALGORITHM;
|
||||
|
||||
const forwardedCount =
|
||||
(sessionData.forwarding_curve25519_key_chain || []).length;
|
||||
|
||||
const userId = this.baseApis.crypto.deviceList.getUserByIdentityKey(
|
||||
MEGOLM_ALGORITHM, session.senderKey,
|
||||
);
|
||||
const device = this.baseApis.crypto.deviceList.getDeviceByIdentityKey(
|
||||
MEGOLM_ALGORITHM, session.senderKey,
|
||||
);
|
||||
const verified = this.baseApis.crypto.checkDeviceInfoTrust(userId, device).isVerified();
|
||||
|
||||
data[roomId]['sessions'][session.sessionId] = {
|
||||
first_message_index: sessionData.first_known_index,
|
||||
forwarded_count: forwardedCount,
|
||||
is_verified: verified,
|
||||
session_data: await this.algorithm.encryptSession(sessionData),
|
||||
};
|
||||
}
|
||||
|
||||
await this.baseApis.sendKeyBackup(
|
||||
undefined, undefined, this.backupInfo.version,
|
||||
{ rooms: data },
|
||||
);
|
||||
|
||||
await this.baseApis.crypto.cryptoStore.unmarkSessionsNeedingBackup(sessions);
|
||||
remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.crypto.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
|
||||
return sessions.length;
|
||||
}
|
||||
|
||||
public async backupGroupSession(
|
||||
senderKey: string, sessionId: string,
|
||||
): Promise<void> {
|
||||
await this.baseApis.crypto.cryptoStore.markSessionsNeedingBackup([{
|
||||
senderKey: senderKey,
|
||||
sessionId: sessionId,
|
||||
}]);
|
||||
|
||||
if (this.backupInfo) {
|
||||
// don't wait for this to complete: it will delay so
|
||||
// happens in the background
|
||||
this.scheduleKeyBackupSend();
|
||||
}
|
||||
// if this.backupInfo is not set, then the keys will be backed up when
|
||||
// this.enableKeyBackup is called
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks all group sessions as needing to be backed up and schedules them to
|
||||
* upload in the background as soon as possible.
|
||||
*/
|
||||
public async scheduleAllGroupSessionsForBackup(): Promise<void> {
|
||||
await this.flagAllGroupSessionsForBackup();
|
||||
|
||||
// Schedule keys to upload in the background as soon as possible.
|
||||
this.scheduleKeyBackupSend(0 /* maxDelay */);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks all group sessions as needing to be backed up without scheduling
|
||||
* them to upload in the background.
|
||||
* @returns {Promise<int>} Resolves to the number of sessions now requiring a backup
|
||||
* (which will be equal to the number of sessions in the store).
|
||||
*/
|
||||
public async flagAllGroupSessionsForBackup(): Promise<number> {
|
||||
await this.baseApis.crypto.cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_BACKUP,
|
||||
],
|
||||
(txn) => {
|
||||
this.baseApis.crypto.cryptoStore.getAllEndToEndInboundGroupSessions(txn, (session) => {
|
||||
if (session !== null) {
|
||||
this.baseApis.crypto.cryptoStore.markSessionsNeedingBackup([session], txn);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const remaining = await this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||
this.baseApis.emit("crypto.keyBackupSessionsRemaining", remaining);
|
||||
return remaining;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of end to end session keys that are waiting to be backed up
|
||||
* @returns {Promise<int>} Resolves to the number of sessions requiring backup
|
||||
*/
|
||||
public countSessionsNeedingBackup(): Promise<number> {
|
||||
return this.baseApis.crypto.cryptoStore.countSessionsNeedingBackup();
|
||||
}
|
||||
}
|
||||
|
||||
export class Curve25519 implements BackupAlgorithm {
|
||||
public static algorithmName = "m.megolm_backup.v1.curve25519-aes-sha2";
|
||||
|
||||
constructor(
|
||||
public authData: AuthData,
|
||||
private publicKey: any, // FIXME: PkEncryption
|
||||
private getKey: () => Promise<Uint8Array>,
|
||||
) {}
|
||||
|
||||
public static async init(
|
||||
authData: AuthData,
|
||||
getKey: () => Promise<Uint8Array>,
|
||||
): Promise<Curve25519> {
|
||||
if (!authData || !authData.public_key) {
|
||||
throw new Error("auth_data missing required information");
|
||||
}
|
||||
const publicKey = new global.Olm.PkEncryption();
|
||||
publicKey.set_recipient_key(authData.public_key);
|
||||
return new Curve25519(authData, publicKey, getKey);
|
||||
}
|
||||
|
||||
public static async prepare(
|
||||
key: string | Uint8Array | null,
|
||||
): Promise<[Uint8Array, AuthData]> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
const authData: AuthData = {};
|
||||
if (!key) {
|
||||
authData.public_key = decryption.generate_key();
|
||||
} else if (key instanceof Uint8Array) {
|
||||
authData.public_key = decryption.init_with_private_key(key);
|
||||
} else {
|
||||
const derivation = await keyFromPassphrase(key);
|
||||
authData.private_key_salt = derivation.salt;
|
||||
authData.private_key_iterations = derivation.iterations;
|
||||
authData.public_key = decryption.init_with_private_key(derivation.key);
|
||||
}
|
||||
const publicKey = new global.Olm.PkEncryption();
|
||||
publicKey.set_recipient_key(authData.public_key);
|
||||
|
||||
return [
|
||||
decryption.get_private_key(),
|
||||
authData,
|
||||
];
|
||||
} finally {
|
||||
decryption.free();
|
||||
}
|
||||
}
|
||||
|
||||
public async encryptSession(data: Record<string, any>): Promise<any> {
|
||||
const plainText: Record<string, any> = Object.assign({}, data);
|
||||
delete plainText.session_id;
|
||||
delete plainText.room_id;
|
||||
delete plainText.first_known_index;
|
||||
return this.publicKey.encrypt(JSON.stringify(plainText));
|
||||
}
|
||||
|
||||
public async decryptSessions(sessions: Record<string, Record<string, any>>): Promise<Record<string, any>[]> {
|
||||
const privKey = await this.getKey();
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
const backupPubKey = decryption.init_with_private_key(privKey);
|
||||
|
||||
if (backupPubKey !== this.authData.public_key) {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw { errcode: MatrixClient.RESTORE_BACKUP_ERROR_BAD_KEY };
|
||||
}
|
||||
|
||||
const keys = [];
|
||||
|
||||
for (const [sessionId, sessionData] of Object.entries(sessions)) {
|
||||
try {
|
||||
const decrypted = JSON.parse(decryption.decrypt(
|
||||
sessionData.session_data.ephemeral,
|
||||
sessionData.session_data.mac,
|
||||
sessionData.session_data.ciphertext,
|
||||
));
|
||||
decrypted.session_id = sessionId;
|
||||
keys.push(decrypted);
|
||||
} catch (e) {
|
||||
logger.log("Failed to decrypt megolm session from backup", e, sessionData);
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
} finally {
|
||||
decryption.free();
|
||||
}
|
||||
}
|
||||
|
||||
public async keyMatches(key: Uint8Array): Promise<boolean> {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
let pubKey;
|
||||
try {
|
||||
pubKey = decryption.init_with_private_key(key);
|
||||
} finally {
|
||||
decryption.free();
|
||||
}
|
||||
|
||||
return pubKey === this.authData.public_key;
|
||||
}
|
||||
|
||||
public free(): void {
|
||||
this.publicKey.free();
|
||||
}
|
||||
}
|
||||
|
||||
export const algorithmsByName: Record<string, BackupAlgorithmClass> = {
|
||||
[Curve25519.algorithmName]: Curve25519,
|
||||
};
|
||||
|
||||
export const DefaultAlgorithm: BackupAlgorithmClass = Curve25519;
|
||||
+39
-26
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,15 +14,28 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {decodeBase64, encodeBase64} from './olmlib';
|
||||
import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store';
|
||||
import {decryptAES, encryptAES} from './aes';
|
||||
import { decodeBase64, encodeBase64 } from './olmlib';
|
||||
import { IndexedDBCryptoStore } from '../crypto/store/indexeddb-crypto-store';
|
||||
import { decryptAES, encryptAES } from './aes';
|
||||
import anotherjson from "another-json";
|
||||
import {logger} from '../logger';
|
||||
import { logger } from '../logger';
|
||||
import { ISecretStorageKeyInfo } from "../matrix";
|
||||
|
||||
// FIXME: these types should eventually go in a different file
|
||||
type Signatures = Record<string, Record<string, string>>;
|
||||
|
||||
export interface IDehydratedDevice {
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
device_data: ISecretStorageKeyInfo & { // eslint-disable-line camelcase
|
||||
algorithm: string;
|
||||
account: string; // pickle
|
||||
};
|
||||
}
|
||||
|
||||
export interface IDehydratedDeviceKeyInfo {
|
||||
passphrase?: string;
|
||||
}
|
||||
|
||||
interface DeviceKeys {
|
||||
algorithms: Array<string>;
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
@@ -51,16 +64,16 @@ export class DehydrationManager {
|
||||
this.getDehydrationKeyFromCache();
|
||||
}
|
||||
async getDehydrationKeyFromCache(): Promise<void> {
|
||||
return await this.crypto._cryptoStore.doTxn(
|
||||
return await this.crypto.cryptoStore.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this.crypto._cryptoStore.getSecretStorePrivateKey(
|
||||
this.crypto.cryptoStore.getSecretStorePrivateKey(
|
||||
txn,
|
||||
async (result) => {
|
||||
if (result) {
|
||||
const {key, keyInfo, deviceDisplayName, time} = result;
|
||||
const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey);
|
||||
const { key, keyInfo, deviceDisplayName, time } = result;
|
||||
const pickleKey = Buffer.from(this.crypto.olmDevice._pickleKey);
|
||||
const decrypted = await decryptAES(key, pickleKey, DEHYDRATION_ALGORITHM);
|
||||
this.key = decodeBase64(decrypted);
|
||||
this.keyInfo = keyInfo;
|
||||
@@ -101,11 +114,11 @@ export class DehydrationManager {
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
// clear storage
|
||||
await this.crypto._cryptoStore.doTxn(
|
||||
await this.crypto.cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this.crypto._cryptoStore.storeSecretStorePrivateKey(
|
||||
this.crypto.cryptoStore.storeSecretStorePrivateKey(
|
||||
txn, "dehydration", null,
|
||||
);
|
||||
},
|
||||
@@ -145,15 +158,15 @@ export class DehydrationManager {
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
try {
|
||||
const pickleKey = Buffer.from(this.crypto._olmDevice._pickleKey);
|
||||
const pickleKey = Buffer.from(this.crypto.olmDevice._pickleKey);
|
||||
|
||||
// update the crypto store with the timestamp
|
||||
const key = await encryptAES(encodeBase64(this.key), pickleKey, DEHYDRATION_ALGORITHM);
|
||||
await this.crypto._cryptoStore.doTxn(
|
||||
await this.crypto.cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this.crypto._cryptoStore.storeSecretStorePrivateKey(
|
||||
this.crypto.cryptoStore.storeSecretStorePrivateKey(
|
||||
txn, "dehydration",
|
||||
{
|
||||
keyInfo: this.keyInfo,
|
||||
@@ -192,7 +205,7 @@ export class DehydrationManager {
|
||||
}
|
||||
|
||||
logger.log("Uploading account to server");
|
||||
const dehydrateResult = await this.crypto._baseApis._http.authedRequest(
|
||||
const dehydrateResult = await this.crypto.baseApis.http.authedRequest(
|
||||
undefined,
|
||||
"PUT",
|
||||
"/dehydrated_device",
|
||||
@@ -210,9 +223,9 @@ export class DehydrationManager {
|
||||
const deviceId = dehydrateResult.device_id;
|
||||
logger.log("Preparing device keys", deviceId);
|
||||
const deviceKeys: DeviceKeys = {
|
||||
algorithms: this.crypto._supportedAlgorithms,
|
||||
algorithms: this.crypto.supportedAlgorithms,
|
||||
device_id: deviceId,
|
||||
user_id: this.crypto._userId,
|
||||
user_id: this.crypto.userId,
|
||||
keys: {
|
||||
[`ed25519:${deviceId}`]: e2eKeys.ed25519,
|
||||
[`curve25519:${deviceId}`]: e2eKeys.curve25519,
|
||||
@@ -220,21 +233,21 @@ export class DehydrationManager {
|
||||
};
|
||||
const deviceSignature = account.sign(anotherjson.stringify(deviceKeys));
|
||||
deviceKeys.signatures = {
|
||||
[this.crypto._userId]: {
|
||||
[this.crypto.userId]: {
|
||||
[`ed25519:${deviceId}`]: deviceSignature,
|
||||
},
|
||||
};
|
||||
if (this.crypto._crossSigningInfo.getId("self_signing")) {
|
||||
await this.crypto._crossSigningInfo.signObject(deviceKeys, "self_signing");
|
||||
if (this.crypto.crossSigningInfo.getId("self_signing")) {
|
||||
await this.crypto.crossSigningInfo.signObject(deviceKeys, "self_signing");
|
||||
}
|
||||
|
||||
logger.log("Preparing one-time keys");
|
||||
const oneTimeKeys = {};
|
||||
for (const [keyId, key] of Object.entries(otks.curve25519)) {
|
||||
const k: OneTimeKey = {key};
|
||||
const k: OneTimeKey = { key };
|
||||
const signature = account.sign(anotherjson.stringify(k));
|
||||
k.signatures = {
|
||||
[this.crypto._userId]: {
|
||||
[this.crypto.userId]: {
|
||||
[`ed25519:${deviceId}`]: signature,
|
||||
},
|
||||
};
|
||||
@@ -244,10 +257,10 @@ export class DehydrationManager {
|
||||
logger.log("Preparing fallback keys");
|
||||
const fallbackKeys = {};
|
||||
for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
|
||||
const k: OneTimeKey = {key, fallback: true};
|
||||
const k: OneTimeKey = { key, fallback: true };
|
||||
const signature = account.sign(anotherjson.stringify(k));
|
||||
k.signatures = {
|
||||
[this.crypto._userId]: {
|
||||
[this.crypto.userId]: {
|
||||
[`ed25519:${deviceId}`]: signature,
|
||||
},
|
||||
};
|
||||
@@ -255,7 +268,7 @@ export class DehydrationManager {
|
||||
}
|
||||
|
||||
logger.log("Uploading keys to server");
|
||||
await this.crypto._baseApis._http.authedRequest(
|
||||
await this.crypto.baseApis.http.authedRequest(
|
||||
undefined,
|
||||
"POST",
|
||||
"/keys/upload/" + encodeURI(deviceId),
|
||||
@@ -279,7 +292,7 @@ export class DehydrationManager {
|
||||
}
|
||||
}
|
||||
|
||||
private stop() {
|
||||
public stop() {
|
||||
if (this.timeoutId) {
|
||||
global.clearTimeout(this.timeoutId);
|
||||
this.timeoutId = undefined;
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module crypto/deviceinfo
|
||||
*/
|
||||
|
||||
/**
|
||||
* Information about a user's device
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:crypto/deviceinfo
|
||||
*
|
||||
* @property {string} deviceId the ID of this device
|
||||
*
|
||||
* @property {string[]} algorithms list of algorithms supported by this device
|
||||
*
|
||||
* @property {Object.<string,string>} keys a map from
|
||||
* <key type>:<id> -> <base64-encoded key>>
|
||||
*
|
||||
* @property {module:crypto/deviceinfo.DeviceVerification} verified
|
||||
* whether the device has been verified/blocked by the user
|
||||
*
|
||||
* @property {boolean} known
|
||||
* whether the user knows of this device's existence (useful when warning
|
||||
* the user that a user has added new devices)
|
||||
*
|
||||
* @property {Object} unsigned additional data from the homeserver
|
||||
*
|
||||
* @param {string} deviceId id of the device
|
||||
*/
|
||||
export function DeviceInfo(deviceId) {
|
||||
// you can't change the deviceId
|
||||
Object.defineProperty(this, 'deviceId', {
|
||||
enumerable: true,
|
||||
value: deviceId,
|
||||
});
|
||||
|
||||
this.algorithms = [];
|
||||
this.keys = {};
|
||||
this.verified = DeviceVerification.UNVERIFIED;
|
||||
this.known = false;
|
||||
this.unsigned = {};
|
||||
this.signatures = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* rehydrate a DeviceInfo from the session store
|
||||
*
|
||||
* @param {object} obj raw object from session store
|
||||
* @param {string} deviceId id of the device
|
||||
*
|
||||
* @return {module:crypto~DeviceInfo} new DeviceInfo
|
||||
*/
|
||||
DeviceInfo.fromStorage = function(obj, deviceId) {
|
||||
const res = new DeviceInfo(deviceId);
|
||||
for (const prop in obj) {
|
||||
if (obj.hasOwnProperty(prop)) {
|
||||
res[prop] = obj[prop];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
/**
|
||||
* Prepare a DeviceInfo for JSON serialisation in the session store
|
||||
*
|
||||
* @return {object} deviceinfo with non-serialised members removed
|
||||
*/
|
||||
DeviceInfo.prototype.toStorage = function() {
|
||||
return {
|
||||
algorithms: this.algorithms,
|
||||
keys: this.keys,
|
||||
verified: this.verified,
|
||||
known: this.known,
|
||||
unsigned: this.unsigned,
|
||||
signatures: this.signatures,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the fingerprint for this device (ie, the Ed25519 key)
|
||||
*
|
||||
* @return {string} base64-encoded fingerprint of this device
|
||||
*/
|
||||
DeviceInfo.prototype.getFingerprint = function() {
|
||||
return this.keys["ed25519:" + this.deviceId];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the identity key for this device (ie, the Curve25519 key)
|
||||
*
|
||||
* @return {string} base64-encoded identity key of this device
|
||||
*/
|
||||
DeviceInfo.prototype.getIdentityKey = function() {
|
||||
return this.keys["curve25519:" + this.deviceId];
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the configured display name for this device, if any
|
||||
*
|
||||
* @return {string?} displayname
|
||||
*/
|
||||
DeviceInfo.prototype.getDisplayName = function() {
|
||||
return this.unsigned.device_display_name || null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if this device is blocked
|
||||
*
|
||||
* @return {Boolean} true if blocked
|
||||
*/
|
||||
DeviceInfo.prototype.isBlocked = function() {
|
||||
return this.verified == DeviceVerification.BLOCKED;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if this device is verified
|
||||
*
|
||||
* @return {Boolean} true if verified
|
||||
*/
|
||||
DeviceInfo.prototype.isVerified = function() {
|
||||
return this.verified == DeviceVerification.VERIFIED;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if this device is unverified
|
||||
*
|
||||
* @return {Boolean} true if unverified
|
||||
*/
|
||||
DeviceInfo.prototype.isUnverified = function() {
|
||||
return this.verified == DeviceVerification.UNVERIFIED;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns true if the user knows about this device's existence
|
||||
*
|
||||
* @return {Boolean} true if known
|
||||
*/
|
||||
DeviceInfo.prototype.isKnown = function() {
|
||||
return this.known == true;
|
||||
};
|
||||
|
||||
/**
|
||||
* @enum
|
||||
*/
|
||||
DeviceInfo.DeviceVerification = {
|
||||
VERIFIED: 1,
|
||||
UNVERIFIED: 0,
|
||||
BLOCKED: -1,
|
||||
};
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
/*
|
||||
Copyright 2016 - 2021 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @module crypto/deviceinfo
|
||||
*/
|
||||
|
||||
export interface IDevice {
|
||||
keys: Record<string, string>;
|
||||
algorithms: string[];
|
||||
verified: DeviceVerification;
|
||||
known: boolean;
|
||||
unsigned?: Record<string, any>;
|
||||
signatures?: Record<string, string>;
|
||||
}
|
||||
|
||||
enum DeviceVerification {
|
||||
Blocked = -1,
|
||||
Unverified = 0,
|
||||
Verified = 1,
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about a user's device
|
||||
*
|
||||
* @constructor
|
||||
* @alias module:crypto/deviceinfo
|
||||
*
|
||||
* @property {string} deviceId the ID of this device
|
||||
*
|
||||
* @property {string[]} algorithms list of algorithms supported by this device
|
||||
*
|
||||
* @property {Object.<string,string>} keys a map from
|
||||
* <key type>:<id> -> <base64-encoded key>>
|
||||
*
|
||||
* @property {module:crypto/deviceinfo.DeviceVerification} verified
|
||||
* whether the device has been verified/blocked by the user
|
||||
*
|
||||
* @property {boolean} known
|
||||
* whether the user knows of this device's existence (useful when warning
|
||||
* the user that a user has added new devices)
|
||||
*
|
||||
* @property {Object} unsigned additional data from the homeserver
|
||||
*
|
||||
* @param {string} deviceId id of the device
|
||||
*/
|
||||
export class DeviceInfo {
|
||||
/**
|
||||
* rehydrate a DeviceInfo from the session store
|
||||
*
|
||||
* @param {object} obj raw object from session store
|
||||
* @param {string} deviceId id of the device
|
||||
*
|
||||
* @return {module:crypto~DeviceInfo} new DeviceInfo
|
||||
*/
|
||||
public static fromStorage(obj: IDevice, deviceId: string): DeviceInfo {
|
||||
const res = new DeviceInfo(deviceId);
|
||||
for (const prop in obj) {
|
||||
if (obj.hasOwnProperty(prop)) {
|
||||
res[prop] = obj[prop];
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @enum
|
||||
*/
|
||||
public static DeviceVerification = {
|
||||
VERIFIED: DeviceVerification.Verified,
|
||||
UNVERIFIED: DeviceVerification.Unverified,
|
||||
BLOCKED: DeviceVerification.Blocked,
|
||||
};
|
||||
|
||||
public algorithms: string[];
|
||||
public keys: Record<string, string> = {};
|
||||
public verified = DeviceVerification.Unverified;
|
||||
public known = false;
|
||||
public unsigned: Record<string, any> = {};
|
||||
public signatures: Record<string, string> = {};
|
||||
|
||||
constructor(public readonly deviceId: string) {}
|
||||
|
||||
/**
|
||||
* Prepare a DeviceInfo for JSON serialisation in the session store
|
||||
*
|
||||
* @return {object} deviceinfo with non-serialised members removed
|
||||
*/
|
||||
public toStorage(): IDevice {
|
||||
return {
|
||||
algorithms: this.algorithms,
|
||||
keys: this.keys,
|
||||
verified: this.verified,
|
||||
known: this.known,
|
||||
unsigned: this.unsigned,
|
||||
signatures: this.signatures,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the fingerprint for this device (ie, the Ed25519 key)
|
||||
*
|
||||
* @return {string} base64-encoded fingerprint of this device
|
||||
*/
|
||||
public getFingerprint(): string {
|
||||
return this.keys["ed25519:" + this.deviceId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the identity key for this device (ie, the Curve25519 key)
|
||||
*
|
||||
* @return {string} base64-encoded identity key of this device
|
||||
*/
|
||||
public getIdentityKey(): string {
|
||||
return this.keys["curve25519:" + this.deviceId];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured display name for this device, if any
|
||||
*
|
||||
* @return {string?} displayname
|
||||
*/
|
||||
public getDisplayName(): string | null {
|
||||
return this.unsigned.device_display_name || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this device is blocked
|
||||
*
|
||||
* @return {Boolean} true if blocked
|
||||
*/
|
||||
public isBlocked(): boolean {
|
||||
return this.verified == DeviceVerification.Blocked;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this device is verified
|
||||
*
|
||||
* @return {Boolean} true if verified
|
||||
*/
|
||||
public isVerified(): boolean {
|
||||
return this.verified == DeviceVerification.Verified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this device is unverified
|
||||
*
|
||||
* @return {Boolean} true if unverified
|
||||
*/
|
||||
public isUnverified(): boolean {
|
||||
return this.verified == DeviceVerification.Unverified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the user knows about this device's existence
|
||||
*
|
||||
* @return {Boolean} true if known
|
||||
*/
|
||||
public isKnown(): boolean {
|
||||
return this.known === true;
|
||||
}
|
||||
}
|
||||
-3969
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user