Compare commits
1384 Commits
v5.1.0
...
v11.0.0-rc.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 684511ff06 | |||
| 88b328df7e | |||
| e3583dd04e | |||
| 2d73805ca3 | |||
| eedfa550a6 | |||
| 4ca718adc4 | |||
| 7c4ced8f46 | |||
| 7018f4ab25 | |||
| fda13875ef | |||
| 8005917452 | |||
| a449c5f8c2 | |||
| 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 | |||
| 069ca4a89d | |||
| e3ba08fbbc | |||
| dd84e51161 | |||
| bca8568d64 | |||
| 5407717534 | |||
| 74ef760591 | |||
| b435b582bd | |||
| d46021a05e | |||
| 1b31d0622e | |||
| 68206a6e19 | |||
| 56797948af | |||
| c0af2f25a1 | |||
| 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 | |||
| 45f3a2f909 | |||
| 5904378170 | |||
| f6e8048d9e | |||
| 5afca17d27 | |||
| 2d7f5ae279 | |||
| 458384d658 | |||
| 159b98132d | |||
| 349bb2730a | |||
| c13813348d | |||
| 26e70d6b30 | |||
| f6d3b50b08 | |||
| 50ee489079 | |||
| b60e5f40ab | |||
| 5b1fdb7b37 | |||
| 65d4015300 | |||
| b692cd109e | |||
| 0f90f055ba | |||
| 28d5ce288c | |||
| c701bf279f | |||
| 6039066e7f | |||
| c9a2f8b170 | |||
| b34a36d853 | |||
| f8f76f6806 | |||
| e25ae546fc | |||
| 5b73bf3e5d | |||
| c16b093bd7 | |||
| e406f32386 | |||
| c4e7c149a4 | |||
| f91edfabbb | |||
| f410004d45 | |||
| 49e238d580 | |||
| 489d188966 | |||
| 79fb7bab0b | |||
| c410954bad | |||
| 53a8a7d50f | |||
| 1c7f95c0ee | |||
| 1717fcf499 | |||
| b25453cf87 | |||
| 07b596bf30 | |||
| 1166947c21 | |||
| 6e7b9ca6c0 | |||
| 5b1e3537cc | |||
| 44843d418d | |||
| a103ffa038 | |||
| 712335789e | |||
| cdf8186f44 | |||
| d06942d602 | |||
| 45ac3a60dc | |||
| 4f244da3ec | |||
| 4eefa05d3f | |||
| 40fb31099a | |||
| bcd85f5397 | |||
| 89aeda45c6 | |||
| 7581e5ffdc | |||
| 7046fa3224 | |||
| ef392785e8 | |||
| 5bd029115c | |||
| b6f42b25dd | |||
| 75dd9625a0 | |||
| fd6110679e | |||
| c761019aca | |||
| 853363fdf5 | |||
| f7753f8be3 | |||
| d2dfa29556 | |||
| c0a88b7f4e | |||
| 150e5fede4 | |||
| bb3ec322fb | |||
| f3ee164a7d | |||
| 035cb9fe08 | |||
| 58d0018174 | |||
| 52ed0f8615 | |||
| 2a46513dfd | |||
| 907567182d | |||
| 46cebcd1ca | |||
| 40198f95dc | |||
| 736b934b18 | |||
| b009811ac9 | |||
| ff6612f9d0 | |||
| 565d446b1d | |||
| 044d334398 | |||
| e31ef2dfb5 | |||
| c20566083a | |||
| 9845553a5f | |||
| 03737546fe | |||
| 579cb00c98 | |||
| a307950213 | |||
| 0f5c469be6 | |||
| 6f7e409e9a | |||
| d707912d81 | |||
| 97ab680f4e | |||
| ea35a29bd8 | |||
| 63c182bc3e | |||
| b1a0a12a5f | |||
| cc242230be | |||
| aef9211ea8 | |||
| de5d557882 | |||
| fa9adf199d | |||
| 0807066f1f | |||
| 142e79941d | |||
| 5e9ce38a24 | |||
| f42e6373c4 | |||
| bc46609caa | |||
| cc44abe2d3 | |||
| db6398acdd | |||
| 6661bde608 | |||
| 69f6bba964 | |||
| 2a4e722f0f | |||
| dd20828ded | |||
| 5993dd588c | |||
| 30f86e2437 | |||
| 3ef91e16d8 | |||
| 45e9b3ac68 | |||
| a076e3f0fc | |||
| 99bff04ccc | |||
| bd906e619d | |||
| 34882cc438 | |||
| 5ac00e3465 | |||
| 622dd065ff | |||
| c5c98a6ac1 | |||
| da423ed508 | |||
| 11c4337cfc | |||
| 458164384d | |||
| 13c7f55a79 | |||
| 4ab675863a | |||
| 5414b3b39d | |||
| c54db30dc8 | |||
| f11103bfcc | |||
| b56936003d | |||
| f61604a51e | |||
| ae77f900ef | |||
| 645842f0fd | |||
| 39d3640973 | |||
| 66aa9c4831 | |||
| 7de0ca2048 | |||
| 5bbc5cad9f | |||
| 0a7a80d5a8 | |||
| 33d1a33a17 | |||
| 7f130949c8 | |||
| ba7ee37899 | |||
| f8863d5c24 | |||
| 1d7954c831 | |||
| 2bb4e91dfd | |||
| abe5bf4240 | |||
| c493bf7866 | |||
| 06bf0f22be | |||
| 02d9fe1d30 | |||
| c3091c5aa4 | |||
| 677a427f1f | |||
| c416dd01a7 | |||
| 662fcb426b | |||
| fd0fe9e225 | |||
| 0ca8613896 | |||
| 4b1817719e | |||
| 295c591e95 | |||
| 9df0480b78 | |||
| 5260f40451 | |||
| 0cbe35e41f | |||
| 502745271d | |||
| 95baa3cd27 | |||
| 8fe4a29176 | |||
| 1a1a0e7324 | |||
| d00d07a1c1 | |||
| f27db16e30 | |||
| d4e107b3cd | |||
| 881b60c85f | |||
| c5e1aade12 | |||
| 28198a6f40 | |||
| 30a01e26de | |||
| 8712703f7c | |||
| e3a8631faa | |||
| 3eab51ce79 | |||
| 9db0fe0795 | |||
| 228af037e3 | |||
| 654f250fd8 | |||
| a8693d9d68 | |||
| f9f345e428 | |||
| e678706414 | |||
| 8018259480 | |||
| 9f713781cd | |||
| 48d56dc5c0 | |||
| 701dfa09b4 | |||
| d965648fd7 | |||
| 08c15e7203 | |||
| 9b9e52d0a2 | |||
| 38cc8fe7dc | |||
| 590fac0fa9 | |||
| 9590c8aaf0 | |||
| e2b79e4e7e | |||
| 2df588f95a | |||
| 7c3af91b42 | |||
| 4cc4b28c47 | |||
| fad9b4c67f | |||
| ade3b3a021 | |||
| d8c4101fdd | |||
| a12c250f2b | |||
| 57eef2d832 | |||
| b67a179a54 | |||
| 06044b39c3 | |||
| d16cf26c5f | |||
| b060c5af38 | |||
| b28bad651e | |||
| 5c4b7a3213 | |||
| 7f21c591ae | |||
| 65b24f595c | |||
| 9d9c2720c2 | |||
| f845100062 | |||
| e6155f9e37 | |||
| e9590e9093 | |||
| 49f2d1501c | |||
| c6819e0450 | |||
| f518ea95f4 | |||
| 92c6332143 | |||
| 0df1a7da21 | |||
| 487a9c0967 | |||
| fb89761671 | |||
| d1d3ae074d | |||
| 7dedaf90c3 | |||
| 687b98a09d | |||
| c6b2e9873c | |||
| 1e80491675 | |||
| a727da9193 | |||
| 0b7754581a | |||
| 452e8ea385 | |||
| a189de9a2e | |||
| 8632ca6e37 | |||
| 293860b6c5 | |||
| 07667172cd | |||
| f5240cdac6 | |||
| 7529d2b638 | |||
| 03fc12e888 | |||
| e05a50528e | |||
| 356ee90417 | |||
| 5dced57724 | |||
| 8ec3b88c5d | |||
| d7ef128510 | |||
| bb2502409b | |||
| aa6aab4245 | |||
| 12aab0caeb | |||
| 2f316c558d | |||
| f447273b75 | |||
| c8a18d51e6 | |||
| d77af1e67a | |||
| 81c95224d1 | |||
| a0e66291df | |||
| 5733f46f4c | |||
| da2128feff | |||
| d413faefdb | |||
| 0d1d767d96 | |||
| 5619554023 | |||
| 53880a4bb2 | |||
| 812ae227b6 | |||
| c6b44098ac | |||
| 92b95a98d0 | |||
| a3505ff42d | |||
| 15b2e3ff1d | |||
| 1845d1ac55 | |||
| 463819caa7 | |||
| a0317d9587 | |||
| fa6ce0cb70 | |||
| 7471ff4b0d | |||
| 65a5bfac88 | |||
| d2321410f8 | |||
| f90b5a99cd | |||
| cc4656b36a | |||
| 34bc63a146 | |||
| 09bd91a588 | |||
| bb3e082e5b | |||
| 60c863f829 | |||
| 2d2a73bf52 | |||
| 7b9f73709d | |||
| 1dc89f642d | |||
| a78b59010a | |||
| a2e1d94fcf | |||
| b181c83b93 | |||
| cf7c84c4ba | |||
| 6e5230f9f9 | |||
| f03f7c0acb | |||
| aa9b807b82 | |||
| fa9921e091 | |||
| a9a6b2de48 | |||
| 1ef746658f | |||
| fe0099b497 | |||
| 7e9c4146a5 | |||
| 3e978c64e4 | |||
| 13c5920a46 | |||
| 3b19203fd2 | |||
| 84cd05b218 | |||
| 9b9a9642ee | |||
| 639d2317ed | |||
| 52379d7655 | |||
| 29827362d6 | |||
| f33315f610 | |||
| aec92a41da | |||
| d570811ddc | |||
| 9cd015a218 | |||
| 8d2cc5096e | |||
| 7ec0bf69f3 | |||
| f3a8306107 | |||
| 2b29e9934c | |||
| 4b1104e463 | |||
| 60b9ef959d | |||
| 0074c2cf57 | |||
| b0204ab54e | |||
| bc5b07aa75 | |||
| cfe90dbed5 | |||
| fa913655bc | |||
| af0b6fc6ac | |||
| 6347031f61 | |||
| 3eda039898 | |||
| 5447d99481 | |||
| 9d08744fe2 | |||
| 848fa257ea | |||
| 54553fb671 | |||
| aad7484c8d | |||
| ad658ead37 | |||
| c787f0e9d0 | |||
| 6f5da701aa | |||
| a01368bd60 | |||
| 16dccd75c1 | |||
| 5151b52688 | |||
| f682097e45 | |||
| 89ebffd69f | |||
| cdeb4ead9e | |||
| 79cc835874 | |||
| 8df866865d | |||
| fa7eff50e5 | |||
| 6558881952 | |||
| 9cbe2c985d | |||
| 7769c53cd6 | |||
| 7a5a47ecc7 | |||
| 492c1d77d8 | |||
| 8c315ebd15 | |||
| 6044b2cbad | |||
| 7acfd0b423 | |||
| e201aab440 | |||
| 3952768667 | |||
| fb25fa3a27 | |||
| 857ad9b180 | |||
| 14843a1bca | |||
| 6331b34cf7 | |||
| 2ebe1dfa16 | |||
| 6bdbee533c | |||
| b9886d4f34 | |||
| 666cbbce08 | |||
| 69c575a4be | |||
| c920de0d28 | |||
| 53cdf53f63 | |||
| d9dda6aebc | |||
| 3d4a9a24ce | |||
| de339d3098 | |||
| 0f83234be9 | |||
| bcd9b45589 | |||
| 19f3996e09 | |||
| 25c2cc1768 | |||
| eec7c4c61b | |||
| 25b4b049b7 | |||
| 2191eb3f41 | |||
| bebdbf7e05 | |||
| 646c091966 | |||
| 952729cb1b | |||
| c6992e2056 | |||
| 77ed79e9a9 | |||
| c4f4add0ec | |||
| 7eeb60c838 | |||
| 438861ae5e | |||
| d42cdbbc5b | |||
| 66237e1ea6 | |||
| a51c0450c3 | |||
| 7a2416bb6d | |||
| a1528e9e33 | |||
| 71cc4d535e | |||
| c06723df3d | |||
| 06b285c013 | |||
| 49c06ef0ca | |||
| 5f92357fec | |||
| 3e0dd3d918 | |||
| f19d76b08d | |||
| 22713d8f89 | |||
| 8b6b16067b | |||
| a2da0de17d | |||
| 93ff3edb6b | |||
| 7c67fd69dd | |||
| 5ef5412a55 | |||
| e88a384aa7 | |||
| 9067feeafb | |||
| ed978f69fb | |||
| 743f2465ea | |||
| 41fffa233a | |||
| e45377166b | |||
| 24939bf0b0 | |||
| 3221be4855 | |||
| 3135f1ed24 | |||
| 1b0834ffb0 | |||
| ad85740ae2 | |||
| d79d613cb7 | |||
| d8cc1f7b7a | |||
| d7c8856fdd | |||
| 9d80a332aa | |||
| e14f7b63c7 | |||
| 3bd2880923 | |||
| 6ec7e3a0b7 | |||
| 1a1fe759c3 | |||
| 2401ad7159 | |||
| a919c798f8 | |||
| 80fe66c481 | |||
| 4577dc8f44 | |||
| cf0a5305e0 | |||
| e69e6e1981 | |||
| 0febd99fbe | |||
| 6e8e3e4150 | |||
| 5d95398621 | |||
| 64cdd73b93 | |||
| 48a9236ea8 | |||
| 8b3126e9d8 | |||
| 74d497cd2d | |||
| 5070a5c598 | |||
| 2e30b08e74 | |||
| 8bf63f5f0b | |||
| 11665d18ee | |||
| a8a9fc0c9d | |||
| 098cd1b8d4 | |||
| 3166a4880d | |||
| 9d1c7136cc | |||
| e100943edf | |||
| a6fe4cdf1c | |||
| 8b5213c09a | |||
| 23a133c825 | |||
| 69c4496dfe | |||
| 8a4440c314 | |||
| 4d74cca206 | |||
| 955e081699 | |||
| b7e73422ab | |||
| a9a4ba33aa | |||
| 7116ad9f58 | |||
| 3d18bdb2aa | |||
| 4c14581606 | |||
| a9c9ec3977 | |||
| 694d1f9631 | |||
| 2b9cfae18a | |||
| 800d8380ce | |||
| 621ca28f68 | |||
| 4792241ff6 | |||
| 0b984df5f9 | |||
| 2e260155ea | |||
| b15c8a2d1c | |||
| 94ab317f23 | |||
| 02b37e1219 | |||
| 9d25848a21 | |||
| 3958768e1f | |||
| cec00cd303 | |||
| 45cfef4294 | |||
| 2a01e99635 | |||
| a9c3aee447 | |||
| c669382e12 | |||
| 19251c427a | |||
| a7aec9e2a4 | |||
| 99d7622b42 | |||
| 8728619d2c | |||
| f6d51fdfb8 | |||
| 0ffaa8d617 | |||
| 8a974172ab | |||
| 72675f7266 | |||
| 0f559050d8 | |||
| 58084774bf | |||
| c7aee7cebd | |||
| c68d135ae8 | |||
| 9ed6c99ec8 | |||
| 7d398d41d0 | |||
| 0af763252c | |||
| 1c9dbbbb19 | |||
| 2a688bdac8 | |||
| 3c53c818cb | |||
| 8e53cb324e | |||
| efbd454775 | |||
| 68c7273f56 | |||
| 8b56ff4eff | |||
| 8134eedd93 | |||
| a87ae770cc | |||
| 355da0f9a9 | |||
| 10bdd63762 | |||
| 69dc518c2c | |||
| fa1dddf06c | |||
| f683f4544a | |||
| bc5b587651 | |||
| 8083031029 | |||
| 7b8102a42c | |||
| dbe2f5e4db | |||
| f27791b7de | |||
| 2a78170395 | |||
| cfca1c7b06 | |||
| fb7a67025f | |||
| 8a6cd48b8e | |||
| e21d1f539d | |||
| f62049559c | |||
| 1fe9dd03a3 | |||
| c3283a7297 | |||
| f423164a1c | |||
| 75fe596e24 | |||
| c5eb290e66 | |||
| d3b2c8246d | |||
| c11796af4b | |||
| 8591815d66 | |||
| b82870adb2 | |||
| 3c5b304b6b | |||
| f656698061 | |||
| d4b4bc5031 | |||
| 9bcf33b6d3 | |||
| fa550e8f03 | |||
| 2d73564eba | |||
| 1bd80247fd | |||
| 1c194e8163 | |||
| db7848c9ba | |||
| 97497ed9d5 | |||
| 60fcc652de | |||
| cb57717424 | |||
| 590f7786eb | |||
| 0024edcb7f | |||
| aa2d0d9a08 | |||
| fd126b8563 | |||
| bc97e7a5ea | |||
| 3dece4f46b | |||
| be05452c70 | |||
| 583650cf7d | |||
| 505915528f | |||
| ace8a787b4 | |||
| ed2ea9ac8e | |||
| 8d09a4abe6 | |||
| 1da959ab02 | |||
| 997dd9b88a | |||
| ebe66bdd6e | |||
| 12b573bc63 | |||
| cc8c163e0f | |||
| abc7f76679 | |||
| eee04895fe | |||
| f520b88f79 | |||
| f0f1c113e4 | |||
| 84a15761ad | |||
| 013fbb87a7 | |||
| 73764d23dc | |||
| 8f62703bf2 | |||
| 6dedae2e4d | |||
| d32131b2b8 | |||
| 0a790b2ae3 | |||
| ef1d5e3d76 | |||
| 30720bfdd7 | |||
| 234a18f016 | |||
| c525a19df5 | |||
| a987a31667 | |||
| 10329c3436 | |||
| 29f10bcd44 | |||
| 19fe9b8ac7 | |||
| 76da708352 | |||
| 145f01ff2d | |||
| 2c2d531e7f | |||
| 91e0f7fbc4 | |||
| bebeec7d84 | |||
| 18b1e00875 | |||
| 86d448c285 | |||
| 73f8867a6f | |||
| d021498fa9 | |||
| b83aa54661 | |||
| 429550ca3e | |||
| 2a6d8c2b1d | |||
| 01c9159830 | |||
| 3b3ed5159c | |||
| 636661dd45 | |||
| 0d7cb2bf25 | |||
| 991f590056 | |||
| 0e6758ccbc | |||
| 1d9c6520e5 | |||
| c81f11df0a | |||
| f39a1e70de | |||
| f0fa249d36 | |||
| 8f72197817 | |||
| 1556ac84da | |||
| b6da6bb835 | |||
| b1a882ea79 | |||
| 3305f2cc72 | |||
| c589a5211b | |||
| ff0d91979b | |||
| 9d6888cf74 | |||
| a4a7097c10 | |||
| cf2b058e7c | |||
| 048d7a9b63 | |||
| d6e37d0288 | |||
| 3cd562fa96 | |||
| bd569cb041 | |||
| e3c6a0e1a0 | |||
| b29176507c | |||
| db25e9719b | |||
| 661901b00d | |||
| b9352cfcc1 | |||
| e381b1901e | |||
| ebdff505eb | |||
| f806e4342e | |||
| 7f32d7d320 | |||
| b1b4b21d45 | |||
| 486e8b5445 | |||
| 9fe0e1e85f | |||
| 8d81240c58 | |||
| bdadcd4532 | |||
| 1de9a24677 | |||
| 6335f14a34 | |||
| da2ef381ac | |||
| 7b173f4c74 | |||
| d49eb0bbc4 | |||
| 91556d5bcd | |||
| 039abe1f75 | |||
| bc32faf467 | |||
| 74a4dfeb67 | |||
| f120533fad | |||
| a1baf39299 | |||
| 9f0f1bcc68 | |||
| 246963e181 | |||
| e3134ab0de | |||
| b38d52da72 | |||
| 411c4f40d9 | |||
| 694a85b652 | |||
| c84e72f53a | |||
| cb7e1a9d82 | |||
| 3c9dfc195e | |||
| a5c13041c6 | |||
| d699e98346 | |||
| 135a76febd | |||
| fae2856e7e | |||
| 98426b6186 | |||
| a63d9d2ccd | |||
| c567139e28 | |||
| 3d495b0753 | |||
| 4946b55c21 | |||
| ad7d7154f4 | |||
| e5b6a9f8cb | |||
| 04f27d36ef | |||
| 683fc98fdc | |||
| c303e83444 | |||
| ae5a40d686 | |||
| 409b7068bb | |||
| f2a12c7154 | |||
| efac2eac07 | |||
| f3f6f3e39a | |||
| f905586dbd | |||
| f393cea2c2 | |||
| e937998b40 | |||
| f85ec08886 | |||
| 7ff68f3844 | |||
| 04529bd524 | |||
| 2c681d93d9 | |||
| 1451fcb040 | |||
| 4986f3c2ca | |||
| e9edb85a32 | |||
| dca60ae4ae | |||
| 025f964b0b | |||
| ff46a8fa9e | |||
| 59412aba51 | |||
| a351ee9f76 | |||
| a5b14092cd | |||
| 90f1de9c3f | |||
| a516f06946 | |||
| 7c1f3e4519 | |||
| 3bb1a6336b | |||
| 7dc926596f | |||
| e775515c38 | |||
| c116f2b1bc | |||
| f24993d6aa | |||
| f053cf1fdb | |||
| 251318eaf0 | |||
| a66e7d79ad | |||
| 38884083a2 | |||
| 0b1088d9a8 | |||
| 875f9ca388 | |||
| 25cd1f25f1 | |||
| 7152ece70a | |||
| 896c76ce9d | |||
| 21d3dd4506 | |||
| 300b32c8d7 | |||
| 19e3b35fc7 | |||
| 320811f9ed | |||
| 991457496f | |||
| 1cb6134758 | |||
| a255b8e450 | |||
| 324f036b35 | |||
| 479a284e10 | |||
| e0660ce01c | |||
| 3a3f6cb7ca | |||
| 9797e6021b | |||
| a9212d33b1 | |||
| d32d3fd864 | |||
| a19cdd06cf | |||
| f4d0f89fda | |||
| ca34cb951e | |||
| 4c2d1b0385 | |||
| 6b4fefc123 | |||
| 86913dccb0 | |||
| 30101a4428 | |||
| 668e8f6f24 | |||
| 2f51e206c7 | |||
| 78b8b36a87 | |||
| af3ef86d19 | |||
| 50febaf477 | |||
| 907f317b04 | |||
| 6edb78d015 | |||
| b58846ab6e | |||
| 32233a2c7b | |||
| 8b2752441d | |||
| e6af29df94 | |||
| 4ae5404fe4 | |||
| 7f0bdc8ddb | |||
| 39836f115b | |||
| 4a699fe6a7 | |||
| 7cc61a887c | |||
| 8f73f1ed3c | |||
| 67fb027d39 | |||
| c6b61ea0ea | |||
| 32841234a7 | |||
| 6f6520ed0f | |||
| b3860f3754 | |||
| 0958917317 | |||
| f42fa7e791 | |||
| 7022d1f3bf | |||
| b592c41f96 | |||
| 62188571d7 | |||
| 8cca3392f6 | |||
| 7082516664 | |||
| 77a1fc9e60 | |||
| e2b1dd6532 | |||
| 7b7fdb0a65 | |||
| 414279f644 | |||
| be91c8580d | |||
| befaa782f6 | |||
| 8e29dae36a | |||
| d2438d10a6 | |||
| b3752bb2c6 | |||
| 02e145b0de | |||
| 0fcf8777a1 | |||
| 26cae5543b | |||
| b0573dec77 | |||
| c5f4e762e5 | |||
| f4c08477d0 | |||
| 4342ee6d4a | |||
| 7b73561923 | |||
| 87c0cf233c | |||
| 145cd7894b | |||
| 6cf8a76c29 | |||
| 7ea09ebe4a | |||
| 1dcb16365b | |||
| 83b33d9d7a | |||
| 83879fa945 | |||
| c3903f2796 | |||
| 18564553ad | |||
| fe3e87a9d6 | |||
| 78c734d2e9 | |||
| 5afa16d454 | |||
| 57233416d9 | |||
| f56ce29210 | |||
| 149f59e9be | |||
| 761892d6b1 | |||
| 4b639d5f9a | |||
| da8ed77aae | |||
| 12f46909f7 | |||
| faf18b1996 | |||
| 218deddd00 | |||
| d6fe650c4c | |||
| 919189db1a | |||
| 951191d99a | |||
| f814a96eac | |||
| 2654f4bf0f | |||
| 218aa423c3 | |||
| 195f3a7550 | |||
| 1c9343ed8f | |||
| 5c7d9981f8 | |||
| 63bc17a6b4 | |||
| 629490c4ae | |||
| d59d62f96a | |||
| 8a460c2368 | |||
| 6f60183316 | |||
| a21c62519b | |||
| 1b8f6d1739 | |||
| e087bce61a | |||
| d2f24c3e87 | |||
| 5d606bba66 | |||
| 864fe459b7 | |||
| ea8362ed63 | |||
| 3a1508c2ab | |||
| f914391aaf | |||
| 8992438aa6 | |||
| 197d5ebb44 | |||
| 4039498eee | |||
| a686231a5b | |||
| 97cf4bff1f | |||
| cc8e8434ec | |||
| 11727833a2 | |||
| df38fde336 | |||
| 00233d610b | |||
| 1b94b3c4de | |||
| d1c9030fac | |||
| 70071eef41 | |||
| 65dd56f53a | |||
| c8fb4af369 | |||
| 9e1ba992e3 | |||
| bad09fed44 | |||
| 5bd146bb85 | |||
| 75703f273f | |||
| ca7b49d209 | |||
| 379322db0d | |||
| 30bce8a682 | |||
| f413f0ee5f | |||
| be6778a931 | |||
| 84637c6ebd | |||
| 2ed6e9ba2f | |||
| 2e3cb54d38 | |||
| 0efac7eae0 | |||
| dc56f05d03 | |||
| 4d2625278c | |||
| bff8a947a1 | |||
| 9cb7406ebd | |||
| 38681ca6ca | |||
| 916696c906 | |||
| 1b3c4c935e | |||
| bf6d1f6555 | |||
| 9faeac4c83 | |||
| a863cf3d72 | |||
| 66417e6742 | |||
| 6ad4d6dd62 | |||
| 59607f8dbf | |||
| 62380a48d5 | |||
| 4245372495 | |||
| 964a448470 | |||
| 2151f28d4c | |||
| 45a92bcf9c | |||
| 0a0441756b | |||
| c1de2ebbf9 | |||
| 060549656e | |||
| 557c2db955 | |||
| ed8d064a13 | |||
| 7dabf507a2 | |||
| d69afa47a1 | |||
| 275a8175aa | |||
| 40bebf4cbd | |||
| b8bc323c03 | |||
| c6d32ea2b0 | |||
| 2ace35ad6b | |||
| b5ea8d3a78 | |||
| f3a05f6ed0 | |||
| 43eae4929b | |||
| 6144962c24 | |||
| 544cc36006 | |||
| 1834e6688f | |||
| e1ee56aa43 | |||
| a1779719be | |||
| 0e464af627 | |||
| fd7f0c3b5a | |||
| ed210a4fb1 | |||
| c2a0504980 | |||
| b6708871d3 | |||
| 275ea6aacb | |||
| f97d413603 | |||
| 2c7ea0606b | |||
| d7a7100912 | |||
| 2c54b8d77e | |||
| b30f278e03 | |||
| b642030a34 | |||
| 725976d472 | |||
| 80d87e1bf1 | |||
| d01f527e71 | |||
| 3e68c82171 | |||
| 51bde23207 | |||
| 934ed37fdc | |||
| 4a965f5408 | |||
| 6343da33c3 | |||
| e6edee863f | |||
| 9a1d62438d | |||
| d2ba3039c7 | |||
| b6ad4c10bc | |||
| 93954d314e | |||
| 282f85f1dd | |||
| 223d37ffce | |||
| 3d20388ca0 | |||
| 198c9d934e | |||
| d43005d91e | |||
| 02264b4572 | |||
| add652f18e | |||
| 1b9146b9e7 | |||
| 5178819b51 | |||
| f57c25ec27 | |||
| 794429b68b | |||
| 983a04bb00 | |||
| adbef16b9d | |||
| 157ea49328 | |||
| 17386e7aae | |||
| cb19cd673f | |||
| 4f0a297cf3 | |||
| 6553e331cd | |||
| 21908aea6c | |||
| 7c40798ee0 | |||
| 8cdc635cad | |||
| 7f5ac072e6 | |||
| d69af72c7a | |||
| ece1e202de | |||
| 91f38a362d | |||
| 5a3cc314be | |||
| 3dfaafd177 | |||
| bdba61975b | |||
| 3b9023ec2b | |||
| 4dfc7958b6 | |||
| 2fad318726 | |||
| 480b0e64a6 | |||
| 6ec7b5d404 | |||
| 0781d78da8 | |||
| 513a256ec1 | |||
| 9372790666 | |||
| a6532b7881 | |||
| cea3582ed1 | |||
| 6bd22a3e9c | |||
| 7b93b99054 | |||
| c6eb1525b5 | |||
| a4b8ba0bb3 | |||
| 02216b15e5 | |||
| 42efdf1e0a | |||
| 465f9e634e | |||
| 7e92f0e5c8 | |||
| 859a0d8db2 | |||
| 71740cabb5 | |||
| 8f77680750 | |||
| 509e4b337d | |||
| 942ff0c9fd | |||
| 24c3dd1f1a | |||
| 4f58e9945b | |||
| 547ded9155 | |||
| 4f112e8379 | |||
| 4d63f8ed04 | |||
| 944d39c836 | |||
| 433977b918 | |||
| d9796e3bec | |||
| 0a7b9109f0 | |||
| 89bf9ff65b | |||
| 7f6e223c0c | |||
| c696e5238b | |||
| d303fd0c7c | |||
| e1ad2f8a21 | |||
| 7053cf0182 | |||
| e25158975b | |||
| 7e028a82fc | |||
| 17fe3e4dc1 | |||
| 4bd09c45a0 | |||
| 6a7a255081 | |||
| 6701fdd486 | |||
| ddce14b20b | |||
| f1317e824b | |||
| db285af0b5 | |||
| 0434bf5a48 | |||
| 78d9111646 | |||
| 0f28a89c52 | |||
| 92db6599d8 | |||
| 70fb5dcaa4 | |||
| a265574da1 | |||
| 9911766435 | |||
| fb08ef9a9b | |||
| 2fab06111c | |||
| 11e3b1ab53 | |||
| 3c78f7dbe1 | |||
| 999cebc304 | |||
| b2e154377a | |||
| d5c68139c0 | |||
| cbde77a5cd | |||
| 8120041ba7 | |||
| 68bc8edaae | |||
| 7ec339985a | |||
| 70c0abaef8 | |||
| d4dcac93b1 | |||
| 43889cfb31 | |||
| 9e4e14802d | |||
| 9bebb22746 | |||
| 3b06b0ffc1 | |||
| 1b24d55b24 | |||
| c8c6444f6a | |||
| 45a88f0517 | |||
| 53cb3ca79b | |||
| 68526284f1 | |||
| 68cebc7ff9 | |||
| 38286b74e3 | |||
| 86f56082f0 | |||
| e87bbfc535 | |||
| 758e12d6dd | |||
| bff461081a | |||
| 33d36395aa | |||
| e373508211 | |||
| 9051edad37 | |||
| 678b268008 | |||
| 0361bcf94f | |||
| b1f02d30c1 | |||
| 2af0e5b176 | |||
| c204812d9c | |||
| 3b7def880f | |||
| e5ec2f03c2 | |||
| a1b3e8055f | |||
| 1e503261f2 | |||
| 9107a3e569 | |||
| c6070519ed | |||
| 30ece1be70 | |||
| b66a1d30a0 | |||
| 51e1f56873 | |||
| 86304fd037 | |||
| 04387e78cc | |||
| 2bfc44b947 | |||
| 33941eb37b | |||
| 0a45559276 | |||
| 800441e0ed | |||
| 95164d08d5 | |||
| 98d955ef1f | |||
| 950dadc14e | |||
| 31d2f0135b | |||
| c02928f294 | |||
| 951fff45e6 | |||
| 4fdd817ff5 | |||
| acba31bd6d | |||
| b5eea01848 | |||
| 074e02ccf2 | |||
| 4b9bc67cb6 | |||
| 936ef4116b | |||
| 9883d6851a | |||
| 4c08e126ca | |||
| bc53f8fdec | |||
| 0b76d3d7bd | |||
| abaf71418e | |||
| c96a906b39 | |||
| da96765020 | |||
| d11946d86b | |||
| 3a4c72ac08 | |||
| 6d3f0f653b | |||
| 81d3534569 | |||
| c54922dba3 | |||
| 9da1f7b8d5 | |||
| a4ed3d97fc | |||
| 656694ee00 | |||
| c6b5936f8a | |||
| 03752ab60c | |||
| 7203542cfd | |||
| 4b36bbc122 | |||
| ecaf21ceb0 | |||
| 67fe4e1460 | |||
| a94503ad03 | |||
| ce6dd8688c | |||
| 1151bdc6db | |||
| ed223d1d76 | |||
| 650eee7705 | |||
| 4510eb6540 | |||
| 9a236f317d | |||
| 25c467d608 | |||
| c2daf0d74e | |||
| fa19616ad1 | |||
| 02cbd33284 | |||
| 941ae18d74 | |||
| 90f400abe1 | |||
| ff2d93d421 | |||
| 8d26bd9a17 | |||
| a9fa0484ff | |||
| d3d12ab62f | |||
| 1e29b1a31d | |||
| 9318bf5f2f | |||
| b1e3150a81 | |||
| 5d52053caa | |||
| c4e449fc45 |
+24
-68
@@ -1,71 +1,14 @@
|
||||
module.exports = {
|
||||
parser: "babel-eslint", // now needed for class properties
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
}
|
||||
},
|
||||
plugins: [
|
||||
"matrix-org",
|
||||
],
|
||||
extends: ["plugin:matrix-org/javascript"],
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
|
||||
// babel's transform-runtime converts references to ES6 globals such as
|
||||
// Promise and Map to core-js polyfills, so we can use ES6 globals.
|
||||
es6: true,
|
||||
jest: true,
|
||||
},
|
||||
extends: ["eslint:recommended", "google"],
|
||||
plugins: [
|
||||
"babel",
|
||||
"jest",
|
||||
],
|
||||
rules: {
|
||||
// rules we've always adhered to or now do
|
||||
"max-len": ["error", {
|
||||
code: 90,
|
||||
ignoreComments: true,
|
||||
}],
|
||||
curly: ["error", "multi-line"],
|
||||
"prefer-const": ["error"],
|
||||
"comma-dangle": ["error", {
|
||||
arrays: "always-multiline",
|
||||
objects: "always-multiline",
|
||||
imports: "always-multiline",
|
||||
exports: "always-multiline",
|
||||
functions: "always-multiline",
|
||||
}],
|
||||
|
||||
// loosen jsdoc requirements a little
|
||||
"require-jsdoc": ["error", {
|
||||
require: {
|
||||
FunctionDeclaration: false,
|
||||
}
|
||||
}],
|
||||
"valid-jsdoc": ["error", {
|
||||
requireParamDescription: false,
|
||||
requireReturn: false,
|
||||
requireReturnDescription: false,
|
||||
}],
|
||||
|
||||
// rules we do not want from eslint-recommended
|
||||
"no-console": ["off"],
|
||||
"no-constant-condition": ["off"],
|
||||
"no-empty": ["error", { "allowEmptyCatch": true }],
|
||||
|
||||
// rules we do not want from the google styleguide
|
||||
"object-curly-spacing": ["off"],
|
||||
"spaced-comment": ["off"],
|
||||
"guard-for-in": ["off"],
|
||||
|
||||
// in principle we prefer single quotes, but life is too short
|
||||
quotes: ["off"],
|
||||
|
||||
// rules we'd ideally like to adhere to, but the current
|
||||
// code does not (in most cases because it's still ES5)
|
||||
// we set these to warnings, and assert that the number
|
||||
// of warnings doesn't exceed a given threshold
|
||||
"no-var": ["warn"],
|
||||
"brace-style": ["warn", "1tbs", {"allowSingleLine": true}],
|
||||
"prefer-rest-params": ["warn"],
|
||||
"prefer-spread": ["warn"],
|
||||
"one-var": ["warn"],
|
||||
@@ -79,10 +22,23 @@ module.exports = {
|
||||
"asyncArrow": "always",
|
||||
}],
|
||||
"arrow-parens": "off",
|
||||
|
||||
// eslint's built in no-invalid-this rule breaks with class properties
|
||||
"no-invalid-this": "off",
|
||||
// so we replace it with a version that is class property aware
|
||||
"babel/no-invalid-this": "error",
|
||||
}
|
||||
}
|
||||
"prefer-promise-reject-errors": "off",
|
||||
"quotes": "off",
|
||||
"indent": "off",
|
||||
"no-constant-condition": "off",
|
||||
"no-async-promise-executor": "off",
|
||||
// We use a `logger` intermediary module
|
||||
"no-console": "error",
|
||||
},
|
||||
overrides: [{
|
||||
"files": ["src/**/*.ts"],
|
||||
"extends": ["plugin:matrix-org/typescript"],
|
||||
"rules": {
|
||||
// We're okay being explicit at the moment
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
// While we're converting to ts we make heavy use of this
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"quotes": "off",
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
@@ -12,7 +12,6 @@ lib-cov
|
||||
out
|
||||
/dist
|
||||
/lib
|
||||
/specbuild
|
||||
|
||||
# version file and tarball created by `npm pack` / `yarn pack`
|
||||
/git-revision.txt
|
||||
|
||||
+1137
File diff suppressed because it is too large
Load Diff
+3
-4
@@ -28,10 +28,9 @@ use GitHub's pull request workflow to review the contribution, and either ask
|
||||
you to make any refinements needed or merge it and make them ourselves. The
|
||||
changes will then land on master when we next do a release.
|
||||
|
||||
We use Travis for continuous integration, and all pull requests get
|
||||
automatically tested by Travis: if your change breaks the build, then the PR
|
||||
will show that there are failed checks, so please check back after a few
|
||||
minutes.
|
||||
We use continuous integration, and all pull requests get automatically tested:
|
||||
if your change breaks the build, then the PR will show that there are failed
|
||||
checks, so please check back after a few minutes.
|
||||
|
||||
Code style
|
||||
~~~~~~~~~~
|
||||
|
||||
@@ -30,7 +30,7 @@ This SDK targets Node 10 for compatibility, which translates to ES6. If you're u
|
||||
a bundler like webpack you'll likely have to transpile dependencies, including this
|
||||
SDK, to match your target browsers.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://yarnpkg.com/docs/install/)
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://classic.yarnpkg.com/en/docs/install)
|
||||
if you do not have it already.
|
||||
|
||||
``yarn add matrix-js-sdk``
|
||||
@@ -182,10 +182,8 @@ you can pass the result of the promise into it with something like:
|
||||
matrixClient.someMethod(arg1, arg2).nodeify(callback);
|
||||
```
|
||||
|
||||
The main thing to note is that it is an error to discard the result of a
|
||||
promise-returning function, as that will cause exceptions to go unobserved. If
|
||||
you have nothing better to do with the result, just call ``.done()`` on it. See
|
||||
http://documentup.com/kriskowal/q/#the-end for more information.
|
||||
The main thing to note is that it is problematic to discard the result of a
|
||||
promise-returning function, as that will cause exceptions to go unobserved.
|
||||
|
||||
Methods which return a promise show this in their documentation.
|
||||
|
||||
@@ -309,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.
|
||||
|
||||
|
||||
@@ -288,7 +288,7 @@ function printMemberList(room) {
|
||||
}
|
||||
|
||||
function printRoomInfo(room) {
|
||||
var eventDict = room.currentState.events;
|
||||
var eventMap = room.currentState.events;
|
||||
var eTypeHeader = " Event Type(state_key) ";
|
||||
var sendHeader = " Sender ";
|
||||
// pad content to 100
|
||||
@@ -300,14 +300,15 @@ function printRoomInfo(room) {
|
||||
var contentHeader = padSide + "Content" + padSide;
|
||||
print(eTypeHeader+sendHeader+contentHeader);
|
||||
print(new Array(100).join("-"));
|
||||
Object.keys(eventDict).forEach(function(eventType) {
|
||||
eventMap.keys().forEach(function(eventType) {
|
||||
if (eventType === "m.room.member") { return; } // use /members instead.
|
||||
Object.keys(eventDict[eventType]).forEach(function(stateKey) {
|
||||
var eventEventMap = eventMap.get(eventType);
|
||||
eventEventMap.keys().forEach(function(stateKey) {
|
||||
var typeAndKey = eventType + (
|
||||
stateKey.length > 0 ? "("+stateKey+")" : ""
|
||||
);
|
||||
var typeStr = fixWidth(typeAndKey, eTypeHeader.length);
|
||||
var event = eventDict[eventType][stateKey];
|
||||
var event = eventEventMap.get(stateKey);
|
||||
var sendStr = fixWidth(event.getSender(), sendHeader.length);
|
||||
var contentStr = fixWidth(
|
||||
JSON.stringify(event.getContent()), contentHeader.length
|
||||
|
||||
@@ -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>
|
||||
+8
-1
@@ -18,6 +18,13 @@
|
||||
"readme": "README.md",
|
||||
"recurse": true,
|
||||
"verbose": true,
|
||||
"template": "node_modules/better-docs"
|
||||
"template": "node_modules/docdash"
|
||||
},
|
||||
"docdash": {
|
||||
"static": true,
|
||||
"private": false,
|
||||
"search": true,
|
||||
"collapse": true,
|
||||
"typedefs": true
|
||||
}
|
||||
}
|
||||
|
||||
+62
-43
@@ -1,24 +1,25 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "5.1.0",
|
||||
"version": "11.0.0-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"scripts": {
|
||||
"prepare": "yarn build",
|
||||
"prepublishOnly": "yarn build",
|
||||
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"dist": "echo 'This is for the release script so it can make assets (browser bundle).' && yarn build",
|
||||
"clean": "rimraf lib dist",
|
||||
"build": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:compile-browser && yarn build:minify-browser && yarn build:types",
|
||||
"build": "yarn build:dev && yarn build:compile-browser && yarn build:minify-browser",
|
||||
"build:dev": "yarn clean && git rev-parse HEAD > git-revision.txt && yarn build:compile && yarn build:types",
|
||||
"build:types": "tsc --emitDeclarationOnly",
|
||||
"build:compile": "babel -d lib --verbose --extensions \".ts,.js\" src",
|
||||
"build:compile-browser": "mkdirp dist && browserify -d src/browser-index.js -p [ tsify -p ./tsconfig.json ] -t [ babelify --sourceMaps=inline --presets [ @babel/preset-env @babel/preset-typescript ] ] | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js",
|
||||
"build:minify-browser": "terser dist/browser-matrix.js --compress --mangle --source-map --output dist/browser-matrix.min.js",
|
||||
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
||||
"lint": "yarn lint:types && yarn lint:ts && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 93 src spec",
|
||||
"lint": "yarn lint:types && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 72 src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"lint:ts": "tslint --project ./tsconfig.json -t stylish",
|
||||
"test": "jest spec/ --coverage --testEnvironment node",
|
||||
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"coverage": "yarn test --coverage"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -28,13 +29,15 @@
|
||||
"matrix-org"
|
||||
],
|
||||
"main": "./lib/index.js",
|
||||
"typings": "./lib/index.d.ts",
|
||||
"browser": "./lib/browser-index.js",
|
||||
"matrix_src_main": "./src/index.ts",
|
||||
"matrix_src_browser": "./src/browser-index.js",
|
||||
"matrix_lib_main": "./lib/index.js",
|
||||
"matrix_lib_typings": "./lib/index.d.ts",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
"dist",
|
||||
"lib",
|
||||
"src",
|
||||
"git-revision.txt",
|
||||
@@ -46,49 +49,65 @@
|
||||
"release.sh"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.3",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"another-json": "^0.2.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"bs58": "^4.0.1",
|
||||
"content-type": "^1.0.2",
|
||||
"loglevel": "^1.6.4",
|
||||
"qs": "^6.5.2",
|
||||
"request": "^2.88.0",
|
||||
"unhomoglyph": "^1.0.2"
|
||||
"content-type": "^1.0.4",
|
||||
"loglevel": "^1.7.1",
|
||||
"qs": "^6.9.6",
|
||||
"request": "^2.88.2",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.7.5",
|
||||
"@babel/core": "^7.7.5",
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.7.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.7.4",
|
||||
"@babel/plugin-transform-runtime": "^7.8.3",
|
||||
"@babel/preset-env": "^7.7.6",
|
||||
"@babel/preset-typescript": "^7.7.4",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@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.10",
|
||||
"@babel/preset-env": "^7.12.11",
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/register": "^7.12.10",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "12",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
"@types/request": "^2.48.5",
|
||||
"@typescript-eslint/eslint-plugin": "^4.17.0",
|
||||
"@typescript-eslint/parser": "^4.17.0",
|
||||
"babel-jest": "^26.6.3",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^1.4.7",
|
||||
"browserify": "^16.5.0",
|
||||
"eslint": "^5.12.0",
|
||||
"eslint-config-google": "^0.7.1",
|
||||
"eslint-plugin-babel": "^5.3.0",
|
||||
"eslint-plugin-jest": "^23.0.4",
|
||||
"better-docs": "^2.3.2",
|
||||
"browserify": "^17.0.0",
|
||||
"docdash": "^1.2.0",
|
||||
"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",
|
||||
"jest": "^24.9.0",
|
||||
"jsdoc": "^3.5.5",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"jest": "^26.6.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.1.4.tgz",
|
||||
"rimraf": "^3.0.0",
|
||||
"terser": "^4.4.3",
|
||||
"tsify": "^4.0.1",
|
||||
"tslint": "^5.20.1",
|
||||
"typescript": "^3.7.3"
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^5.5.1",
|
||||
"tsify": "^5.0.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"
|
||||
}
|
||||
|
||||
+76
-26
@@ -10,7 +10,7 @@
|
||||
# npm; typically installed by Node.js
|
||||
# yarn; install via brew (macOS) or similar (https://yarnpkg.com/docs/install/)
|
||||
#
|
||||
# Note: this script is also used to release matrix-react-sdk and riot-web.
|
||||
# Note: this script is also used to release matrix-react-sdk and element-web.
|
||||
|
||||
set -e
|
||||
|
||||
@@ -38,6 +38,7 @@ $USAGE
|
||||
-c changelog_file: specify name of file containing changelog
|
||||
-x: skip updating the changelog
|
||||
-z: skip generating the jsdoc
|
||||
-n: skip publish to NPM
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -60,9 +61,10 @@ fi
|
||||
|
||||
skip_changelog=
|
||||
skip_jsdoc=
|
||||
skip_npm=
|
||||
changelog_file="CHANGELOG.md"
|
||||
expected_npm_user="matrixdotorg"
|
||||
while getopts hc:u:xz f; do
|
||||
while getopts hc:u:xzn f; do
|
||||
case $f in
|
||||
h)
|
||||
help
|
||||
@@ -77,6 +79,9 @@ while getopts hc:u:xz f; do
|
||||
z)
|
||||
skip_jsdoc=1
|
||||
;;
|
||||
n)
|
||||
skip_npm=1
|
||||
;;
|
||||
u)
|
||||
expected_npm_user="$OPTARG"
|
||||
;;
|
||||
@@ -89,6 +94,14 @@ if [ $# -ne 1 ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# We use Git branch / commit dependencies for some packages, and Yarn seems
|
||||
# to have a hard time getting that right. See also
|
||||
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
|
||||
# global cache here to ensure we get the right thing.
|
||||
yarn cache clean
|
||||
# Ensure all dependencies are updated
|
||||
yarn install --ignore-scripts
|
||||
|
||||
if [ -z "$skip_changelog" ]; then
|
||||
# update_changelog doesn't have a --version flag
|
||||
update_changelog -h > /dev/null || (echo "github-changelog-generator is required: please install it"; exit)
|
||||
@@ -96,10 +109,12 @@ fi
|
||||
|
||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
||||
# defined options and semantics than `yarn` for writing to the registry.
|
||||
actual_npm_user=`npm whoami`;
|
||||
if [ $expected_npm_user != $actual_npm_user ]; then
|
||||
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
|
||||
exit 1
|
||||
if [ -z "$skip_npm" ]; then
|
||||
actual_npm_user=`npm whoami`;
|
||||
if [ $expected_npm_user != $actual_npm_user ]; then
|
||||
echo "you need to be logged into npm as $expected_npm_user, but you are logged in as $actual_npm_user" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ignore leading v on release
|
||||
@@ -163,6 +178,19 @@ echo "yarn version"
|
||||
# manually commit the result.
|
||||
yarn version --no-git-tag-version --new-version "$release"
|
||||
|
||||
# For the published and dist versions of the package, we copy the
|
||||
# `matrix_lib_main` and `matrix_lib_typings` fields to `main` and `typings` (if
|
||||
# they exist). This small bit of gymnastics allows us to use the TypeScript
|
||||
# source directly for development without needing to build before linting or
|
||||
# testing.
|
||||
for i in main typings
|
||||
do
|
||||
lib_value=$(jq -r ".matrix_lib_$i" package.json)
|
||||
if [ "$lib_value" != "null" ]; then
|
||||
jq ".$i = .matrix_lib_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
fi
|
||||
done
|
||||
|
||||
# commit yarn.lock if it exists, is versioned, and is modified
|
||||
if [[ -f yarn.lock && `git status --porcelain yarn.lock | grep '^ M'` ]];
|
||||
then
|
||||
@@ -197,11 +225,6 @@ if [ $dodist -eq 0 ]; then
|
||||
pushd "$builddir"
|
||||
git clone "$projdir" .
|
||||
git checkout "$rel_branch"
|
||||
# We use Git branch / commit dependencies for some packages, and Yarn seems
|
||||
# to have a hard time getting that right. See also
|
||||
# https://github.com/yarnpkg/yarn/issues/4734. As a workaround, we clean the
|
||||
# global cache here to ensure we get the right thing.
|
||||
yarn cache clean
|
||||
yarn install
|
||||
# We haven't tagged yet, so tell the dist script what version
|
||||
# it's building
|
||||
@@ -298,11 +321,13 @@ rm "${latest_changes}"
|
||||
# defined options and semantics than `yarn` for writing to the registry.
|
||||
# Tag both releases and prereleases as `next` so the last stable release remains
|
||||
# the default.
|
||||
npm publish --tag next
|
||||
if [ $prerelease -eq 0 ]; then
|
||||
# For a release, also add the default `latest` tag.
|
||||
package=$(cat package.json | jq -er .name)
|
||||
npm dist-tag add "$package@$release" latest
|
||||
if [ -z "$skip_npm" ]; then
|
||||
npm publish --tag next
|
||||
if [ $prerelease -eq 0 ]; then
|
||||
# For a release, also add the default `latest` tag.
|
||||
package=$(cat package.json | jq -er .name)
|
||||
npm dist-tag add "$package@$release" latest
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
@@ -318,6 +343,7 @@ if [ -z "$skip_jsdoc" ]; then
|
||||
$release index.html
|
||||
git add "$release"
|
||||
git commit --no-verify -m "Add jsdoc for $release" index.html "$release"
|
||||
git push origin gh-pages
|
||||
fi
|
||||
|
||||
# if it is a pre-release, leave it on the release branch for now.
|
||||
@@ -330,16 +356,40 @@ fi
|
||||
echo "updating master branch"
|
||||
git checkout master
|
||||
git pull
|
||||
git merge "$rel_branch"
|
||||
git merge "$rel_branch" --no-edit
|
||||
|
||||
# push master and docs (if generated) to github
|
||||
# push master to github
|
||||
git push origin master
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
git push origin gh-pages
|
||||
fi
|
||||
|
||||
# finally, merge master back onto develop
|
||||
git checkout develop
|
||||
git pull
|
||||
git merge master
|
||||
git push origin develop
|
||||
# finally, merge master back onto develop (if it exists)
|
||||
if [ $(git branch -lr | grep origin/develop -c) -ge 1 ]; then
|
||||
git checkout develop
|
||||
git pull
|
||||
git merge master --no-edit
|
||||
|
||||
# When merging to develop, we need revert the `main` and `typings` fields if
|
||||
# we adjusted them previously.
|
||||
for i in main typings
|
||||
do
|
||||
# If a `lib` prefixed value is present, it means we adjusted the field
|
||||
# earlier at publish time, so we should revert it now.
|
||||
if [ "$(jq -r ".matrix_lib_$i" package.json)" != "null" ]; then
|
||||
# If there's a `src` prefixed value, use that, otherwise delete.
|
||||
# This is used to delete the `typings` field and reset `main` back
|
||||
# to the TypeScript source.
|
||||
src_value=$(jq -r ".matrix_src_$i" package.json)
|
||||
if [ "$src_value" != "null" ]; then
|
||||
jq ".$i = .matrix_src_$i" package.json > package.json.new && mv package.json.new package.json
|
||||
else
|
||||
jq "del(.$i)" package.json > package.json.new && mv package.json.new package.json
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$(git ls-files --modified package.json)" ]; then
|
||||
echo "Committing develop package.json"
|
||||
git commit package.json -m "Resetting package fields for development"
|
||||
fi
|
||||
|
||||
git push origin develop
|
||||
fi
|
||||
|
||||
+8
-1
@@ -69,6 +69,9 @@ export function TestClient(
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = {};
|
||||
this._callEventHandler = {
|
||||
calls: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
TestClient.prototype.toString = function() {
|
||||
@@ -185,7 +188,7 @@ TestClient.prototype.expectKeyQuery = function(response) {
|
||||
200, (path, content) => {
|
||||
Object.keys(response.device_keys).forEach((userId) => {
|
||||
expect(content.device_keys[userId]).toEqual(
|
||||
{},
|
||||
[],
|
||||
"Expected key query for " + userId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
@@ -230,3 +233,7 @@ TestClient.prototype.flushSync = function() {
|
||||
logger.log(`${this}: flushSync completed`);
|
||||
});
|
||||
};
|
||||
|
||||
TestClient.prototype.isFallbackICEServerAllowed = function() {
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// stub for browser-matrix browserify tests
|
||||
global.XMLHttpRequest = jest.fn();
|
||||
|
||||
afterAll(() => {
|
||||
// clean up XMLHttpRequest mock
|
||||
global.XMLHttpRequest = undefined;
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// 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 MockHttpBackend from "matrix-mock-request";
|
||||
import {LocalStorageCryptoStore} from "../../src/crypto/store/localStorage-crypto-store";
|
||||
import * as utils from "../test-utils";
|
||||
|
||||
const USER_ID = "@user:test.server";
|
||||
const DEVICE_ID = "device_id";
|
||||
const ACCESS_TOKEN = "access_token";
|
||||
const ROOM_ID = "!room_id:server.test";
|
||||
|
||||
/* global matrixcs */
|
||||
|
||||
describe("Browserify Test", function() {
|
||||
let client;
|
||||
let httpBackend;
|
||||
|
||||
async function createTestClient() {
|
||||
const sessionStoreBackend = new MockStorageApi();
|
||||
const sessionStore = new WebStorageSessionStore(sessionStoreBackend);
|
||||
const httpBackend = new MockHttpBackend();
|
||||
|
||||
const options = {
|
||||
baseUrl: "http://" + USER_ID + ".test.server",
|
||||
userId: USER_ID,
|
||||
accessToken: ACCESS_TOKEN,
|
||||
deviceId: DEVICE_ID,
|
||||
sessionStore: sessionStore,
|
||||
request: httpBackend.requestFn,
|
||||
cryptoStore: new LocalStorageCryptoStore(sessionStoreBackend),
|
||||
};
|
||||
|
||||
const client = matrixcs.createClient(options);
|
||||
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
|
||||
return { client, httpBackend };
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
({client, httpBackend} = await createTestClient());
|
||||
await client.startClient();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
client.stopClient();
|
||||
await httpBackend.stop();
|
||||
});
|
||||
|
||||
it("Sync", async function() {
|
||||
const event = utils.mkMembership({
|
||||
room: ROOM_ID,
|
||||
mship: "join",
|
||||
user: "@other_user:server.test",
|
||||
name: "Displayname",
|
||||
});
|
||||
|
||||
const syncData = {
|
||||
next_batch: "batch1",
|
||||
rooms: {
|
||||
join: {},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[ROOM_ID] = {
|
||||
timeline: {
|
||||
events: [
|
||||
event,
|
||||
],
|
||||
limited: false,
|
||||
},
|
||||
};
|
||||
|
||||
httpBackend.when("GET", "/sync").respond(200, syncData);
|
||||
await Promise.race([
|
||||
Promise.all([
|
||||
httpBackend.flushAllExpected(),
|
||||
]),
|
||||
new Promise((_, reject) => {
|
||||
client.once("sync.unexpectedError", reject);
|
||||
}),
|
||||
]);
|
||||
}, 20000); // additional timeout as this test can take quite a while
|
||||
});
|
||||
@@ -140,7 +140,7 @@ describe("DeviceList management:", function() {
|
||||
|
||||
it("We should not get confused by out-of-order device query responses",
|
||||
() => {
|
||||
// https://github.com/vector-im/riot-web/issues/3126
|
||||
// https://github.com/vector-im/element-web/issues/3126
|
||||
aliceTestClient.expectKeyQuery({device_keys: {'@alice:localhost': {}}});
|
||||
return aliceTestClient.start().then(() => {
|
||||
aliceTestClient.httpBackend.when('GET', '/sync').respond(
|
||||
@@ -271,7 +271,7 @@ describe("DeviceList management:", function() {
|
||||
});
|
||||
}).timeout(3000);
|
||||
|
||||
// https://github.com/vector-im/riot-web/issues/4983
|
||||
// https://github.com/vector-im/element-web/issues/4983
|
||||
describe("Alice should know she has stale device lists", () => {
|
||||
beforeEach(async function() {
|
||||
await aliceTestClient.start();
|
||||
|
||||
@@ -70,7 +70,7 @@ function expectAliQueryKeys() {
|
||||
aliTestClient.httpBackend.when("POST", "/keys/query")
|
||||
.respond(200, function(path, content) {
|
||||
expect(content.device_keys[bobUserId]).toEqual(
|
||||
{},
|
||||
[],
|
||||
"Expected Alice to key query for " + bobUserId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
@@ -98,7 +98,7 @@ function expectBobQueryKeys() {
|
||||
"POST", "/keys/query",
|
||||
).respond(200, function(path, content) {
|
||||
expect(content.device_keys[aliUserId]).toEqual(
|
||||
{},
|
||||
[],
|
||||
"Expected Bob to key query for " + aliUserId + ", got " +
|
||||
Object.keys(content.device_keys),
|
||||
);
|
||||
|
||||
@@ -347,8 +347,8 @@ describe("MatrixClient", function() {
|
||||
|
||||
httpBackend.when("POST", "/keys/query").check(function(req) {
|
||||
expect(req.data).toEqual({device_keys: {
|
||||
'boris': {},
|
||||
'chaz': {},
|
||||
'boris': [],
|
||||
'chaz': [],
|
||||
}});
|
||||
}).respond(200, {
|
||||
device_keys: {
|
||||
|
||||
@@ -3,6 +3,7 @@ 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";
|
||||
|
||||
describe("MatrixClient opts", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
@@ -132,10 +133,10 @@ describe("MatrixClient opts", function() {
|
||||
});
|
||||
|
||||
it("shouldn't retry sending events", function(done) {
|
||||
httpBackend.when("PUT", "/txn1").fail(500, {
|
||||
httpBackend.when("PUT", "/txn1").fail(500, new MatrixError({
|
||||
errcode: "M_SOMETHING",
|
||||
error: "Ruh roh",
|
||||
});
|
||||
}));
|
||||
client.sendTextMessage("!foo:bar", "a body", "txn1").then(function(res) {
|
||||
expect(false).toBe(true, "sendTextMessage resolved but shouldn't");
|
||||
}, function(err) {
|
||||
|
||||
@@ -90,7 +90,7 @@ describe("MatrixClient retrying", function() {
|
||||
// wait for the localecho of ev1 to be updated
|
||||
const p3 = new Promise((resolve, reject) => {
|
||||
room.on("Room.localEchoUpdated", (ev0) => {
|
||||
if(ev0 === ev1) {
|
||||
if (ev0 === ev1) {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -346,7 +346,7 @@ describe("megolm", function() {
|
||||
});
|
||||
|
||||
it("Alice receives a megolm message before the session keys", function() {
|
||||
// https://github.com/vector-im/riot-web/issues/2273
|
||||
// https://github.com/vector-im/element-web/issues/2273
|
||||
let roomKeyEncrypted;
|
||||
|
||||
return aliceTestClient.start().then(() => {
|
||||
@@ -726,7 +726,7 @@ describe("megolm", function() {
|
||||
});
|
||||
});
|
||||
|
||||
// https://github.com/vector-im/riot-web/issues/2676
|
||||
// https://github.com/vector-im/element-web/issues/2676
|
||||
it("Alice should send to her other devices", function() {
|
||||
// for this test, we make the testOlmAccount be another of Alice's devices.
|
||||
// it ought to get included in messages Alice sends.
|
||||
@@ -971,4 +971,60 @@ describe("megolm", function() {
|
||||
expect(event.getContent().body).toEqual('42');
|
||||
});
|
||||
});
|
||||
|
||||
it("Alice receives an untrusted megolm key, only to receive the trusted one shortly after", function() {
|
||||
const testClient = new TestClient(
|
||||
"@alice:localhost", "device2", "access_token2",
|
||||
);
|
||||
const groupSession = new Olm.OutboundGroupSession();
|
||||
groupSession.create();
|
||||
const inboundGroupSession = new Olm.InboundGroupSession();
|
||||
inboundGroupSession.create(groupSession.session_key());
|
||||
const rawEvent = encryptMegolmEvent({
|
||||
senderKey: testSenderKey,
|
||||
groupSession: groupSession,
|
||||
room_id: ROOM_ID,
|
||||
});
|
||||
return testClient.client.initCrypto().then(() => {
|
||||
const keys = [{
|
||||
room_id: ROOM_ID,
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
session_id: groupSession.session_id(),
|
||||
session_key: inboundGroupSession.export_session(0),
|
||||
sender_key: testSenderKey,
|
||||
}];
|
||||
return testClient.client.importRoomKeys(keys, { untrusted: true });
|
||||
}).then(() => {
|
||||
const event = testUtils.mkEvent({
|
||||
event: true,
|
||||
...rawEvent,
|
||||
room: ROOM_ID,
|
||||
});
|
||||
return event.attemptDecryption(testClient.client._crypto, true).then(() => {
|
||||
expect(event.isKeySourceUntrusted()).toBeTruthy();
|
||||
});
|
||||
}).then(() => {
|
||||
const event = testUtils.mkEvent({
|
||||
type: 'm.room_key',
|
||||
content: {
|
||||
room_id: ROOM_ID,
|
||||
algorithm: 'm.megolm.v1.aes-sha2',
|
||||
session_id: groupSession.session_id(),
|
||||
session_key: groupSession.session_key(),
|
||||
},
|
||||
event: true,
|
||||
});
|
||||
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(() => {
|
||||
expect(event.isKeySourceUntrusted()).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {logger} from '../src/logger';
|
||||
import * as utils from "../src/utils";
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
@@ -24,3 +25,11 @@ try {
|
||||
} catch (e) {
|
||||
logger.warn("unable to run crypto tests: libolm not available");
|
||||
}
|
||||
|
||||
// also try to set node crypto
|
||||
try {
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
||||
logger.log('nodejs was compiled without crypto support: some tests will fail');
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import {getHttpUriForMxc, getIdenticonUri} from "../../src/content-repo";
|
||||
import {getHttpUriForMxc} from "../../src/content-repo";
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
const baseUrl = "https://my.home.server";
|
||||
@@ -56,31 +56,4 @@ describe("ContentRepo", function() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIdenticonUri", function() {
|
||||
it("should do nothing for null input", function() {
|
||||
expect(getIdenticonUri(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it("should set w/h by default to 96", function() {
|
||||
expect(getIdenticonUri(baseUrl, "foobar")).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
|
||||
"?width=96&height=96",
|
||||
);
|
||||
});
|
||||
|
||||
it("should be able to set custom w/h", function() {
|
||||
expect(getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
|
||||
"?width=32&height=64",
|
||||
);
|
||||
});
|
||||
|
||||
it("should URL encode the identicon string", function() {
|
||||
expect(getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foo%23bar" +
|
||||
"?width=32&height=64",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ 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";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -26,6 +27,66 @@ describe("Crypto", function() {
|
||||
expect(Crypto.getOlmVersion()[0]).toEqual(3);
|
||||
});
|
||||
|
||||
describe("encrypted events", function() {
|
||||
it("provides encryption information", async function() {
|
||||
const client = (new TestClient(
|
||||
"@alice:example.com", "deviceid",
|
||||
)).client;
|
||||
await client.initCrypto();
|
||||
|
||||
// unencrypted event
|
||||
const event = {
|
||||
getId: () => "$event_id",
|
||||
getSenderKey: () => null,
|
||||
getWireContent: () => {return {};},
|
||||
};
|
||||
|
||||
let encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeFalsy();
|
||||
|
||||
// unknown sender (e.g. deleted device), forwarded megolm key (untrusted)
|
||||
event.getSenderKey = () => 'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
event.getWireContent = () => {return {algorithm: olmlib.MEGOLM_ALGORITHM};};
|
||||
event.getForwardingCurve25519KeyChain = () => ["not empty"];
|
||||
event.isKeySourceUntrusted = () => false;
|
||||
event.getClaimedEd25519Key =
|
||||
() => 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
expect(encryptionInfo.authenticated).toBeFalsy();
|
||||
expect(encryptionInfo.sender).toBeFalsy();
|
||||
|
||||
// known sender, megolm key from backup
|
||||
event.getForwardingCurve25519KeyChain = () => [];
|
||||
event.isKeySourceUntrusted = () => true;
|
||||
const device = new DeviceInfo("FLIBBLE");
|
||||
device.keys["curve25519:FLIBBLE"] =
|
||||
'YmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmJiYmI';
|
||||
device.keys["ed25519:FLIBBLE"] =
|
||||
'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
client._crypto._deviceList.getDeviceByIdentityKey = () => device;
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
expect(encryptionInfo.authenticated).toBeFalsy();
|
||||
expect(encryptionInfo.sender).toBeTruthy();
|
||||
expect(encryptionInfo.mismatchedSender).toBeFalsy();
|
||||
|
||||
// known sender, trusted megolm key, but bad ed25519key
|
||||
event.isKeySourceUntrusted = () => false;
|
||||
device.keys["ed25519:FLIBBLE"] =
|
||||
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB';
|
||||
|
||||
encryptionInfo = client.getEventEncryptionInfo(event);
|
||||
expect(encryptionInfo.encrypted).toBeTruthy();
|
||||
expect(encryptionInfo.authenticated).toBeTruthy();
|
||||
expect(encryptionInfo.sender).toBeTruthy();
|
||||
expect(encryptionInfo.mismatchedSender).toBeTruthy();
|
||||
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Session management', function() {
|
||||
const otkResponse = {
|
||||
@@ -313,6 +374,10 @@ describe("Crypto", function() {
|
||||
// make a room key request, and record the transaction ID for the
|
||||
// sendToDevice call
|
||||
await aliceClient.cancelAndResendEventRoomKeyRequest(event);
|
||||
// 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();
|
||||
jest.runAllTimers();
|
||||
await Promise.resolve();
|
||||
expect(aliceClient.sendToDevice).toBeCalledTimes(1);
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import '../../olm-loader';
|
||||
import {
|
||||
CrossSigningInfo,
|
||||
createCryptoStoreCacheCallbacks,
|
||||
} from '../../../src/crypto/CrossSigning';
|
||||
import {
|
||||
IndexedDBCryptoStore,
|
||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import {MemoryCryptoStore} from '../../../src/crypto/store/memory-crypto-store';
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
import {OlmDevice} from "../../../src/crypto/OlmDevice";
|
||||
import {logger} from '../../../src/logger';
|
||||
|
||||
const userId = "@alice:example.com";
|
||||
|
||||
// Private key for tests only
|
||||
const testKey = new Uint8Array([
|
||||
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
|
||||
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
|
||||
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
|
||||
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
|
||||
]);
|
||||
|
||||
const types = [
|
||||
{ type: "master", shouldCache: true },
|
||||
{ type: "self_signing", shouldCache: true },
|
||||
{ type: "user_signing", shouldCache: true },
|
||||
{ type: "invalid", shouldCache: false },
|
||||
];
|
||||
|
||||
const badKey = Uint8Array.from(testKey);
|
||||
badKey[0] ^= 1;
|
||||
|
||||
const masterKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
|
||||
|
||||
describe("CrossSigningInfo.getCrossSigningKey", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
it("should throw if no callback is provided", async () => {
|
||||
const info = new CrossSigningInfo(userId);
|
||||
await expect(info.getCrossSigningKey("master")).rejects.toThrow();
|
||||
});
|
||||
|
||||
it.each(types)("should throw if the callback returns falsey",
|
||||
async ({type, shouldCache}) => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: () => false,
|
||||
});
|
||||
await expect(info.getCrossSigningKey(type)).rejects.toThrow("falsey");
|
||||
});
|
||||
|
||||
it("should throw if the expected key doesn't come back", async () => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: () => masterKeyPub,
|
||||
});
|
||||
await expect(info.getCrossSigningKey("master", "")).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should return a key from its callback", async () => {
|
||||
const info = new CrossSigningInfo(userId, {
|
||||
getCrossSigningKey: () => testKey,
|
||||
});
|
||||
const [pubKey, pkSigning] = await info.getCrossSigningKey("master", masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
// check that the pkSigning object corresponds to the pubKey
|
||||
const signature = pkSigning.sign("message");
|
||||
const util = new global.Olm.Utility();
|
||||
try {
|
||||
util.ed25519_verify(pubKey, "message", signature);
|
||||
} finally {
|
||||
util.free();
|
||||
}
|
||||
});
|
||||
|
||||
it.each(types)("should request a key from the cache callback (if set)" +
|
||||
" and does not call app if one is found" +
|
||||
" %o",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockImplementation(() => {
|
||||
if (shouldCache) {
|
||||
return Promise.reject(new Error("Regular callback called"));
|
||||
} else {
|
||||
return Promise.resolve(testKey);
|
||||
}
|
||||
});
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache },
|
||||
);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
expect(getCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(types)("should store a key with the cache callback (if set)",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ storeCrossSigningKeyCache },
|
||||
);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(shouldCache ? 1 : 0);
|
||||
if (shouldCache) {
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][0]).toBe(type);
|
||||
expect(storeCrossSigningKeyCache.mock.calls[0][1]).toBe(testKey);
|
||||
}
|
||||
});
|
||||
|
||||
it.each(types)("does not store a bad key to the cache",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(badKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ storeCrossSigningKeyCache },
|
||||
);
|
||||
await expect(info.getCrossSigningKey(type, masterKeyPub)).rejects.toThrow();
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toEqual(0);
|
||||
});
|
||||
|
||||
it.each(types)("does not store a value to the cache if it came from the cache",
|
||||
async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockImplementation(() => {
|
||||
if (shouldCache) {
|
||||
return Promise.reject(new Error("Regular callback called"));
|
||||
} else {
|
||||
return Promise.resolve(testKey);
|
||||
}
|
||||
});
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(testKey);
|
||||
const storeCrossSigningKeyCache = jest.fn().mockRejectedValue(
|
||||
new Error("Tried to store a value from cache"),
|
||||
);
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
|
||||
);
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(0);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
});
|
||||
|
||||
it.each(types)("requests a key from the cache callback (if set) and then calls app" +
|
||||
" if one is not found", async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(undefined);
|
||||
const storeCrossSigningKeyCache = jest.fn();
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
|
||||
);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(getCrossSigningKey.mock.calls.length).toBe(1);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
|
||||
/* Also expect that the cache gets updated */
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
});
|
||||
|
||||
it.each(types)("requests a key from the cache callback (if set) and then" +
|
||||
" calls app if that key doesn't match", async ({ type, shouldCache }) => {
|
||||
const getCrossSigningKey = jest.fn().mockResolvedValue(testKey);
|
||||
const getCrossSigningKeyCache = jest.fn().mockResolvedValue(badKey);
|
||||
const storeCrossSigningKeyCache = jest.fn();
|
||||
const info = new CrossSigningInfo(
|
||||
userId,
|
||||
{ getCrossSigningKey },
|
||||
{ getCrossSigningKeyCache, storeCrossSigningKeyCache },
|
||||
);
|
||||
const [pubKey] = await info.getCrossSigningKey(type, masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(getCrossSigningKey.mock.calls.length).toBe(1);
|
||||
expect(getCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
|
||||
/* Also expect that the cache gets updated */
|
||||
expect(storeCrossSigningKeyCache.mock.calls.length).toBe(shouldCache ? 1 : 0);
|
||||
});
|
||||
});
|
||||
|
||||
/*
|
||||
* Note that MemoryStore is weird. It's only used for testing - as far as I can tell,
|
||||
* it's not possible to get one in normal execution unless you hack as we do here.
|
||||
*/
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore",
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore",
|
||||
() => new IndexedDBCryptoStore(undefined, "tests")],
|
||||
["MemoryCryptoStore", () => {
|
||||
const store = new IndexedDBCryptoStore(undefined, "tests");
|
||||
store._backend = new MemoryCryptoStore();
|
||||
store._backendPromise = Promise.resolve(store._backend);
|
||||
return store;
|
||||
}],
|
||||
])("CrossSigning > createCryptoStoreCacheCallbacks [%s]", function(name, dbFactory) {
|
||||
let store;
|
||||
|
||||
beforeAll(() => {
|
||||
store = dbFactory();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await store.deleteAllData();
|
||||
});
|
||||
|
||||
it("should cache data to the store and retrieve it", async () => {
|
||||
await store.startup();
|
||||
const olmDevice = new OlmDevice(store);
|
||||
const { getCrossSigningKeyCache, storeCrossSigningKeyCache } =
|
||||
createCryptoStoreCacheCallbacks(store, olmDevice);
|
||||
await storeCrossSigningKeyCache("self_signing", testKey);
|
||||
|
||||
// If we've not saved anything, don't expect anything
|
||||
// Definitely don't accidentally return the wrong key for the type
|
||||
const nokey = await getCrossSigningKeyCache("self", "");
|
||||
expect(nokey).toBeNull();
|
||||
|
||||
const key = await getCrossSigningKeyCache("self_signing", "");
|
||||
expect(new Uint8Array(key)).toEqual(testKey);
|
||||
});
|
||||
});
|
||||
@@ -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,14 +99,16 @@ describe('DeviceList', function() {
|
||||
}
|
||||
});
|
||||
|
||||
function createTestDeviceList() {
|
||||
function createTestDeviceList(keyDownloadChunkSize = 250) {
|
||||
const baseApis = {
|
||||
downloadKeysForUsers: downloadSpy,
|
||||
getUserId: () => '@test1:sw1v.org',
|
||||
deviceId: 'HGKAWHRVJQ',
|
||||
};
|
||||
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;
|
||||
}
|
||||
@@ -148,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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -297,6 +297,10 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
}));
|
||||
|
||||
mockCrypto.checkDeviceTrust.mockReturnValue({
|
||||
isVerified: () => false,
|
||||
});
|
||||
|
||||
const megolmEncryption = new MegolmEncryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
@@ -320,14 +324,14 @@ describe("MegolmDecryption", function() {
|
||||
|
||||
// this should have claimed a key for alice as it's starting a new session
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
|
||||
);
|
||||
expect(mockCrypto.downloadKeys).toHaveBeenCalledWith(
|
||||
['@alice:home.server'], false,
|
||||
);
|
||||
expect(mockBaseApis.sendToDevice).toHaveBeenCalled();
|
||||
expect(mockBaseApis.claimOneTimeKeys).toHaveBeenCalledWith(
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519',
|
||||
[['@alice:home.server', 'aliceDevice']], 'signed_curve25519', 2000,
|
||||
);
|
||||
|
||||
mockBaseApis.claimOneTimeKeys.mockReset();
|
||||
@@ -516,21 +520,22 @@ describe("MegolmDecryption", function() {
|
||||
};
|
||||
};
|
||||
|
||||
let run = false;
|
||||
aliceClient.sendToDevice = async (msgtype, contentMap) => {
|
||||
run = true;
|
||||
expect(msgtype).toBe("org.matrix.room_key.withheld");
|
||||
expect(contentMap).toStrictEqual({
|
||||
'@bob:example.com': {
|
||||
bobdevice: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
code: 'm.no_olm',
|
||||
reason: 'Unable to establish a secure channel.',
|
||||
sender_key: aliceDevice.deviceCurve25519Key,
|
||||
const sendPromise = new Promise((resolve, reject) => {
|
||||
aliceClient.sendToDevice = async (msgtype, contentMap) => {
|
||||
expect(msgtype).toBe("org.matrix.room_key.withheld");
|
||||
expect(contentMap).toStrictEqual({
|
||||
'@bob:example.com': {
|
||||
bobdevice: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
code: 'm.no_olm',
|
||||
reason: 'Unable to establish a secure channel.',
|
||||
sender_key: aliceDevice.deviceCurve25519Key,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
});
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
@@ -540,8 +545,7 @@ describe("MegolmDecryption", function() {
|
||||
content: {},
|
||||
});
|
||||
await aliceClient._crypto.encryptEvent(event, aliceRoom);
|
||||
|
||||
expect(run).toBe(true);
|
||||
await sendPromise;
|
||||
});
|
||||
|
||||
it("throws an error describing why it doesn't have a key", async function() {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -27,6 +27,7 @@ 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";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -332,7 +333,7 @@ describe("MegolmBackup", function() {
|
||||
client.on("crossSigning.getKey", function(e) {
|
||||
e.done(privateKeys[e.type]);
|
||||
});
|
||||
await client.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(client);
|
||||
let numCalls = 0;
|
||||
await new Promise((resolve, reject) => {
|
||||
client._http.authedRequest = function(
|
||||
@@ -517,6 +518,7 @@ describe("MegolmBackup", function() {
|
||||
return megolmDecryption.decryptEvent(ENCRYPTED_EVENT);
|
||||
}).then((res) => {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
expect(res.untrusted).toBeTruthy(); // keys from backup are untrusted
|
||||
});
|
||||
});
|
||||
|
||||
@@ -541,5 +543,31 @@ describe("MegolmBackup", function() {
|
||||
expect(res.clearEvent.content).toEqual('testytest');
|
||||
});
|
||||
});
|
||||
|
||||
it('has working cache functions', async function() {
|
||||
const key = Uint8Array.from([1, 2, 3, 4, 5, 6, 7, 8]);
|
||||
await client._crypto.storeSessionBackupPrivateKey(key);
|
||||
const result = await client._crypto.getSessionBackupPrivateKey();
|
||||
expect(new Uint8Array(result)).toEqual(key);
|
||||
});
|
||||
|
||||
it('caches session backup keys as it encounters them', async function() {
|
||||
const cachedNull = await client._crypto.getSessionBackupPrivateKey();
|
||||
expect(cachedNull).toBeNull();
|
||||
client._http.authedRequest = function() {
|
||||
return Promise.resolve(KEY_BACKUP_DATA);
|
||||
};
|
||||
await new Promise((resolve) => {
|
||||
client.restoreKeyBackupWithRecoveryKey(
|
||||
"EsTc LW2K PGiF wKEA 3As5 g5c4 BXwk qeeJ ZJV8 Q9fu gUMN UE4d",
|
||||
ROOM_ID,
|
||||
SESSION_ID,
|
||||
BACKUP_INFO,
|
||||
{ cacheCompleteCallback: resolve },
|
||||
);
|
||||
});
|
||||
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
|
||||
expect(cachedKey).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,6 +20,9 @@ import anotherjson from 'another-json';
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
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';
|
||||
|
||||
async function makeTestClient(userInfo, options, keys) {
|
||||
if (!keys) keys = {};
|
||||
@@ -47,7 +50,7 @@ async function makeTestClient(userInfo, options, keys) {
|
||||
|
||||
describe("Cross Signing", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm backup unit tests: libolm not present');
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -66,11 +69,66 @@ describe("Cross Signing", function() {
|
||||
);
|
||||
});
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.setAccountData = async () => {};
|
||||
alice.getAccountDataFromServer = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await alice.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should abort bootstrap if device signing auth fails", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async (auth, keys) => {
|
||||
const errorResponse = {
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
{
|
||||
stages: [
|
||||
"m.login.password",
|
||||
],
|
||||
},
|
||||
],
|
||||
params: {},
|
||||
};
|
||||
|
||||
// If we're not just polling for flows, add on error rejecting the
|
||||
// auth attempt.
|
||||
if (auth) {
|
||||
Object.assign(errorResponse, {
|
||||
completed: [],
|
||||
error: "Invalid password",
|
||||
errcode: "M_FORBIDDEN",
|
||||
});
|
||||
}
|
||||
|
||||
const error = new MatrixError(errorResponse);
|
||||
error.httpStatus == 401;
|
||||
throw error;
|
||||
};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
alice.setAccountData = async () => {};
|
||||
alice.getAccountDataFromServer = async () => { };
|
||||
const authUploadDeviceSigningKeys = async func => await func({});
|
||||
|
||||
// Try bootstrap, expecting `authUploadDeviceSigningKeys` to pass
|
||||
// through failure, stopping before actually applying changes.
|
||||
let bootstrapDidThrow = false;
|
||||
try {
|
||||
await alice.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.errcode === "M_FORBIDDEN") {
|
||||
bootstrapDidThrow = true;
|
||||
}
|
||||
}
|
||||
expect(bootstrapDidThrow).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should upload a signature when a user is verified", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
@@ -78,7 +136,7 @@ describe("Cross Signing", function() {
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's device key
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@bob:example.com", {
|
||||
keys: {
|
||||
@@ -135,7 +193,9 @@ 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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -273,7 +333,7 @@ describe("Cross Signing", function() {
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
@@ -363,7 +423,7 @@ describe("Cross Signing", function() {
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
|
||||
const selfSigningKey = new Uint8Array([
|
||||
0x1e, 0xf4, 0x01, 0x6d, 0x4f, 0xa1, 0x73, 0x66,
|
||||
@@ -520,7 +580,7 @@ describe("Cross Signing", function() {
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's ssk and device key
|
||||
// (NOTE: device key is not signed by ssk)
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
@@ -588,7 +648,7 @@ describe("Cross Signing", function() {
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
// Alice downloads Bob's keys
|
||||
const bobMasterSigning = new global.Olm.PkSigning();
|
||||
const bobMasterPrivkey = bobMasterSigning.generate_seed();
|
||||
@@ -740,7 +800,7 @@ describe("Cross Signing", function() {
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
// set Bob's cross-signing key
|
||||
await bob.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(bob);
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: {
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
@@ -766,7 +826,7 @@ describe("Cross Signing", function() {
|
||||
let upgradePromise = new Promise((resolve) => {
|
||||
upgradeResolveFunc = resolve;
|
||||
});
|
||||
await alice.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice);
|
||||
await upgradePromise;
|
||||
|
||||
const bobTrust = alice.checkUserTrust("@bob:example.com");
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
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.
|
||||
export async function resetCrossSigningKeys(client, {
|
||||
level,
|
||||
authUploadDeviceSigningKeys = async func => await func(),
|
||||
} = {}) {
|
||||
const crypto = client._crypto;
|
||||
|
||||
const oldKeys = Object.assign({}, crypto._crossSigningInfo.keys);
|
||||
try {
|
||||
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(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
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;
|
||||
throw e;
|
||||
}
|
||||
crypto._baseApis.emit("crossSigning.keysChanged", {});
|
||||
await crypto._afterCrossSigningLocalKeyChange();
|
||||
}
|
||||
|
||||
export async function createSecretStorageKey() {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const storagePublicKey = decryption.generate_key();
|
||||
const storagePrivateKey = decryption.get_private_key();
|
||||
decryption.free();
|
||||
return {
|
||||
// `pubkey` not used anymore with symmetric 4S
|
||||
keyInfo: { pubkey: storagePublicKey },
|
||||
privateKey: storagePrivateKey,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
IndexedDBCryptoStore,
|
||||
} from '../../../src/crypto/store/indexeddb-crypto-store';
|
||||
import {MemoryCryptoStore} from '../../../src/crypto/store/memory-crypto-store';
|
||||
import 'fake-indexeddb/auto';
|
||||
import 'jest-localstorage-mock';
|
||||
|
||||
import {
|
||||
ROOM_KEY_REQUEST_STATES,
|
||||
} from '../../../src/crypto/OutgoingRoomKeyRequestManager';
|
||||
|
||||
const requests = [
|
||||
{
|
||||
requestId: "A",
|
||||
requestBody: { session_id: "A", room_id: "A" },
|
||||
state: ROOM_KEY_REQUEST_STATES.SENT,
|
||||
},
|
||||
{
|
||||
requestId: "B",
|
||||
requestBody: { session_id: "B", room_id: "B" },
|
||||
state: ROOM_KEY_REQUEST_STATES.SENT,
|
||||
},
|
||||
{
|
||||
requestId: "C",
|
||||
requestBody: { session_id: "C", room_id: "C" },
|
||||
state: ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||
},
|
||||
];
|
||||
|
||||
describe.each([
|
||||
["IndexedDBCryptoStore",
|
||||
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
|
||||
["LocalStorageCryptoStore",
|
||||
() => new IndexedDBCryptoStore(undefined, "tests")],
|
||||
["MemoryCryptoStore", () => {
|
||||
const store = new IndexedDBCryptoStore(undefined, "tests");
|
||||
store._backend = new MemoryCryptoStore();
|
||||
store._backendPromise = Promise.resolve(store._backend);
|
||||
return store;
|
||||
}],
|
||||
])("Outgoing room key requests [%s]", function(name, dbFactory) {
|
||||
let store;
|
||||
|
||||
beforeAll(async () => {
|
||||
store = dbFactory();
|
||||
await store.startup();
|
||||
await Promise.all(requests.map((request) =>
|
||||
store.getOrAddOutgoingRoomKeyRequest(request),
|
||||
));
|
||||
});
|
||||
|
||||
it("getAllOutgoingRoomKeyRequestsByState retrieves all entries in a given state",
|
||||
async () => {
|
||||
const r = await
|
||||
store.getAllOutgoingRoomKeyRequestsByState(ROOM_KEY_REQUEST_STATES.SENT);
|
||||
expect(r).toHaveLength(2);
|
||||
requests.filter((e) => e.state == ROOM_KEY_REQUEST_STATES.SENT).forEach((e) => {
|
||||
expect(r).toContainEqual(e);
|
||||
});
|
||||
});
|
||||
|
||||
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
|
||||
async () => {
|
||||
const r =
|
||||
await store.getOutgoingRoomKeyRequestByState([ROOM_KEY_REQUEST_STATES.SENT]);
|
||||
expect(r).not.toBeNull();
|
||||
expect(r).not.toBeUndefined();
|
||||
expect(r.state).toEqual(ROOM_KEY_REQUEST_STATES.SENT);
|
||||
expect(requests).toContainEqual(r);
|
||||
});
|
||||
});
|
||||
@@ -16,10 +16,22 @@ limitations under the License.
|
||||
|
||||
import '../../olm-loader';
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import {SECRET_STORAGE_ALGORITHM_V1} from "../../../src/crypto/SecretStorage";
|
||||
import {SECRET_STORAGE_ALGORITHM_V1_AES} from "../../../src/crypto/SecretStorage";
|
||||
import {MatrixEvent} from "../../../src/models/event";
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {makeTestClients} from './verification/util';
|
||||
import {encryptAES} from "../../../src/crypto/aes";
|
||||
import {resetCrossSigningKeys, createSecretStorageKey} from "./crypto-utils";
|
||||
import {logger} from '../../../src/logger';
|
||||
|
||||
import * as utils from "../../../src/utils";
|
||||
|
||||
try {
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
||||
logger.log('nodejs was compiled without crypto support');
|
||||
}
|
||||
|
||||
async function makeTestClient(userInfo, options) {
|
||||
const client = (new TestClient(
|
||||
@@ -40,9 +52,16 @@ async function makeTestClient(userInfo, options) {
|
||||
return client;
|
||||
}
|
||||
|
||||
// Wrapper around pkSign to return a signed object. pkSign returns the
|
||||
// signature, rather than the signed object.
|
||||
function sign(obj, key, userId) {
|
||||
olmlib.pkSign(obj, key, userId);
|
||||
return obj;
|
||||
}
|
||||
|
||||
describe("Secrets", function() {
|
||||
if (!global.Olm) {
|
||||
console.warn('Not running megolm backup unit tests: libolm not present');
|
||||
logger.warn('Not running megolm backup unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -51,9 +70,8 @@ describe("Secrets", function() {
|
||||
});
|
||||
|
||||
it("should store and retrieve a secret", async function() {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const pubkey = decryption.generate_key();
|
||||
const privkey = decryption.get_private_key();
|
||||
const key = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) key[i] = i;
|
||||
|
||||
const signing = new global.Olm.PkSigning();
|
||||
const signingKey = signing.generate_seed();
|
||||
@@ -69,7 +87,7 @@ describe("Secrets", function() {
|
||||
|
||||
const getKey = jest.fn(e => {
|
||||
expect(Object.keys(e.keys)).toEqual(["abc"]);
|
||||
return ['abc', privkey];
|
||||
return ['abc', key];
|
||||
});
|
||||
|
||||
const alice = await makeTestClient(
|
||||
@@ -100,8 +118,7 @@ describe("Secrets", function() {
|
||||
};
|
||||
|
||||
const keyAccountData = {
|
||||
algorithm: SECRET_STORAGE_ALGORITHM_V1,
|
||||
pubkey: pubkey,
|
||||
algorithm: SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
};
|
||||
await alice._crypto._crossSigningInfo.signObject(keyAccountData, 'master');
|
||||
|
||||
@@ -112,11 +129,11 @@ describe("Secrets", function() {
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(await secretStorage.isStored("foo")).toBe(false);
|
||||
expect(await secretStorage.isStored("foo")).toBeFalsy();
|
||||
|
||||
await secretStorage.store("foo", "bar", ["abc"]);
|
||||
|
||||
expect(await secretStorage.isStored("foo")).toBe(true);
|
||||
expect(await secretStorage.isStored("foo")).toBeTruthy();
|
||||
expect(await secretStorage.get("foo")).toBe("bar");
|
||||
|
||||
expect(getKey).toHaveBeenCalled();
|
||||
@@ -149,6 +166,13 @@ describe("Secrets", function() {
|
||||
});
|
||||
|
||||
it("should encrypt with default key if keys is null", async function() {
|
||||
const key = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) key[i] = i;
|
||||
const getKey = jest.fn(e => {
|
||||
expect(Object.keys(e.keys)).toEqual([newKeyId]);
|
||||
return [newKeyId, key];
|
||||
});
|
||||
|
||||
let keys = {};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
@@ -156,6 +180,7 @@ describe("Secrets", function() {
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => keys[t],
|
||||
saveCrossSigningKeys: k => keys = k,
|
||||
getSecretStorageKey: getKey,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -167,10 +192,10 @@ describe("Secrets", function() {
|
||||
}),
|
||||
]);
|
||||
};
|
||||
alice.resetCrossSigningKeys();
|
||||
resetCrossSigningKeys(alice);
|
||||
|
||||
const newKeyId = await alice.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1,
|
||||
const { keyId: newKeyId } = await alice.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1_AES,
|
||||
);
|
||||
// we don't await on this because it waits for the event to come down the sync
|
||||
// which won't happen in the test setup
|
||||
@@ -201,8 +226,8 @@ describe("Secrets", function() {
|
||||
],
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
onSecretRequested: e => {
|
||||
expect(e.name).toBe("foo");
|
||||
onSecretRequested: (userId, deviceId, requestId, secretName, deviceTrust) => {
|
||||
expect(secretName).toBe("foo");
|
||||
return "bar";
|
||||
},
|
||||
},
|
||||
@@ -251,89 +276,399 @@ describe("Secrets", function() {
|
||||
expect(secret).toBe("bar");
|
||||
});
|
||||
|
||||
it("bootstraps when no storage or cross-signing keys locally", async function() {
|
||||
const bob = await makeTestClient(
|
||||
{
|
||||
userId: "@bob:example.com",
|
||||
deviceId: "bob1",
|
||||
},
|
||||
describe("bootstrap", function() {
|
||||
// keys used in some of the tests
|
||||
const XSK = new Uint8Array(
|
||||
olmlib.decodeBase64("3lo2YdJugHjfE+Or7KJ47NuKbhE7AAGLgQ/dc19913Q="),
|
||||
);
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
bob.setAccountData = async function(eventType, contents, callback) {
|
||||
const event = new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
const XSPubKey = "DRb8pFVJyEJ9OWvXeUoM0jq/C2Wt+NxzBZVuk2nRb+0";
|
||||
const USK = new Uint8Array(
|
||||
olmlib.decodeBase64("lKWi3hJGUie5xxHgySoz8PHFnZv6wvNaud/p2shN9VU="),
|
||||
);
|
||||
const USPubKey = "CUpoiTtHiyXpUmd+3ohb7JVxAlUaOG1NYs9Jlx8soQU";
|
||||
const SSK = new Uint8Array(
|
||||
olmlib.decodeBase64("1R6JVlXX99UcfUZzKuCDGQgJTw8ur1/ofgPD8pp+96M="),
|
||||
);
|
||||
const SSPubKey = "0DfNsRDzEvkCLA0gD3m7VAGJ5VClhjEsewI35xq873Q";
|
||||
const SSSSKey = new Uint8Array(
|
||||
olmlib.decodeBase64(
|
||||
"XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0=",
|
||||
),
|
||||
);
|
||||
|
||||
it("bootstraps when no storage or cross-signing keys locally", async function() {
|
||||
const key = new Uint8Array(16);
|
||||
for (let i = 0; i < 16; i++) key[i] = i;
|
||||
const getKey = jest.fn(e => {
|
||||
return [Object.keys(e.keys)[0], key];
|
||||
});
|
||||
this.store.storeAccountDataEvents([
|
||||
event,
|
||||
]);
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
|
||||
await bob.bootstrapSecretStorage();
|
||||
|
||||
const crossSigning = bob._crypto._crossSigningInfo;
|
||||
const secretStorage = bob._crypto._secretStorage;
|
||||
|
||||
expect(crossSigning.getId()).toBeTruthy();
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
|
||||
expect(await secretStorage.hasKey()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("bootstraps when cross-signing keys in secret storage", async function() {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const storagePublicKey = decryption.generate_key();
|
||||
const storagePrivateKey = decryption.get_private_key();
|
||||
|
||||
const bob = await makeTestClient(
|
||||
{
|
||||
userId: "@bob:example.com",
|
||||
deviceId: "bob1",
|
||||
},
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getSecretStorageKey: async request => {
|
||||
const defaultKeyId = await bob.getDefaultSecretStorageKeyId();
|
||||
expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
|
||||
return [defaultKeyId, storagePrivateKey];
|
||||
const bob = await makeTestClient(
|
||||
{
|
||||
userId: "@bob:example.com",
|
||||
deviceId: "bob1",
|
||||
},
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getSecretStorageKey: getKey,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
);
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
bob.setAccountData = async function(eventType, contents, callback) {
|
||||
const event = new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
});
|
||||
this.store.storeAccountDataEvents([
|
||||
event,
|
||||
]);
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
bob.setAccountData = async function(eventType, contents, callback) {
|
||||
const event = new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
await bob.bootstrapSecretStorage({
|
||||
createSecretStorageKey,
|
||||
});
|
||||
this.store.storeAccountDataEvents([
|
||||
event,
|
||||
]);
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
bob._crypto.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.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => ({ pubkey: storagePublicKey }),
|
||||
expect(crossSigning.getId()).toBeTruthy();
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
.toBeTruthy();
|
||||
expect(await secretStorage.hasKey()).toBeTruthy();
|
||||
});
|
||||
|
||||
// Clear local cross-signing keys and read from secret storage
|
||||
bob._crypto._deviceList.storeCrossSigningForUser(
|
||||
"@bob:example.com",
|
||||
crossSigning.toStorage(),
|
||||
);
|
||||
crossSigning.keys = {};
|
||||
await bob.bootstrapSecretStorage();
|
||||
it("bootstraps when cross-signing keys in secret storage", async function() {
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
const storagePublicKey = decryption.generate_key();
|
||||
const storagePrivateKey = decryption.get_private_key();
|
||||
|
||||
expect(crossSigning.getId()).toBeTruthy();
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
|
||||
expect(await secretStorage.hasKey()).toBeTruthy();
|
||||
const bob = await makeTestClient(
|
||||
{
|
||||
userId: "@bob:example.com",
|
||||
deviceId: "bob1",
|
||||
},
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getSecretStorageKey: async request => {
|
||||
const defaultKeyId = await bob.getDefaultSecretStorageKeyId();
|
||||
expect(Object.keys(request.keys)).toEqual([defaultKeyId]);
|
||||
return [defaultKeyId, storagePrivateKey];
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
bob.setAccountData = async function(eventType, contents, callback) {
|
||||
const event = new MatrixEvent({
|
||||
type: eventType,
|
||||
content: contents,
|
||||
});
|
||||
this.store.storeAccountDataEvents([
|
||||
event,
|
||||
]);
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
bob._crypto.checkKeyBackup = async () => {};
|
||||
|
||||
const crossSigning = bob._crypto._crossSigningInfo;
|
||||
const secretStorage = bob._crypto._secretStorage;
|
||||
|
||||
// Set up cross-signing keys from scratch with specific storage key
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
await bob.bootstrapSecretStorage({
|
||||
createSecretStorageKey: async () => ({
|
||||
// `pubkey` not used anymore with symmetric 4S
|
||||
keyInfo: { pubkey: storagePublicKey },
|
||||
privateKey: storagePrivateKey,
|
||||
}),
|
||||
});
|
||||
|
||||
// Clear local cross-signing keys and read from secret storage
|
||||
bob._crypto._deviceList.storeCrossSigningForUser(
|
||||
"@bob:example.com",
|
||||
crossSigning.toStorage(),
|
||||
);
|
||||
crossSigning.keys = {};
|
||||
await bob.bootstrapCrossSigning({
|
||||
authUploadDeviceSigningKeys: async func => await func({}),
|
||||
});
|
||||
|
||||
expect(crossSigning.getId()).toBeTruthy();
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
.toBeTruthy();
|
||||
expect(await secretStorage.hasKey()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("adds passphrase checking if it's lacking", async function() {
|
||||
let crossSigningKeys = {
|
||||
master: XSK,
|
||||
user_signing: USK,
|
||||
self_signing: SSK,
|
||||
};
|
||||
const secretStorageKeys = {
|
||||
key_id: SSSSKey,
|
||||
};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => crossSigningKeys[t],
|
||||
saveCrossSigningKeys: k => crossSigningKeys = k,
|
||||
getSecretStorageKey: ({keys}, name) => {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (secretStorageKeys[keyId]) {
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: "m.secret_storage.default_key",
|
||||
content: {
|
||||
key: "key_id",
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.secret_storage.key.key_id",
|
||||
content: {
|
||||
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
|
||||
passphrase: {
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: 500000,
|
||||
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
|
||||
},
|
||||
},
|
||||
}),
|
||||
// we never use these values, other than checking that they
|
||||
// exist, so just use dummy values
|
||||
new MatrixEvent({
|
||||
type: "m.cross_signing.master",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.cross_signing.self_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.cross_signing.user_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
[`ed25519:${XSPubKey}`]: XSPubKey,
|
||||
},
|
||||
},
|
||||
self_signing: sign({
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
[`ed25519:${SSPubKey}`]: SSPubKey,
|
||||
},
|
||||
}, XSK, "@alice:example.com"),
|
||||
user_signing: sign({
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
[`ed25519:${USPubKey}`]: USPubKey,
|
||||
},
|
||||
}, XSK, "@alice:example.com"),
|
||||
},
|
||||
});
|
||||
alice.getKeyBackupVersion = async () => {
|
||||
return {
|
||||
version: "1",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
auth_data: sign({
|
||||
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
|
||||
}, XSK, "@alice:example.com"),
|
||||
};
|
||||
};
|
||||
alice.setAccountData = async function(name, data) {
|
||||
const event = new MatrixEvent({
|
||||
type: name,
|
||||
content: data,
|
||||
});
|
||||
alice.store.storeAccountDataEvents([event]);
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
|
||||
await alice.bootstrapSecretStorage();
|
||||
|
||||
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
|
||||
.toEqual({key: "key_id"});
|
||||
const keyInfo = alice.getAccountData("m.secret_storage.key.key_id")
|
||||
.getContent();
|
||||
expect(keyInfo.algorithm)
|
||||
.toEqual("m.secret_storage.v1.aes-hmac-sha2");
|
||||
expect(keyInfo.passphrase).toEqual({
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: 500000,
|
||||
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
|
||||
});
|
||||
expect(keyInfo).toHaveProperty("iv");
|
||||
expect(keyInfo).toHaveProperty("mac");
|
||||
expect(alice.checkSecretStorageKey(secretStorageKeys.key_id, keyInfo))
|
||||
.toBeTruthy();
|
||||
});
|
||||
it("fixes backup keys in the wrong format", async function() {
|
||||
let crossSigningKeys = {
|
||||
master: XSK,
|
||||
user_signing: USK,
|
||||
self_signing: SSK,
|
||||
};
|
||||
const secretStorageKeys = {
|
||||
key_id: SSSSKey,
|
||||
};
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
{
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => crossSigningKeys[t],
|
||||
saveCrossSigningKeys: k => crossSigningKeys = k,
|
||||
getSecretStorageKey: ({keys}, name) => {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
if (secretStorageKeys[keyId]) {
|
||||
return [keyId, secretStorageKeys[keyId]];
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: "m.secret_storage.default_key",
|
||||
content: {
|
||||
key: "key_id",
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.secret_storage.key.key_id",
|
||||
content: {
|
||||
algorithm: "m.secret_storage.v1.aes-hmac-sha2",
|
||||
passphrase: {
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: 500000,
|
||||
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.cross_signing.master",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.cross_signing.self_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.cross_signing.user_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: {ciphertext: "bla", mac: "bla", iv: "bla"},
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.megolm_backup.v1",
|
||||
content: {
|
||||
encrypted: {
|
||||
key_id: await encryptAES(
|
||||
"123,45,6,7,89,1,234,56,78,90,12,34,5,67,8,90",
|
||||
secretStorageKeys.key_id, "m.megolm_backup.v1",
|
||||
),
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
alice._crypto._deviceList.storeCrossSigningForUser("@alice:example.com", {
|
||||
keys: {
|
||||
master: {
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["master"],
|
||||
keys: {
|
||||
[`ed25519:${XSPubKey}`]: XSPubKey,
|
||||
},
|
||||
},
|
||||
self_signing: sign({
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["self_signing"],
|
||||
keys: {
|
||||
[`ed25519:${SSPubKey}`]: SSPubKey,
|
||||
},
|
||||
}, XSK, "@alice:example.com"),
|
||||
user_signing: sign({
|
||||
user_id: "@alice:example.com",
|
||||
usage: ["user_signing"],
|
||||
keys: {
|
||||
[`ed25519:${USPubKey}`]: USPubKey,
|
||||
},
|
||||
}, XSK, "@alice:example.com"),
|
||||
},
|
||||
});
|
||||
alice.getKeyBackupVersion = async () => {
|
||||
return {
|
||||
version: "1",
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
auth_data: sign({
|
||||
public_key: "pxEXhg+4vdMf/kFwP4bVawFWdb0EmytL3eFJx++zQ0A",
|
||||
}, XSK, "@alice:example.com"),
|
||||
};
|
||||
};
|
||||
alice.setAccountData = async function(name, data) {
|
||||
const event = new MatrixEvent({
|
||||
type: name,
|
||||
content: data,
|
||||
});
|
||||
alice.store.storeAccountDataEvents([event]);
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
|
||||
await alice.bootstrapSecretStorage();
|
||||
|
||||
const backupKey = alice.getAccountData("m.megolm_backup.v1")
|
||||
.getContent();
|
||||
expect(backupKey.encrypted).toHaveProperty("key_id");
|
||||
expect(await alice.getSecret("m.megolm_backup.v1"))
|
||||
.toEqual("ey0GB1kB6jhOWgwiBUMIWg==");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ 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";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -44,7 +45,16 @@ describe("SAS verification", function() {
|
||||
});
|
||||
|
||||
it("should error on an unexpected event", async function() {
|
||||
const sas = new SAS({}, "@alice:example.com", "ABCDEFG");
|
||||
//channel, baseApis, userId, deviceId, startEvent, request
|
||||
const request = {
|
||||
onVerifierCancelled: function() {},
|
||||
};
|
||||
const channel = {
|
||||
send: function() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
};
|
||||
const sas = new SAS(channel, {}, "@alice:example.com", "ABCDEFG", null, request);
|
||||
sas.handleEvent(new MatrixEvent({
|
||||
sender: "@alice:example.com",
|
||||
type: "es.inquisition",
|
||||
@@ -122,8 +132,8 @@ describe("SAS verification", function() {
|
||||
bobSasEvent = null;
|
||||
|
||||
bobPromise = new Promise((resolve, reject) => {
|
||||
bob.client.on("crypto.verification.start", (verifier) => {
|
||||
verifier.on("show_sas", (e) => {
|
||||
bob.client.on("crypto.verification.request", request => {
|
||||
request.verifier.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
} else if (!aliceSasEvent) {
|
||||
@@ -139,7 +149,7 @@ describe("SAS verification", function() {
|
||||
}
|
||||
}
|
||||
});
|
||||
resolve(verifier);
|
||||
resolve(request.verifier);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -172,11 +182,14 @@ describe("SAS verification", function() {
|
||||
|
||||
it("should verify a key", async () => {
|
||||
let macMethod;
|
||||
let keyAgreement;
|
||||
const origSendToDevice = bob.client.sendToDevice.bind(bob.client);
|
||||
bob.client.sendToDevice = function(type, map) {
|
||||
if (type === "m.key.verification.accept") {
|
||||
macMethod = map[alice.client.getUserId()][alice.client.deviceId]
|
||||
.message_authentication_code;
|
||||
keyAgreement = map[alice.client.getUserId()][alice.client.deviceId]
|
||||
.key_agreement_protocol;
|
||||
}
|
||||
return origSendToDevice(type, map);
|
||||
};
|
||||
@@ -203,6 +216,7 @@ describe("SAS verification", function() {
|
||||
|
||||
// make sure that it uses the preferred method
|
||||
expect(macMethod).toBe("hkdf-hmac-sha256");
|
||||
expect(keyAgreement).toBe("curve25519-hkdf-sha256");
|
||||
|
||||
// make sure Alice and Bob verified each other
|
||||
const bobDevice
|
||||
@@ -275,12 +289,12 @@ describe("SAS verification", function() {
|
||||
);
|
||||
alice.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
|
||||
alice.httpBackend.flush(undefined, 2);
|
||||
await alice.client.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(alice.client);
|
||||
bob.httpBackend.when('POST', '/keys/device_signing/upload').respond(200, {});
|
||||
bob.httpBackend.when('POST', '/keys/signatures/upload').respond(200, {});
|
||||
bob.httpBackend.flush(undefined, 2);
|
||||
|
||||
await bob.client.resetCrossSigningKeys();
|
||||
await resetCrossSigningKeys(bob.client);
|
||||
|
||||
bob.client._crypto._deviceList.storeCrossSigningForUser(
|
||||
"@alice:example.com", {
|
||||
@@ -339,11 +353,11 @@ describe("SAS verification", function() {
|
||||
};
|
||||
|
||||
const bobPromise = new Promise((resolve, reject) => {
|
||||
bob.client.on("crypto.verification.start", (verifier) => {
|
||||
verifier.on("show_sas", (e) => {
|
||||
bob.client.on("crypto.verification.request", request => {
|
||||
request.verifier.on("show_sas", (e) => {
|
||||
e.mismatch();
|
||||
});
|
||||
resolve(verifier);
|
||||
resolve(request.verifier);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {VerificationBase} from '../../../../src/crypto/verification/Base';
|
||||
import {CrossSigningInfo} from '../../../../src/crypto/CrossSigning';
|
||||
import {encodeBase64} from "../../../../src/crypto/olmlib";
|
||||
import {setupWebcrypto, teardownWebcrypto} from './util';
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
// Private key for tests only
|
||||
const testKey = new Uint8Array([
|
||||
0xda, 0x5a, 0x27, 0x60, 0xe3, 0x3a, 0xc5, 0x82,
|
||||
0x9d, 0x12, 0xc3, 0xbe, 0xe8, 0xaa, 0xc2, 0xef,
|
||||
0xae, 0xb1, 0x05, 0xc1, 0xe7, 0x62, 0x78, 0xa6,
|
||||
0xd7, 0x1f, 0xf8, 0x2c, 0x51, 0x85, 0xf0, 0x1d,
|
||||
]);
|
||||
const testKeyPub = "nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk";
|
||||
|
||||
describe("self-verifications", () => {
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
return global.Olm.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("triggers a request for key sharing upon completion", async () => {
|
||||
const userId = "@test:localhost";
|
||||
|
||||
const cacheCallbacks = {
|
||||
getCrossSigningKeyCache: jest.fn().mockReturnValue(null),
|
||||
storeCrossSigningKeyCache: jest.fn(),
|
||||
};
|
||||
|
||||
const _crossSigningInfo = new CrossSigningInfo(
|
||||
userId,
|
||||
{},
|
||||
cacheCallbacks,
|
||||
);
|
||||
_crossSigningInfo.keys = {
|
||||
master: { keys: { X: testKeyPub } },
|
||||
self_signing: { keys: { X: testKeyPub } },
|
||||
user_signing: { keys: { X: testKeyPub } },
|
||||
};
|
||||
|
||||
const _secretStorage = {
|
||||
request: jest.fn().mockReturnValue({
|
||||
promise: Promise.resolve(encodeBase64(testKey)),
|
||||
}),
|
||||
};
|
||||
|
||||
const storeSessionBackupPrivateKey = jest.fn();
|
||||
const restoreKeyBackupWithCache = jest.fn(() => Promise.resolve());
|
||||
|
||||
const client = {
|
||||
_crypto: {
|
||||
_crossSigningInfo,
|
||||
_secretStorage,
|
||||
storeSessionBackupPrivateKey,
|
||||
getSessionBackupPrivateKey: () => null,
|
||||
},
|
||||
requestSecret: _secretStorage.request.bind(_secretStorage),
|
||||
getUserId: () => userId,
|
||||
getKeyBackupVersion: () => Promise.resolve({}),
|
||||
restoreKeyBackupWithCache,
|
||||
};
|
||||
|
||||
const request = {
|
||||
onVerifierFinished: () => undefined,
|
||||
};
|
||||
|
||||
const verification = new VerificationBase(
|
||||
undefined, // channel
|
||||
client, // baseApis
|
||||
userId,
|
||||
"ABC", // deviceId
|
||||
undefined, // startEvent
|
||||
request,
|
||||
);
|
||||
verification._resolve = () => undefined;
|
||||
|
||||
const result = await verification.done();
|
||||
|
||||
/* We should request, and store, 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(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[0][1])
|
||||
.toEqual(testKey);
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls[1][1])
|
||||
.toEqual(testKey);
|
||||
|
||||
expect(storeSessionBackupPrivateKey.mock.calls[0][0])
|
||||
.toEqual(testKey);
|
||||
|
||||
expect(restoreKeyBackupWithCache).toHaveBeenCalled();
|
||||
|
||||
expect(result).toBeInstanceOf(Array);
|
||||
expect(result[0][0]).toBe(testKeyPub);
|
||||
expect(result[1][0]).toBe(testKeyPub);
|
||||
});
|
||||
});
|
||||
@@ -18,18 +18,19 @@ limitations under the License.
|
||||
import {TestClient} from '../../../TestClient';
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import nodeCrypto from "crypto";
|
||||
import {logger} from '../../../../src/logger';
|
||||
|
||||
export async function makeTestClients(userInfos, options) {
|
||||
const clients = [];
|
||||
const clientMap = {};
|
||||
const sendToDevice = function(type, map) {
|
||||
// console.log(this.getUserId(), "sends", type, map);
|
||||
// logger.log(this.getUserId(), "sends", type, map);
|
||||
for (const [userId, devMap] of Object.entries(map)) {
|
||||
if (userId in clientMap) {
|
||||
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,
|
||||
});
|
||||
@@ -48,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,
|
||||
@@ -60,14 +61,14 @@ 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
|
||||
console.log("sending remote echo!!");
|
||||
if (tc.client === this) { // eslint-disable-line @babel/no-invalid-this
|
||||
logger.log("sending remote echo!!");
|
||||
tc.client.emit("Room.timeline", remoteEcho);
|
||||
} else {
|
||||
tc.client.emit("Room.timeline", event);
|
||||
|
||||
@@ -119,8 +119,9 @@ async function distributeEvent(ownRequest, theirRequest, event) {
|
||||
await theirRequest.channel.handleEvent(event, theirRequest, true);
|
||||
}
|
||||
|
||||
describe("verification request unit tests", function() {
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("verification request unit tests", function() {
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
});
|
||||
@@ -247,4 +248,38 @@ describe("verification request unit tests", function() {
|
||||
expect(bob1Request.done).toBe(true);
|
||||
expect(bob2Request.done).toBe(true);
|
||||
});
|
||||
|
||||
it("request times out after 10 minutes", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
await aliceRequest.channel.handleEvent(requestEvent, aliceRequest, true,
|
||||
true, true);
|
||||
|
||||
expect(aliceRequest.cancelled).toBe(false);
|
||||
expect(aliceRequest._cancellingUserId).toBe(undefined);
|
||||
jest.advanceTimersByTime(10 * 60 * 1000);
|
||||
expect(aliceRequest._cancellingUserId).toBe(alice.getUserId());
|
||||
});
|
||||
|
||||
it("request times out 2 minutes after receipt", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob.getUserId()), new Map(), alice);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
const bobRequest = new VerificationRequest(
|
||||
new InRoomChannel(bob, "!room"), new Map(), bob);
|
||||
|
||||
await bobRequest.channel.handleEvent(requestEvent, bobRequest, true);
|
||||
|
||||
expect(bobRequest.cancelled).toBe(false);
|
||||
expect(bobRequest._cancellingUserId).toBe(undefined);
|
||||
jest.advanceTimersByTime(2 * 60 * 1000);
|
||||
expect(bobRequest._cancellingUserId).toBe(bob.getUserId());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import {FilterComponent} from "../../src/filter-component";
|
||||
import {mkEvent} from '../test-utils';
|
||||
|
||||
describe("Filter Component", function() {
|
||||
describe("types", function() {
|
||||
it("should filter out events with other types", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.member',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(false);
|
||||
});
|
||||
|
||||
it("should validate events with the same type", function() {
|
||||
const filter = new FilterComponent({ types: ['m.room.message'] });
|
||||
const event = mkEvent({
|
||||
type: 'm.room.message',
|
||||
content: { },
|
||||
room: 'roomId',
|
||||
event: true,
|
||||
});
|
||||
|
||||
const checkResult = filter.check(event);
|
||||
|
||||
expect(checkResult).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -97,7 +97,7 @@ describe("InteractiveAuth", function() {
|
||||
// first we expect a call to doRequest
|
||||
doRequest.mockImplementation(function(authData) {
|
||||
logger.log("request1", authData);
|
||||
expect(authData).toEqual({});
|
||||
expect(authData).toEqual(null); // first request should be null
|
||||
const err = new MatrixError({
|
||||
session: "sessionId",
|
||||
flows: [
|
||||
@@ -143,4 +143,33 @@ describe("InteractiveAuth", function() {
|
||||
expect(stateUpdated).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it("should start an auth stage and reject if no auth flow", function() {
|
||||
const doRequest = jest.fn();
|
||||
const stateUpdated = jest.fn();
|
||||
|
||||
const ia = new InteractiveAuth({
|
||||
matrixClient: new FakeClient(),
|
||||
doRequest: doRequest,
|
||||
stateUpdated: stateUpdated,
|
||||
});
|
||||
|
||||
doRequest.mockImplementation(function(authData) {
|
||||
logger.log("request1", authData);
|
||||
expect(authData).toEqual(null); // first request should be null
|
||||
const err = new MatrixError({
|
||||
session: "sessionId",
|
||||
flows: [],
|
||||
params: {
|
||||
"logintype": { param: "aa" },
|
||||
},
|
||||
});
|
||||
err.httpStatus = 401;
|
||||
throw err;
|
||||
});
|
||||
|
||||
return ia.attemptAuth().catch(function(error) {
|
||||
expect(error.message).toBe('No appropriate authentication flow found');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -521,4 +521,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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -33,12 +33,6 @@ describe("RoomMember", function() {
|
||||
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return an identicon HTTP URL if allowDefault was set and there " +
|
||||
"was no m.room.member event", function() {
|
||||
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", true);
|
||||
expect(url.indexOf("http")).toEqual(0); // don't care about form
|
||||
});
|
||||
|
||||
it("should return nothing if there is no m.room.member and allowDefault=false",
|
||||
function() {
|
||||
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
|
||||
|
||||
+15
-19
@@ -3,6 +3,7 @@ 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 {TestClient} from "../TestClient";
|
||||
|
||||
describe("Room", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -45,12 +46,6 @@ describe("Room", function() {
|
||||
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
|
||||
});
|
||||
|
||||
it("should return an identicon HTTP URL if allowDefault was set and there " +
|
||||
"was no m.room.avatar event", function() {
|
||||
const url = room.getAvatarUrl(hsUrl, 64, 64, "crop", true);
|
||||
expect(url.indexOf("http")).toEqual(0); // don't care about form
|
||||
});
|
||||
|
||||
it("should return nothing if there is no m.room.avatar and allowDefault=false",
|
||||
function() {
|
||||
const url = room.getAvatarUrl(hsUrl, 64, 64, "crop", false);
|
||||
@@ -617,15 +612,10 @@ describe("Room", function() {
|
||||
}, event: true,
|
||||
})]);
|
||||
};
|
||||
const setAliases = function(aliases, stateKey) {
|
||||
if (!stateKey) {
|
||||
stateKey = aliases.length
|
||||
? aliases[0].split(':').splice(1).join(':') // domain+port
|
||||
: 'fibble';
|
||||
}
|
||||
const setAltAliases = function(aliases) {
|
||||
room.addLiveEvents([utils.mkEvent({
|
||||
type: "m.room.aliases", room: roomId, skey: stateKey, content: {
|
||||
aliases: aliases,
|
||||
type: "m.room.canonical_alias", room: roomId, skey: "", content: {
|
||||
alt_aliases: aliases,
|
||||
}, event: true,
|
||||
})]);
|
||||
};
|
||||
@@ -862,7 +852,7 @@ describe("Room", function() {
|
||||
"(invite join_rules) rooms if a room name doesn't exist.", function() {
|
||||
const alias = "#room_alias:here";
|
||||
setJoinRule("invite");
|
||||
setAliases([alias, "#another:here"]);
|
||||
setAltAliases([alias, "#another:here"]);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name).toEqual(alias);
|
||||
@@ -872,7 +862,7 @@ describe("Room", function() {
|
||||
"(public join_rules) rooms if a room name doesn't exist.", function() {
|
||||
const alias = "#room_alias:here";
|
||||
setJoinRule("public");
|
||||
setAliases([alias, "#another:here"]);
|
||||
setAltAliases([alias, "#another:here"]);
|
||||
room.recalculate();
|
||||
const name = room.name;
|
||||
expect(name).toEqual(alias);
|
||||
@@ -1187,7 +1177,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({
|
||||
@@ -1237,7 +1230,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({
|
||||
@@ -1384,7 +1380,7 @@ describe("Room", function() {
|
||||
let hasThrown = false;
|
||||
try {
|
||||
await room.loadMembersIfNeeded();
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
hasThrown = true;
|
||||
}
|
||||
expect(hasThrown).toEqual(true);
|
||||
|
||||
@@ -143,7 +143,7 @@ describe("MatrixScheduler", function() {
|
||||
deferA.reject({});
|
||||
try {
|
||||
await globalA;
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
await Promise.resolve();
|
||||
expect(procCount).toEqual(2);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,39 @@ limitations under the License.
|
||||
|
||||
import {SyncAccumulator} from "../../src/sync-accumulator";
|
||||
|
||||
// The event body & unsigned object get frozen to assert that they don't get altered
|
||||
// by the impl
|
||||
const RES_WITH_AGE = {
|
||||
next_batch: "abc",
|
||||
rooms: {
|
||||
invite: {},
|
||||
leave: {},
|
||||
join: {
|
||||
"!foo:bar": {
|
||||
account_data: { events: [] },
|
||||
ephemeral: { events: [] },
|
||||
unread_notifications: {},
|
||||
timeline: {
|
||||
events: [
|
||||
Object.freeze({
|
||||
content: {
|
||||
body: "This thing is happening right now!",
|
||||
},
|
||||
origin_server_ts: 123456789,
|
||||
sender: "@alice:localhost",
|
||||
type: "m.room.message",
|
||||
unsigned: Object.freeze({
|
||||
age: 50,
|
||||
}),
|
||||
}),
|
||||
],
|
||||
prev_batch: "something",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe("SyncAccumulator", function() {
|
||||
let sa;
|
||||
|
||||
@@ -368,6 +401,39 @@ describe("SyncAccumulator", function() {
|
||||
expect(summary["m.joined_member_count"]).toEqual(5);
|
||||
expect(summary["m.heroes"]).toEqual(["@bob:bar"]);
|
||||
});
|
||||
|
||||
it("should return correctly adjusted age attributes", () => {
|
||||
const delta = 1000;
|
||||
const startingTs = 1000;
|
||||
|
||||
const oldDateNow = Date.now;
|
||||
try {
|
||||
Date.now = jest.fn();
|
||||
Date.now.mockReturnValue(startingTs);
|
||||
|
||||
sa.accumulate(RES_WITH_AGE);
|
||||
|
||||
Date.now.mockReturnValue(startingTs + delta);
|
||||
|
||||
const output = sa.getJSON();
|
||||
expect(output.roomsData.join["!foo:bar"].timeline.events[0].unsigned.age).toEqual(
|
||||
RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0].unsigned.age + delta,
|
||||
);
|
||||
expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual(
|
||||
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
|
||||
);
|
||||
} finally {
|
||||
Date.now = oldDateNow;
|
||||
}
|
||||
});
|
||||
|
||||
it("should mangle age without adding extra keys", () => {
|
||||
sa.accumulate(RES_WITH_AGE);
|
||||
const output = sa.getJSON();
|
||||
expect(Object.keys(output.roomsData.join["!foo:bar"].timeline.events[0])).toEqual(
|
||||
Object.keys(RES_WITH_AGE.rooms.join["!foo:bar"].timeline.events[0]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -282,4 +282,30 @@ describe("utils", function() {
|
||||
expect(target.nonenumerableProp).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe("chunkPromises", function() {
|
||||
it("should execute promises in chunks", async function() {
|
||||
let promiseCount = 0;
|
||||
|
||||
function fn1() {
|
||||
return new Promise(async function(resolve, reject) {
|
||||
await utils.sleep(1);
|
||||
expect(promiseCount).toEqual(0);
|
||||
++promiseCount;
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
function fn2() {
|
||||
return new Promise(function(resolve, reject) {
|
||||
expect(promiseCount).toEqual(1);
|
||||
++promiseCount;
|
||||
resolve();
|
||||
});
|
||||
}
|
||||
|
||||
await utils.chunkPromises([fn1, fn2], 1);
|
||||
expect(promiseCount).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {MatrixCall, CallErrorCode, CallEvent} from '../../../src/webrtc/call';
|
||||
|
||||
const DUMMY_SDP = (
|
||||
"v=0\r\n" +
|
||||
"o=- 5022425983810148698 2 IN IP4 127.0.0.1\r\n" +
|
||||
"s=-\r\nt=0 0\r\na=group:BUNDLE 0\r\n" +
|
||||
"a=msid-semantic: WMS h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA\r\n" +
|
||||
"m=audio 9 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126\r\n" +
|
||||
"c=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=ice-ufrag:hLDR\r\n" +
|
||||
"a=ice-pwd:bMGD9aOldHWiI+6nAq/IIlRw\r\n" +
|
||||
"a=ice-options:trickle\r\n" +
|
||||
"a=fingerprint:sha-256 E4:94:84:F9:4A:98:8A:56:F5:5F:FD:AF:72:B9:32:89:49:5C:4B:9A:" +
|
||||
"4A:15:8E:41:8A:F3:69:E4:39:52:DC:D6\r\n" +
|
||||
"a=setup:active\r\n" +
|
||||
"a=mid:0\r\n" +
|
||||
"a=extmap:1 urn:ietf:params:rtp-hdrext:ssrc-audio-level\r\n" +
|
||||
"a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\n" +
|
||||
"a=extmap:3 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\n" +
|
||||
"a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid\r\n" +
|
||||
"a=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n" +
|
||||
"a=extmap:6 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n" +
|
||||
"a=sendrecv\r\n" +
|
||||
"a=msid:h3wAi7s8QpiQMH14WG3BnDbmlOqo9I5ezGZA 4357098f-3795-4131-bff4-9ba9c0348c49\r\n" +
|
||||
"a=rtcp-mux\r\n" +
|
||||
"a=rtpmap:111 opus/48000/2\r\n" +
|
||||
"a=rtcp-fb:111 transport-cc\r\n" +
|
||||
"a=fmtp:111 minptime=10;useinbandfec=1\r\n" +
|
||||
"a=rtpmap:103 ISAC/16000\r\n" +
|
||||
"a=rtpmap:104 ISAC/32000\r\n" +
|
||||
"a=rtpmap:9 G722/8000\r\n" +
|
||||
"a=rtpmap:0 PCMU/8000\r\n" +
|
||||
"a=rtpmap:8 PCMA/8000\r\n" +
|
||||
"a=rtpmap:106 CN/32000\r\n" +
|
||||
"a=rtpmap:105 CN/16000\r\n" +
|
||||
"a=rtpmap:13 CN/8000\r\n" +
|
||||
"a=rtpmap:110 telephone-event/48000\r\n" +
|
||||
"a=rtpmap:112 telephone-event/32000\r\n" +
|
||||
"a=rtpmap:113 telephone-event/16000\r\n" +
|
||||
"a=rtpmap:126 telephone-event/8000\r\n" +
|
||||
"a=ssrc:3619738545 cname:2RWtmqhXLdoF4sOi\r\n"
|
||||
);
|
||||
|
||||
class MockRTCPeerConnection {
|
||||
localDescription: RTCSessionDescription;
|
||||
|
||||
constructor() {
|
||||
this.localDescription = {
|
||||
sdp: DUMMY_SDP,
|
||||
type: 'offer',
|
||||
toJSON: function() {},
|
||||
};
|
||||
}
|
||||
|
||||
addEventListener() {}
|
||||
createOffer() {
|
||||
return Promise.resolve({});
|
||||
}
|
||||
setRemoteDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
setLocalDescription() {
|
||||
return Promise.resolve();
|
||||
}
|
||||
close() {}
|
||||
getStats() { return []; }
|
||||
}
|
||||
|
||||
describe('Call', function() {
|
||||
let client;
|
||||
let call;
|
||||
let prevNavigator;
|
||||
let prevDocument;
|
||||
let prevWindow;
|
||||
|
||||
beforeEach(function() {
|
||||
prevNavigator = global.navigator;
|
||||
prevDocument = global.document;
|
||||
prevWindow = global.window;
|
||||
|
||||
global.navigator = {
|
||||
mediaDevices: {
|
||||
// @ts-ignore Mock
|
||||
getUserMedia: () => {
|
||||
return {
|
||||
getTracks: () => [],
|
||||
getAudioTracks: () => [],
|
||||
getVideoTracks: () => [],
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
global.window = {
|
||||
// @ts-ignore Mock
|
||||
RTCPeerConnection: MockRTCPeerConnection,
|
||||
// @ts-ignore Mock
|
||||
RTCSessionDescription: {},
|
||||
// @ts-ignore Mock
|
||||
RTCIceCandidate: {},
|
||||
getUserMedia: {},
|
||||
};
|
||||
// @ts-ignore Mock
|
||||
global.document = {};
|
||||
|
||||
client = new TestClient("@alice:foo", "somedevice", "token", undefined, {});
|
||||
// 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',
|
||||
});
|
||||
// call checks one of these is wired up
|
||||
call.on('error', () => {});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
client.stop();
|
||||
global.navigator = prevNavigator;
|
||||
global.window = prevWindow;
|
||||
global.document = prevDocument;
|
||||
});
|
||||
|
||||
it('should ignore candidate events from non-matching party ID', 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: 'the_correct_party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
call.peerConn.addIceCandidate = jest.fn();
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'the_correct_party_id',
|
||||
candidates: [
|
||||
{
|
||||
candidate: '',
|
||||
sdpMid: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
|
||||
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 1,
|
||||
call_id: call.callId,
|
||||
party_id: 'some_other_party_id',
|
||||
candidates: [
|
||||
{
|
||||
candidate: '',
|
||||
sdpMid: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(call.peerConn.addIceCandidate.mock.calls.length).toBe(1);
|
||||
|
||||
// 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,101 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export enum EventType {
|
||||
// Room state events
|
||||
RoomCanonicalAlias = "m.room.canonical_alias",
|
||||
RoomCreate = "m.room.create",
|
||||
RoomJoinRules = "m.room.join_rules",
|
||||
RoomMember = "m.room.member",
|
||||
RoomThirdPartyInvite = "m.room.third_party_invite",
|
||||
RoomPowerLevels = "m.room.power_levels",
|
||||
RoomName = "m.room.name",
|
||||
RoomTopic = "m.room.topic",
|
||||
RoomAvatar = "m.room.avatar",
|
||||
RoomPinnedEvents = "m.room.pinned_events",
|
||||
RoomEncryption = "m.room.encryption",
|
||||
RoomHistoryVisibility = "m.room.history_visibility",
|
||||
RoomGuestAccess = "m.room.guest_access",
|
||||
RoomServerAcl = "m.room.server_acl",
|
||||
RoomTombstone = "m.room.tombstone",
|
||||
/**
|
||||
* @deprecated Should not be used.
|
||||
*/
|
||||
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",
|
||||
RoomMessageEncrypted = "m.room.encrypted",
|
||||
Sticker = "m.sticker",
|
||||
CallInvite = "m.call.invite",
|
||||
CallCandidates = "m.call.candidates",
|
||||
CallAnswer = "m.call.answer",
|
||||
CallHangup = "m.call.hangup",
|
||||
CallReject = "m.call.reject",
|
||||
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",
|
||||
|
||||
// Room ephemeral events
|
||||
Typing = "m.typing",
|
||||
Receipt = "m.receipt",
|
||||
Presence = "m.presence",
|
||||
|
||||
// Room account_data events
|
||||
FullyRead = "m.fully_read",
|
||||
Tag = "m.tag",
|
||||
|
||||
// User account_data events
|
||||
PushRules = "m.push_rules",
|
||||
Direct = "m.direct",
|
||||
IgnoredUserList = "m.ignored_user_list",
|
||||
|
||||
// to_device events
|
||||
RoomKey = "m.room_key",
|
||||
RoomKeyRequest = "m.room_key_request",
|
||||
ForwardedRoomKey = "m.forwarded_room_key",
|
||||
Dummy = "m.dummy",
|
||||
}
|
||||
|
||||
export enum MsgType {
|
||||
Text = "m.text",
|
||||
Emote = "m.emote",
|
||||
Notice = "m.notice",
|
||||
Image = "m.image",
|
||||
File = "m.file",
|
||||
Audio = "m.audio",
|
||||
Location = "m.location",
|
||||
Video = "m.video",
|
||||
}
|
||||
|
||||
export const RoomCreateTypeField = "type";
|
||||
|
||||
export enum RoomType {
|
||||
Space = "m.space",
|
||||
}
|
||||
Vendored
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// this is needed to tell TS about global.Olm
|
||||
import * as Olm from "olm"; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
|
||||
export {};
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface Global {
|
||||
localStorage: Storage;
|
||||
}
|
||||
}
|
||||
|
||||
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 | 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 {
|
||||
// sinkId & setSinkId are experimental and typescript doesn't know about them
|
||||
sinkId: string;
|
||||
setSinkId(outputId: string);
|
||||
}
|
||||
|
||||
interface DummyInterfaceWeShouldntBeUsingThis {}
|
||||
|
||||
interface Navigator {
|
||||
// We check for the webkit-prefixed getUserMedia to detect if we're
|
||||
// on webkit: we should check if we still need to do this
|
||||
webkitGetUserMedia: DummyInterfaceWeShouldntBeUsingThis;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -502,10 +502,12 @@ export class AutoDiscovery {
|
||||
request(
|
||||
{ method: "GET", uri: url, timeout: 5000 },
|
||||
(err, response, body) => {
|
||||
if (err || response.statusCode < 200 || response.statusCode >= 300) {
|
||||
if (err || response &&
|
||||
(response.statusCode < 200 || response.statusCode >= 300)
|
||||
) {
|
||||
let action = "FAIL_PROMPT";
|
||||
let reason = (err ? err.message : null) || "General failure";
|
||||
if (response.statusCode === 404) {
|
||||
if (response && response.statusCode === 404) {
|
||||
action = "IGNORE";
|
||||
reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
|
||||
}
|
||||
|
||||
+160
-164
@@ -28,13 +28,7 @@ import {SERVICE_TYPES} from './service-types';
|
||||
import {logger} from './logger';
|
||||
import {PushProcessor} from "./pushprocessor";
|
||||
import * as utils from "./utils";
|
||||
import {
|
||||
MatrixHttpApi,
|
||||
PREFIX_IDENTITY_V1,
|
||||
PREFIX_IDENTITY_V2,
|
||||
PREFIX_R0,
|
||||
PREFIX_UNSTABLE,
|
||||
} from "./http-api";
|
||||
import {MatrixHttpApi, PREFIX_IDENTITY_V2, PREFIX_R0, PREFIX_UNSTABLE} from "./http-api";
|
||||
|
||||
function termsUrlForService(serviceType, baseUrl) {
|
||||
switch (serviceType) {
|
||||
@@ -72,7 +66,7 @@ function termsUrlForService(serviceType, baseUrl) {
|
||||
* callback that returns a Promise<String> of an identity access token to supply
|
||||
* with identity requests. If the object is unset, no access token will be
|
||||
* supplied.
|
||||
* See also https://github.com/vector-im/riot-web/issues/10615 which seeks to
|
||||
* See also https://github.com/vector-im/element-web/issues/10615 which seeks to
|
||||
* replace the previous approach of manual access tokens params with this
|
||||
* callback throughout the SDK.
|
||||
*
|
||||
@@ -212,9 +206,6 @@ MatrixBaseApis.prototype.register = function(
|
||||
inhibitLogin = undefined;
|
||||
}
|
||||
|
||||
if (auth === undefined || auth === null) {
|
||||
auth = {};
|
||||
}
|
||||
if (sessionId) {
|
||||
auth.session = sessionId;
|
||||
}
|
||||
@@ -255,10 +246,25 @@ MatrixBaseApis.prototype.register = function(
|
||||
|
||||
/**
|
||||
* Register a guest account.
|
||||
* This method returns the auth info needed to create a new authenticated client,
|
||||
* Remember to call `setGuest(true)` on the (guest-)authenticated client, e.g:
|
||||
* ```javascript
|
||||
* const tmpClient = await sdk.createClient(MATRIX_INSTANCE);
|
||||
* const { user_id, device_id, access_token } = tmpClient.registerGuest();
|
||||
* const client = createClient({
|
||||
* baseUrl: MATRIX_INSTANCE,
|
||||
* accessToken: access_token,
|
||||
* userId: user_id,
|
||||
* deviceId: device_id,
|
||||
* })
|
||||
* client.setGuest(true);
|
||||
* ```
|
||||
*
|
||||
* @param {Object=} opts Registration options
|
||||
* @param {Object} opts.body JSON HTTP body to provide.
|
||||
* @param {module:client.callback} callback Optional.
|
||||
* @return {Promise} Resolves: TODO
|
||||
* @return {Promise} Resolves: JSON object that contains:
|
||||
* { user_id, device_id, access_token, home_server }
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.registerGuest = function(opts, callback) {
|
||||
@@ -365,15 +371,22 @@ MatrixBaseApis.prototype.getCasLoginUrl = function(redirectUrl) {
|
||||
* authenticates with the SSO.
|
||||
* @param {string} loginType The type of SSO login we are doing (sso or cas).
|
||||
* Defaults to 'sso'.
|
||||
* @param {string} idpId The ID of the Identity Provider being targeted, optional.
|
||||
* @return {string} The HS URL to hit to begin the SSO login process.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType) {
|
||||
MatrixBaseApis.prototype.getSsoLoginUrl = function(redirectUrl, loginType, idpId) {
|
||||
if (loginType === undefined) {
|
||||
loginType = "sso";
|
||||
}
|
||||
return this._http.getUrl("/login/"+loginType+"/redirect", {
|
||||
"redirectUrl": redirectUrl,
|
||||
}, PREFIX_R0);
|
||||
|
||||
let prefix = PREFIX_R0;
|
||||
let url = "/login/" + loginType + "/redirect";
|
||||
if (idpId) {
|
||||
url += "/" + idpId;
|
||||
prefix = "/_matrix/client/unstable/org.matrix.msc2858";
|
||||
}
|
||||
|
||||
return this._http.getUrl(url, { redirectUrl }, prefix);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -471,8 +484,26 @@ MatrixBaseApis.prototype.getFallbackAuthUrl = function(loginType, authSessionId)
|
||||
* room_alias: {string(opt)}}</code>
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.createRoom = function(options, callback) {
|
||||
// valid options include: room_alias_name, visibility, invite
|
||||
MatrixBaseApis.prototype.createRoom = async function(options, callback) {
|
||||
// some valid options include: room_alias_name, visibility, invite
|
||||
|
||||
// inject the id_access_token if inviting 3rd party addresses
|
||||
const invitesNeedingToken = (options.invite_3pid || [])
|
||||
.filter(i => !i.id_access_token);
|
||||
if (
|
||||
invitesNeedingToken.length > 0 &&
|
||||
this.identityServer &&
|
||||
this.identityServer.getAccessToken &&
|
||||
await this.doesServerAcceptIdentityAccessToken()
|
||||
) {
|
||||
const identityAccessToken = await this.identityServer.getAccessToken();
|
||||
if (identityAccessToken) {
|
||||
for (const invite of invitesNeedingToken) {
|
||||
invite.id_access_token = identityAccessToken;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return this._http.authedRequest(
|
||||
callback, "POST", "/createRoom", undefined, options,
|
||||
);
|
||||
@@ -1502,6 +1533,21 @@ MatrixBaseApis.prototype.getDevices = function() {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets specific device details for the logged-in user
|
||||
* @param {string} device_id device to query
|
||||
* @return {Promise} Resolves: result object
|
||||
* @return {module:http-api.MatrixError} Rejects: with an error response.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getDevice = function(device_id) {
|
||||
const path = utils.encodeUri("/devices/$device_id", {
|
||||
$device_id: device_id,
|
||||
});
|
||||
return this._http.authedRequest(
|
||||
undefined, 'GET', path, undefined, undefined,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the given device
|
||||
*
|
||||
@@ -1763,7 +1809,7 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
|
||||
content.token = opts.token;
|
||||
}
|
||||
userIds.forEach((u) => {
|
||||
content.device_keys[u] = {};
|
||||
content.device_keys[u] = [];
|
||||
});
|
||||
|
||||
return this._http.authedRequest(undefined, "POST", "/keys/query", undefined, content);
|
||||
@@ -1776,10 +1822,13 @@ MatrixBaseApis.prototype.downloadKeysForUsers = function(userIds, opts) {
|
||||
*
|
||||
* @param {string} [key_algorithm = signed_curve25519] desired key type
|
||||
*
|
||||
* @param {number} [timeout] the time (in milliseconds) to wait for keys from remote
|
||||
* servers
|
||||
*
|
||||
* @return {Promise} Resolves: result object. Rejects: with
|
||||
* an error response ({@link module:http-api.MatrixError}).
|
||||
*/
|
||||
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
|
||||
MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm, timeout) {
|
||||
const queries = {};
|
||||
|
||||
if (key_algorithm === undefined) {
|
||||
@@ -1794,6 +1843,9 @@ MatrixBaseApis.prototype.claimOneTimeKeys = function(devices, key_algorithm) {
|
||||
query[deviceId] = key_algorithm;
|
||||
}
|
||||
const content = {one_time_keys: queries};
|
||||
if (timeout) {
|
||||
content.timeout = timeout;
|
||||
}
|
||||
const path = "/keys/claim";
|
||||
return this._http.authedRequest(undefined, "POST", path, undefined, content);
|
||||
};
|
||||
@@ -1895,28 +1947,10 @@ MatrixBaseApis.prototype.requestEmailToken = async function(
|
||||
next_link: nextLink,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this._http.idServerRequest(
|
||||
undefined, "POST", "/validate/email/requestToken",
|
||||
params, PREFIX_IDENTITY_V2, identityAccessToken,
|
||||
);
|
||||
// TODO: Fold callback into above call once v1 path below is removed
|
||||
if (callback) callback(null, response);
|
||||
return response;
|
||||
} catch (err) {
|
||||
if (err.cors === "rejected" || err.httpStatus === 404) {
|
||||
// Fall back to deprecated v1 API for now
|
||||
// TODO: Remove this path once v2 is only supported version
|
||||
// See https://github.com/vector-im/riot-web/issues/10443
|
||||
logger.warn("IS doesn't support v2, falling back to deprecated v1");
|
||||
return await this._http.idServerRequest(
|
||||
callback, "POST", "/validate/email/requestToken",
|
||||
params, PREFIX_IDENTITY_V1,
|
||||
);
|
||||
}
|
||||
if (callback) callback(err);
|
||||
throw err;
|
||||
}
|
||||
return await this._http.idServerRequest(
|
||||
callback, "POST", "/validate/email/requestToken",
|
||||
params, PREFIX_IDENTITY_V2, identityAccessToken,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1963,28 +1997,10 @@ MatrixBaseApis.prototype.requestMsisdnToken = async function(
|
||||
next_link: nextLink,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await this._http.idServerRequest(
|
||||
undefined, "POST", "/validate/msisdn/requestToken",
|
||||
params, PREFIX_IDENTITY_V2, identityAccessToken,
|
||||
);
|
||||
// TODO: Fold callback into above call once v1 path below is removed
|
||||
if (callback) callback(null, response);
|
||||
return response;
|
||||
} catch (err) {
|
||||
if (err.cors === "rejected" || err.httpStatus === 404) {
|
||||
// Fall back to deprecated v1 API for now
|
||||
// TODO: Remove this path once v2 is only supported version
|
||||
// See https://github.com/vector-im/riot-web/issues/10443
|
||||
logger.warn("IS doesn't support v2, falling back to deprecated v1");
|
||||
return await this._http.idServerRequest(
|
||||
callback, "POST", "/validate/msisdn/requestToken",
|
||||
params, PREFIX_IDENTITY_V1,
|
||||
);
|
||||
}
|
||||
if (callback) callback(err);
|
||||
throw err;
|
||||
}
|
||||
return await this._http.idServerRequest(
|
||||
callback, "POST", "/validate/msisdn/requestToken",
|
||||
params, PREFIX_IDENTITY_V2, identityAccessToken,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2018,24 +2034,10 @@ MatrixBaseApis.prototype.submitMsisdnToken = async function(
|
||||
token: msisdnToken,
|
||||
};
|
||||
|
||||
try {
|
||||
return await this._http.idServerRequest(
|
||||
undefined, "POST", "/validate/msisdn/submitToken",
|
||||
params, PREFIX_IDENTITY_V2, identityAccessToken,
|
||||
);
|
||||
} catch (err) {
|
||||
if (err.cors === "rejected" || err.httpStatus === 404) {
|
||||
// Fall back to deprecated v1 API for now
|
||||
// TODO: Remove this path once v2 is only supported version
|
||||
// See https://github.com/vector-im/riot-web/issues/10443
|
||||
logger.warn("IS doesn't support v2, falling back to deprecated v1");
|
||||
return await this._http.idServerRequest(
|
||||
undefined, "POST", "/validate/msisdn/submitToken",
|
||||
params, PREFIX_IDENTITY_V1,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return await this._http.idServerRequest(
|
||||
undefined, "POST", "/validate/msisdn/submitToken",
|
||||
params, PREFIX_IDENTITY_V2, identityAccessToken,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2190,53 +2192,32 @@ MatrixBaseApis.prototype.lookupThreePid = async function(
|
||||
callback,
|
||||
identityAccessToken,
|
||||
) {
|
||||
try {
|
||||
// Note: we're using the V2 API by calling this function, but our
|
||||
// function contract requires a V1 response. We therefore have to
|
||||
// convert it manually.
|
||||
const response = await this.identityHashedLookup(
|
||||
[[address, medium]], identityAccessToken,
|
||||
);
|
||||
const result = response.find(p => p.address === address);
|
||||
if (!result) {
|
||||
// TODO: Fold callback into above call once v1 path below is removed
|
||||
if (callback) callback(null, {});
|
||||
return {};
|
||||
}
|
||||
|
||||
const mapping = {
|
||||
address,
|
||||
medium,
|
||||
mxid: result.mxid,
|
||||
|
||||
// We can't reasonably fill these parameters:
|
||||
// not_before
|
||||
// not_after
|
||||
// ts
|
||||
// signatures
|
||||
};
|
||||
|
||||
// TODO: Fold callback into above call once v1 path below is removed
|
||||
if (callback) callback(null, mapping);
|
||||
return mapping;
|
||||
} catch (err) {
|
||||
if (err.cors === "rejected" || err.httpStatus === 404) {
|
||||
// Fall back to deprecated v1 API for now
|
||||
// TODO: Remove this path once v2 is only supported version
|
||||
// See https://github.com/vector-im/riot-web/issues/10443
|
||||
const params = {
|
||||
medium: medium,
|
||||
address: address,
|
||||
};
|
||||
logger.warn("IS doesn't support v2, falling back to deprecated v1");
|
||||
return await this._http.idServerRequest(
|
||||
callback, "GET", "/lookup",
|
||||
params, PREFIX_IDENTITY_V1,
|
||||
);
|
||||
}
|
||||
if (callback) callback(err, undefined);
|
||||
throw err;
|
||||
// Note: we're using the V2 API by calling this function, but our
|
||||
// function contract requires a V1 response. We therefore have to
|
||||
// convert it manually.
|
||||
const response = await this.identityHashedLookup(
|
||||
[[address, medium]], identityAccessToken,
|
||||
);
|
||||
const result = response.find(p => p.address === address);
|
||||
if (!result) {
|
||||
if (callback) callback(null, {});
|
||||
return {};
|
||||
}
|
||||
|
||||
const mapping = {
|
||||
address,
|
||||
medium,
|
||||
mxid: result.mxid,
|
||||
|
||||
// We can't reasonably fill these parameters:
|
||||
// not_before
|
||||
// not_after
|
||||
// ts
|
||||
// signatures
|
||||
};
|
||||
|
||||
if (callback) callback(null, mapping);
|
||||
return mapping;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2254,46 +2235,29 @@ MatrixBaseApis.prototype.bulkLookupThreePids = async function(
|
||||
query,
|
||||
identityAccessToken,
|
||||
) {
|
||||
try {
|
||||
// Note: we're using the V2 API by calling this function, but our
|
||||
// function contract requires a V1 response. We therefore have to
|
||||
// convert it manually.
|
||||
const response = await this.identityHashedLookup(
|
||||
// We have to reverse the query order to get [address, medium] pairs
|
||||
query.map(p => [p[1], p[0]]), identityAccessToken,
|
||||
);
|
||||
// Note: we're using the V2 API by calling this function, but our
|
||||
// function contract requires a V1 response. We therefore have to
|
||||
// convert it manually.
|
||||
const response = await this.identityHashedLookup(
|
||||
// We have to reverse the query order to get [address, medium] pairs
|
||||
query.map(p => [p[1], p[0]]), identityAccessToken,
|
||||
);
|
||||
|
||||
const v1results = [];
|
||||
for (const mapping of response) {
|
||||
const originalQuery = query.find(p => p[1] === mapping.address);
|
||||
if (!originalQuery) {
|
||||
throw new Error("Identity sever returned unexpected results");
|
||||
}
|
||||
|
||||
v1results.push([
|
||||
originalQuery[0], // medium
|
||||
mapping.address,
|
||||
mapping.mxid,
|
||||
]);
|
||||
const v1results = [];
|
||||
for (const mapping of response) {
|
||||
const originalQuery = query.find(p => p[1] === mapping.address);
|
||||
if (!originalQuery) {
|
||||
throw new Error("Identity sever returned unexpected results");
|
||||
}
|
||||
|
||||
return {threepids: v1results};
|
||||
} catch (err) {
|
||||
if (err.cors === "rejected" || err.httpStatus === 404) {
|
||||
// Fall back to deprecated v1 API for now
|
||||
// TODO: Remove this path once v2 is only supported version
|
||||
// See https://github.com/vector-im/riot-web/issues/10443
|
||||
const params = {
|
||||
threepids: query,
|
||||
};
|
||||
logger.warn("IS doesn't support v2, falling back to deprecated v1");
|
||||
return await this._http.idServerRequest(
|
||||
undefined, "POST", "/bulk_lookup", params,
|
||||
PREFIX_IDENTITY_V1, identityAccessToken,
|
||||
);
|
||||
}
|
||||
throw err;
|
||||
v1results.push([
|
||||
originalQuery[0], // medium
|
||||
mapping.address,
|
||||
mapping.mxid,
|
||||
]);
|
||||
}
|
||||
|
||||
return {threepids: v1results};
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -2440,3 +2404,35 @@ MatrixBaseApis.prototype.reportEvent = function(roomId, eventId, score, reason)
|
||||
return this._http.authedRequest(undefined, "POST", path, null, {score, reason});
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches or paginates a summary of a space as defined by MSC2946
|
||||
* @param {string} roomId The ID of the space-room to use as the root of the summary.
|
||||
* @param {number?} maxRoomsPerSpace The maximum number of rooms to return per subspace.
|
||||
* @param {boolean?} suggestedOnly Whether to only return rooms with suggested=true.
|
||||
* @param {boolean?} autoJoinOnly Whether to only return rooms with auto_join=true.
|
||||
* @param {number?} limit The maximum number of rooms to return in total.
|
||||
* @param {string?} batch The opaque token to paginate a previous summary request.
|
||||
* @returns {Promise} the response, with next_batch, rooms, events fields.
|
||||
*/
|
||||
MatrixBaseApis.prototype.getSpaceSummary = function(
|
||||
roomId,
|
||||
maxRoomsPerSpace,
|
||||
suggestedOnly,
|
||||
autoJoinOnly,
|
||||
limit,
|
||||
batch,
|
||||
) {
|
||||
const path = utils.encodeUri("/rooms/$roomId/spaces", {
|
||||
$roomId: roomId,
|
||||
});
|
||||
|
||||
return this._http.authedRequest(undefined, "POST", path, null, {
|
||||
max_rooms_per_space: maxRoomsPerSpace,
|
||||
suggested_only: suggestedOnly,
|
||||
auto_join_only: autoJoinOnly,
|
||||
limit,
|
||||
batch,
|
||||
}, {
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2946",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ matrixcs.request(function(opts, fn) {
|
||||
let indexedDB;
|
||||
try {
|
||||
indexedDB = global.indexedDB;
|
||||
} catch(e) {}
|
||||
} catch (e) {}
|
||||
|
||||
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
|
||||
if (indexedDB) {
|
||||
|
||||
+902
-471
File diff suppressed because it is too large
Load Diff
@@ -75,35 +75,3 @@ export function getHttpUriForMxc(baseUrl, mxc, width, height,
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params))) + fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an identicon URL from an arbitrary string.
|
||||
* @param {string} baseUrl The base homeserver url which has a content repo.
|
||||
* @param {string} identiconString The string to create an identicon for.
|
||||
* @param {Number} width The desired width of the image in pixels. Default: 96.
|
||||
* @param {Number} height The desired height of the image in pixels. Default: 96.
|
||||
* @return {string} The complete URL to the identicon.
|
||||
* @deprecated This is no longer in the specification.
|
||||
*/
|
||||
export function getIdenticonUri(baseUrl, identiconString, width, height) {
|
||||
if (!identiconString) {
|
||||
return null;
|
||||
}
|
||||
if (!width) {
|
||||
width = 96;
|
||||
}
|
||||
if (!height) {
|
||||
height = 96;
|
||||
}
|
||||
const params = {
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
|
||||
const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
|
||||
$ident: identiconString,
|
||||
});
|
||||
return baseUrl + path +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params)));
|
||||
}
|
||||
|
||||
+329
-54
@@ -23,6 +23,10 @@ limitations under the License.
|
||||
import {decodeBase64, encodeBase64, pkSign, pkVerify} from './olmlib';
|
||||
import {EventEmitter} from 'events';
|
||||
import {logger} from '../logger';
|
||||
import {IndexedDBCryptoStore} from '../crypto/store/indexeddb-crypto-store';
|
||||
import {decryptAES, encryptAES} from './aes';
|
||||
|
||||
const KEY_REQUEST_TIMEOUT_MS = 1000 * 60;
|
||||
|
||||
function publicKeyFromKeyInfo(keyInfo) {
|
||||
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
|
||||
@@ -40,8 +44,9 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
* @param {string} userId the user that the information is about
|
||||
* @param {object} callbacks Callbacks used to interact with the app
|
||||
* Requires getCrossSigningKey and saveCrossSigningKeys
|
||||
* @param {object} cacheCallbacks Callbacks used to interact with the cache
|
||||
*/
|
||||
constructor(userId, callbacks) {
|
||||
constructor(userId, callbacks, cacheCallbacks) {
|
||||
super();
|
||||
|
||||
// you can't change the userId
|
||||
@@ -50,42 +55,15 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
value: userId,
|
||||
});
|
||||
this._callbacks = callbacks || {};
|
||||
this._cacheCallbacks = cacheCallbacks || {};
|
||||
this.keys = {};
|
||||
this.firstUse = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the app callback to ask for a private key
|
||||
* @param {string} type The key type ("master", "self_signing", or "user_signing")
|
||||
* @param {string} expectedPubkey The matching public key or undefined to use
|
||||
* the stored public key for the given key type.
|
||||
* @returns {Array} An array with [ public key, Olm.PkSigning ]
|
||||
*/
|
||||
async getCrossSigningKey(type, expectedPubkey) {
|
||||
if (!this._callbacks.getCrossSigningKey) {
|
||||
throw new Error("No getCrossSigningKey callback supplied");
|
||||
}
|
||||
|
||||
if (expectedPubkey === undefined) {
|
||||
expectedPubkey = this.getId(type);
|
||||
}
|
||||
|
||||
const privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey);
|
||||
if (!privkey) {
|
||||
throw new Error(
|
||||
"getCrossSigningKey callback for " + type + " returned falsey",
|
||||
);
|
||||
}
|
||||
const signing = new global.Olm.PkSigning();
|
||||
const gotPubkey = signing.init_with_seed(privkey);
|
||||
if (gotPubkey !== expectedPubkey) {
|
||||
signing.free();
|
||||
throw new Error(
|
||||
"Key type " + type + " from getCrossSigningKey callback did not match",
|
||||
);
|
||||
} else {
|
||||
return [gotPubkey, signing];
|
||||
}
|
||||
// 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) {
|
||||
@@ -102,23 +80,100 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
return {
|
||||
keys: this.keys,
|
||||
firstUse: this.firstUse,
|
||||
crossSigningVerifiedBefore: this.crossSigningVerifiedBefore,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the app callback to ask for a private key
|
||||
*
|
||||
* @param {string} type The key type ("master", "self_signing", or "user_signing")
|
||||
* @param {string} expectedPubkey The matching public key or undefined to use
|
||||
* the stored public key for the given key type.
|
||||
* @returns {Array} An array with [ public key, Olm.PkSigning ]
|
||||
*/
|
||||
async getCrossSigningKey(type, expectedPubkey) {
|
||||
const shouldCache = ["master", "self_signing", "user_signing"].indexOf(type) >= 0;
|
||||
|
||||
if (!this._callbacks.getCrossSigningKey) {
|
||||
throw new Error("No getCrossSigningKey callback supplied");
|
||||
}
|
||||
|
||||
if (expectedPubkey === undefined) {
|
||||
expectedPubkey = this.getId(type);
|
||||
}
|
||||
|
||||
function validateKey(key) {
|
||||
if (!key) return;
|
||||
const signing = new global.Olm.PkSigning();
|
||||
const gotPubkey = signing.init_with_seed(key);
|
||||
if (gotPubkey === expectedPubkey) {
|
||||
return [gotPubkey, signing];
|
||||
}
|
||||
signing.free();
|
||||
}
|
||||
|
||||
let privkey;
|
||||
if (this._cacheCallbacks.getCrossSigningKeyCache && shouldCache) {
|
||||
privkey = await this._cacheCallbacks
|
||||
.getCrossSigningKeyCache(type, expectedPubkey);
|
||||
}
|
||||
|
||||
const cacheresult = validateKey(privkey);
|
||||
if (cacheresult) {
|
||||
return cacheresult;
|
||||
}
|
||||
|
||||
privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey);
|
||||
const result = validateKey(privkey);
|
||||
if (result) {
|
||||
if (this._cacheCallbacks.storeCrossSigningKeyCache && shouldCache) {
|
||||
await this._cacheCallbacks.storeCrossSigningKeyCache(type, privkey);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/* No keysource even returned a key */
|
||||
if (!privkey) {
|
||||
throw new Error(
|
||||
"getCrossSigningKey callback for " + type + " returned falsey",
|
||||
);
|
||||
}
|
||||
|
||||
/* We got some keys from the keysource, but none of them were valid */
|
||||
throw new Error(
|
||||
"Key type " + type + " from getCrossSigningKey callback did not match",
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the private keys exist in secret storage.
|
||||
* XXX: This could be static, be we often seem to have an instance when we
|
||||
* want to know this anyway...
|
||||
*
|
||||
* @param {SecretStorage} secretStorage The secret store using account data
|
||||
* @returns {boolean} Whether all private keys were found in storage
|
||||
* @returns {object} map of key name to key info the secret is encrypted
|
||||
* with, or null if it is not present or not encrypted with a trusted
|
||||
* key
|
||||
*/
|
||||
async isStoredInSecretStorage(secretStorage) {
|
||||
let stored = true;
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
stored &= await secretStorage.isStored(`m.cross_signing.${type}`, false);
|
||||
// check what SSSS keys have encrypted the master key (if any)
|
||||
const stored =
|
||||
await secretStorage.isStored("m.cross_signing.master", false) || {};
|
||||
// then check which of those SSSS keys have also encrypted the SSK and USK
|
||||
function intersect(s) {
|
||||
for (const k of Object.keys(stored)) {
|
||||
if (!s[k]) {
|
||||
delete stored[k];
|
||||
}
|
||||
}
|
||||
}
|
||||
return stored;
|
||||
for (const type of ["self_signing", "user_signing"]) {
|
||||
intersect(
|
||||
await secretStorage.isStored(`m.cross_signing.${type}`, false) || {},
|
||||
);
|
||||
}
|
||||
return Object.keys(stored).length ? stored : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,12 +181,12 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
* typically called in conjunction with the creation of new cross-signing
|
||||
* keys.
|
||||
*
|
||||
* @param {object} keys The keys to store
|
||||
* @param {Map} keys The keys to store
|
||||
* @param {SecretStorage} secretStorage The secret store using account data
|
||||
*/
|
||||
static async storeInSecretStorage(keys, secretStorage) {
|
||||
for (const type of Object.keys(keys)) {
|
||||
const encodedKey = encodeBase64(keys[type]);
|
||||
for (const [type, privateKey] of keys) {
|
||||
const encodedKey = encodeBase64(privateKey);
|
||||
await secretStorage.store(`m.cross_signing.${type}`, encodedKey);
|
||||
}
|
||||
}
|
||||
@@ -147,9 +202,50 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
*/
|
||||
static async getFromSecretStorage(type, secretStorage) {
|
||||
const encodedKey = await secretStorage.get(`m.cross_signing.${type}`);
|
||||
if (!encodedKey) {
|
||||
return null;
|
||||
}
|
||||
return decodeBase64(encodedKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the private keys exist in the local key cache.
|
||||
*
|
||||
* @param {string} [type] The type of key to get. One of "master",
|
||||
* "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;
|
||||
if (!cacheCallbacks) return false;
|
||||
const types = type ? [type] : ["master", "self_signing", "user_signing"];
|
||||
for (const t of types) {
|
||||
if (!await cacheCallbacks.getCrossSigningKeyCache(t)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cross-signing private keys from the local cache.
|
||||
*
|
||||
* @returns {Map} A map from key type (string) to private key (Uint8Array)
|
||||
*/
|
||||
async getCrossSigningKeysFromCache() {
|
||||
const keys = new Map();
|
||||
const cacheCallbacks = this._cacheCallbacks;
|
||||
if (!cacheCallbacks) return keys;
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
const privKey = await cacheCallbacks.getCrossSigningKeyCache(type);
|
||||
if (!privKey) {
|
||||
continue;
|
||||
}
|
||||
keys.set(type, privKey);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID used to identify the user. This can also be used to test for
|
||||
* the existence of a given key type.
|
||||
@@ -259,6 +355,13 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* unsets the keys, used when another session has reset the keys, to disable cross-signing
|
||||
*/
|
||||
clearKeys() {
|
||||
this.keys = {};
|
||||
}
|
||||
|
||||
setKeys(keys) {
|
||||
const signingKeys = {};
|
||||
if (keys.master) {
|
||||
@@ -331,6 +434,14 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
updateCrossSigningVerifiedBefore(isCrossSigningVerified) {
|
||||
// It is critical that this value latches forward from false to true but
|
||||
// never back to false to avoid a downgrade attack.
|
||||
if (!this.crossSigningVerifiedBefore && isCrossSigningVerified) {
|
||||
this.crossSigningVerifiedBefore = true;
|
||||
}
|
||||
}
|
||||
|
||||
async signObject(data, type) {
|
||||
if (!this.keys[type]) {
|
||||
throw new Error(
|
||||
@@ -389,13 +500,13 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
&& this.getId("self_signing")
|
||||
&& this.getId("self_signing") === userCrossSigning.getId("self_signing")
|
||||
) {
|
||||
return new UserTrustLevel(true, this.firstUse);
|
||||
return new UserTrustLevel(true, true, this.firstUse);
|
||||
}
|
||||
|
||||
if (!this.keys.user_signing) {
|
||||
// If there's no user signing key, they can't possibly be verified.
|
||||
// They may be TOFU trusted though.
|
||||
return new UserTrustLevel(false, userCrossSigning.firstUse);
|
||||
return new UserTrustLevel(false, false, userCrossSigning.firstUse);
|
||||
}
|
||||
|
||||
let userTrusted;
|
||||
@@ -407,7 +518,11 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
} catch (e) {
|
||||
userTrusted = false;
|
||||
}
|
||||
return new UserTrustLevel(userTrusted, userCrossSigning.firstUse);
|
||||
return new UserTrustLevel(
|
||||
userTrusted,
|
||||
userCrossSigning.crossSigningVerifiedBefore,
|
||||
userCrossSigning.firstUse,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -416,17 +531,20 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
* @param {CrossSigningInfo} userCrossSigning Cross signing info for user
|
||||
* @param {module:crypto/deviceinfo} device The device to check
|
||||
* @param {bool} localTrust Whether the device is trusted locally
|
||||
* @param {bool} trustCrossSignedDevices Whether we trust cross signed devices
|
||||
*
|
||||
* @returns {DeviceTrustLevel}
|
||||
*/
|
||||
checkDeviceTrust(userCrossSigning, device, localTrust) {
|
||||
checkDeviceTrust(userCrossSigning, device, localTrust, trustCrossSignedDevices) {
|
||||
const userTrust = this.checkUserTrust(userCrossSigning);
|
||||
|
||||
const userSSK = userCrossSigning.keys.self_signing;
|
||||
if (!userSSK) {
|
||||
// if the user has no self-signing key then we cannot make any
|
||||
// trust assertions about this device from cross-signing
|
||||
return new DeviceTrustLevel(false, false, localTrust);
|
||||
return new DeviceTrustLevel(
|
||||
false, false, localTrust, trustCrossSignedDevices,
|
||||
);
|
||||
}
|
||||
|
||||
const deviceObj = deviceToObject(device, userCrossSigning.userId);
|
||||
@@ -438,11 +556,22 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
deviceObj, publicKeyFromKeyInfo(userSSK), userCrossSigning.userId,
|
||||
);
|
||||
// ...then we trust this device as much as far as we trust the user
|
||||
return DeviceTrustLevel.fromUserTrustLevel(userTrust, localTrust);
|
||||
return DeviceTrustLevel.fromUserTrustLevel(
|
||||
userTrust, localTrust, trustCrossSignedDevices,
|
||||
);
|
||||
} catch (e) {
|
||||
return new DeviceTrustLevel(false, false, localTrust);
|
||||
return new DeviceTrustLevel(
|
||||
false, false, localTrust, trustCrossSignedDevices,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {object} Cache callbacks
|
||||
*/
|
||||
getCacheCallbacks() {
|
||||
return this._cacheCallbacks;
|
||||
}
|
||||
}
|
||||
|
||||
function deviceToObject(device, userId) {
|
||||
@@ -465,8 +594,9 @@ export const CrossSigningLevel = {
|
||||
* Represents the ways in which we trust a user
|
||||
*/
|
||||
export class UserTrustLevel {
|
||||
constructor(crossSigningVerified, tofu) {
|
||||
constructor(crossSigningVerified, crossSigningVerifiedBefore, tofu) {
|
||||
this._crossSigningVerified = crossSigningVerified;
|
||||
this._crossSigningVerifiedBefore = crossSigningVerifiedBefore;
|
||||
this._tofu = tofu;
|
||||
}
|
||||
|
||||
@@ -484,6 +614,14 @@ export class UserTrustLevel {
|
||||
return this._crossSigningVerified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if we ever verified this user before (at least for
|
||||
* the history of verifications observed by this device).
|
||||
*/
|
||||
wasCrossSigningVerified() {
|
||||
return this._crossSigningVerifiedBefore;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {bool} true if this user's key is trusted on first use
|
||||
*/
|
||||
@@ -496,17 +634,19 @@ export class UserTrustLevel {
|
||||
* Represents the ways in which we trust a device
|
||||
*/
|
||||
export class DeviceTrustLevel {
|
||||
constructor(crossSigningVerified, tofu, localVerified) {
|
||||
constructor(crossSigningVerified, tofu, localVerified, trustCrossSignedDevices) {
|
||||
this._crossSigningVerified = crossSigningVerified;
|
||||
this._tofu = tofu;
|
||||
this._localVerified = localVerified;
|
||||
this._trustCrossSignedDevices = trustCrossSignedDevices;
|
||||
}
|
||||
|
||||
static fromUserTrustLevel(userTrustLevel, localVerified) {
|
||||
static fromUserTrustLevel(userTrustLevel, localVerified, trustCrossSignedDevices) {
|
||||
return new DeviceTrustLevel(
|
||||
userTrustLevel._crossSigningVerified,
|
||||
userTrustLevel._tofu,
|
||||
localVerified,
|
||||
trustCrossSignedDevices,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -514,7 +654,9 @@ export class DeviceTrustLevel {
|
||||
* @returns {bool} true if this device is verified via any means
|
||||
*/
|
||||
isVerified() {
|
||||
return this.isCrossSigningVerified() || this.isLocallyVerified();
|
||||
return Boolean(this.isLocallyVerified() || (
|
||||
this._trustCrossSignedDevices && this.isCrossSigningVerified()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -539,3 +681,136 @@ export class DeviceTrustLevel {
|
||||
return this._tofu;
|
||||
}
|
||||
}
|
||||
|
||||
export function createCryptoStoreCacheCallbacks(store, olmdevice) {
|
||||
return {
|
||||
getCrossSigningKeyCache: async function(type, _expectedPublicKey) {
|
||||
const key = await new Promise((resolve) => {
|
||||
return store.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
store.getSecretStorePrivateKey(txn, resolve, type);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (key && key.ciphertext) {
|
||||
const pickleKey = Buffer.from(olmdevice._pickleKey);
|
||||
const decrypted = await decryptAES(key, pickleKey, type);
|
||||
return decodeBase64(decrypted);
|
||||
} else {
|
||||
return key;
|
||||
}
|
||||
},
|
||||
storeCrossSigningKeyCache: async function(type, key) {
|
||||
if (!(key instanceof Uint8Array)) {
|
||||
throw new Error(
|
||||
`storeCrossSigningKeyCache expects Uint8Array, got ${key}`,
|
||||
);
|
||||
}
|
||||
const pickleKey = Buffer.from(olmdevice._pickleKey);
|
||||
key = await encryptAES(encodeBase64(key), pickleKey, type);
|
||||
return store.doTxn(
|
||||
'readwrite',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
store.storeSecretStorePrivateKey(txn, type, key);
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Request cross-signing keys from another device during verification.
|
||||
*
|
||||
* @param {module:base-apis~MatrixBaseApis} 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) {
|
||||
// If this is a self-verification, ask the other party for keys
|
||||
if (baseApis.getUserId() !== userId) {
|
||||
return;
|
||||
}
|
||||
logger.log("Cross-signing: Self-verification done; requesting keys");
|
||||
// This happens asynchronously, and we're not concerned about waiting for
|
||||
// it. We return here in order to test.
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = baseApis;
|
||||
const original = client._crypto._crossSigningInfo;
|
||||
|
||||
// We already have all of the infrastructure we need to validate and
|
||||
// cache cross-signing keys, so instead of replicating that, here we set
|
||||
// up callbacks that request them from the other device and call
|
||||
// CrossSigningInfo.getCrossSigningKey() to validate/cache
|
||||
const crossSigning = new CrossSigningInfo(
|
||||
original.userId,
|
||||
{ getCrossSigningKey: async (type) => {
|
||||
logger.debug("Cross-signing: requesting secret",
|
||||
type, deviceId);
|
||||
const { promise } = client.requestSecret(
|
||||
`m.cross_signing.${type}`, [deviceId],
|
||||
);
|
||||
const result = await promise;
|
||||
const decoded = decodeBase64(result);
|
||||
return Uint8Array.from(decoded);
|
||||
} },
|
||||
original._cacheCallbacks,
|
||||
);
|
||||
crossSigning.keys = original.keys;
|
||||
|
||||
// XXX: get all keys out if we get one key out
|
||||
// https://github.com/vector-im/element-web/issues/12604
|
||||
// then change here to reject on the timeout
|
||||
// Requests can be ignored, so don't wait around forever
|
||||
const timeout = new Promise((resolve, reject) => {
|
||||
setTimeout(
|
||||
resolve,
|
||||
KEY_REQUEST_TIMEOUT_MS,
|
||||
new Error("Timeout"),
|
||||
);
|
||||
});
|
||||
|
||||
// also request and cache the key backup key
|
||||
const backupKeyPromise = new Promise(async resolve => {
|
||||
const cachedKey = await client._crypto.getSessionBackupPrivateKey();
|
||||
if (!cachedKey) {
|
||||
logger.info("No cached backup key found. Requesting...");
|
||||
const secretReq = client.requestSecret(
|
||||
'm.megolm_backup.v1', [deviceId],
|
||||
);
|
||||
const base64Key = await secretReq.promise;
|
||||
logger.info("Got key backup key, decoding...");
|
||||
const decodedKey = decodeBase64(base64Key);
|
||||
logger.info("Decoded backup key, storing...");
|
||||
client._crypto.storeSessionBackupPrivateKey(
|
||||
Uint8Array.from(decodedKey),
|
||||
);
|
||||
logger.info("Backup key stored. Starting backup restore...");
|
||||
const backupInfo = await client.getKeyBackupVersion();
|
||||
// no need to await for this - just let it go in the bg
|
||||
client.restoreKeyBackupWithCache(
|
||||
undefined, undefined, backupInfo,
|
||||
).then(() => {
|
||||
logger.info("Backup restored.");
|
||||
});
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
// We call getCrossSigningKey() for its side-effects
|
||||
return Promise.race([
|
||||
Promise.all([
|
||||
crossSigning.getCrossSigningKey("master"),
|
||||
crossSigning.getCrossSigningKey("self_signing"),
|
||||
crossSigning.getCrossSigningKey("user_signing"),
|
||||
backupKeyPromise,
|
||||
]),
|
||||
timeout,
|
||||
]).then(resolve, reject);
|
||||
}).catch((e) => {
|
||||
logger.warn("Cross-signing: failure while requesting keys:", e);
|
||||
});
|
||||
}
|
||||
|
||||
+85
-33
@@ -28,7 +28,7 @@ import {DeviceInfo} from './deviceinfo';
|
||||
import {CrossSigningInfo} from './CrossSigning';
|
||||
import * as olmlib from './olmlib';
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
import {defer, sleep} from '../utils';
|
||||
import {chunkPromises, defer, sleep} from '../utils';
|
||||
|
||||
|
||||
/* State transition diagram for DeviceList._deviceTrackingStatus
|
||||
@@ -62,7 +62,7 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
|
||||
* @alias module:crypto/DeviceList
|
||||
*/
|
||||
export class DeviceList extends EventEmitter {
|
||||
constructor(baseApis, cryptoStore, olmDevice) {
|
||||
constructor(baseApis, cryptoStore, olmDevice, keyDownloadChunkSize = 250) {
|
||||
super();
|
||||
|
||||
this._cryptoStore = cryptoStore;
|
||||
@@ -98,6 +98,9 @@ export class DeviceList extends EventEmitter {
|
||||
// userId -> promise
|
||||
this._keyDownloadsInProgressByUser = {};
|
||||
|
||||
// Maximum number of user IDs per request to prevent server overload (#1619)
|
||||
this._keyDownloadChunkSize = keyDownloadChunkSize;
|
||||
|
||||
// Set whenever changes are made other than setting the sync token
|
||||
this._dirty = false;
|
||||
|
||||
@@ -109,6 +112,9 @@ export class DeviceList extends EventEmitter {
|
||||
this._savePromiseTime = null;
|
||||
// The timer used to delay the save
|
||||
this._saveTimer = null;
|
||||
// True if we have fetched data from the server or loaded a non-empty
|
||||
// set of device data from the store
|
||||
this._hasFetched = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,6 +124,7 @@ export class DeviceList extends EventEmitter {
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
|
||||
this._cryptoStore.getEndToEndDeviceData(txn, (deviceData) => {
|
||||
this._hasFetched = Boolean(deviceData && deviceData.devices);
|
||||
this._devices = deviceData ? deviceData.devices : {},
|
||||
this._crossSigningInfo = deviceData ?
|
||||
deviceData.crossSigningInfo || {} : {};
|
||||
@@ -197,16 +204,16 @@ export class DeviceList extends EventEmitter {
|
||||
const resolveSavePromise = this._resolveSavePromise;
|
||||
this._savePromiseTime = targetTime;
|
||||
this._saveTimer = setTimeout(() => {
|
||||
logger.log('Saving device tracking data at token ' + this._syncToken);
|
||||
logger.log('Saving device tracking data', this._syncToken);
|
||||
|
||||
// null out savePromise now (after the delay but before the write),
|
||||
// otherwise we could return the existing promise when the save has
|
||||
// actually already happened. Likewise for the dirty flag.
|
||||
// actually already happened.
|
||||
this._savePromiseTime = null;
|
||||
this._saveTimer = null;
|
||||
this._savePromise = null;
|
||||
this._resolveSavePromise = null;
|
||||
|
||||
this._dirty = false;
|
||||
this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_DEVICE_DATA], (txn) => {
|
||||
this._cryptoStore.storeEndToEndDeviceData({
|
||||
@@ -217,7 +224,13 @@ export class DeviceList extends EventEmitter {
|
||||
}, txn);
|
||||
},
|
||||
).then(() => {
|
||||
// The device list is considered dirty until the write
|
||||
// completes.
|
||||
this._dirty = false;
|
||||
resolveSavePromise();
|
||||
}, err => {
|
||||
logger.error('Failed to save device tracking data', this._syncToken);
|
||||
logger.error(err);
|
||||
});
|
||||
}, delay);
|
||||
}
|
||||
@@ -311,6 +324,15 @@ export class DeviceList extends EventEmitter {
|
||||
return stored;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all user IDs the DeviceList knows about
|
||||
*
|
||||
* @return {array} All known user IDs
|
||||
*/
|
||||
getKnownUserIds() {
|
||||
return Object.keys(this._devices);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stored device keys for a user id
|
||||
*
|
||||
@@ -373,6 +395,26 @@ export class DeviceList extends EventEmitter {
|
||||
return DeviceInfo.fromStorage(devs[deviceId], deviceId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a user ID by one of their device's curve25519 identity key
|
||||
*
|
||||
* @param {string} algorithm encryption algorithm
|
||||
* @param {string} senderKey curve25519 key to match
|
||||
*
|
||||
* @return {string} user ID
|
||||
*/
|
||||
getUserByIdentityKey(algorithm, senderKey) {
|
||||
if (
|
||||
algorithm !== olmlib.OLM_ALGORITHM &&
|
||||
algorithm !== olmlib.MEGOLM_ALGORITHM
|
||||
) {
|
||||
// we only deal in olm keys
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._userByIdentityKey[senderKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a device by curve25519 identity key
|
||||
*
|
||||
@@ -382,19 +424,11 @@ export class DeviceList extends EventEmitter {
|
||||
* @return {module:crypto/deviceinfo?}
|
||||
*/
|
||||
getDeviceByIdentityKey(algorithm, senderKey) {
|
||||
const userId = this._userByIdentityKey[senderKey];
|
||||
const userId = this.getUserByIdentityKey(algorithm, senderKey);
|
||||
if (!userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
algorithm !== olmlib.OLM_ALGORITHM &&
|
||||
algorithm !== olmlib.MEGOLM_ALGORITHM
|
||||
) {
|
||||
// we only deal in olm keys
|
||||
return null;
|
||||
}
|
||||
|
||||
const devices = this._devices[userId];
|
||||
if (!devices) {
|
||||
return null;
|
||||
@@ -625,6 +659,7 @@ export class DeviceList extends EventEmitter {
|
||||
});
|
||||
|
||||
const finished = (success) => {
|
||||
this.emit("crypto.willUpdateDevices", users, !this._hasFetched);
|
||||
users.forEach((u) => {
|
||||
this._dirty = true;
|
||||
|
||||
@@ -650,7 +685,8 @@ export class DeviceList extends EventEmitter {
|
||||
}
|
||||
});
|
||||
this.saveIfDirty();
|
||||
this.emit("crypto.devicesUpdated", users);
|
||||
this.emit("crypto.devicesUpdated", users, !this._hasFetched);
|
||||
this._hasFetched = true;
|
||||
};
|
||||
|
||||
return prom;
|
||||
@@ -747,33 +783,40 @@ class DeviceListUpdateSerialiser {
|
||||
opts.token = this._syncToken;
|
||||
}
|
||||
|
||||
this._baseApis.downloadKeysForUsers(
|
||||
downloadUsers, opts,
|
||||
).then((res) => {
|
||||
const dk = res.device_keys || {};
|
||||
const masterKeys = res.master_keys || {};
|
||||
const ssks = res.self_signing_keys || {};
|
||||
const usks = res.user_signing_keys || {};
|
||||
const factories = [];
|
||||
for (let i = 0; i < downloadUsers.length; i += this._deviceList._keyDownloadChunkSize) {
|
||||
const userSlice = downloadUsers.slice(i, i + this._deviceList._keyDownloadChunkSize);
|
||||
factories.push(() => this._baseApis.downloadKeysForUsers(userSlice, opts));
|
||||
}
|
||||
|
||||
// do each user in a separate promise, to avoid wedging the CPU
|
||||
// (https://github.com/vector-im/riot-web/issues/3158)
|
||||
chunkPromises(factories, 3).then(async (responses) => {
|
||||
const dk = Object.assign({}, ...(responses.map(res => res.device_keys || {})));
|
||||
const masterKeys = Object.assign({}, ...(responses.map(res => res.master_keys || {})));
|
||||
const ssks = Object.assign({}, ...(responses.map(res => res.self_signing_keys || {})));
|
||||
const usks = Object.assign({}, ...(responses.map(res => res.user_signing_keys || {})));
|
||||
|
||||
// yield to other things that want to execute in between users, to
|
||||
// avoid wedging the CPU
|
||||
// (https://github.com/vector-im/element-web/issues/3158)
|
||||
//
|
||||
// of course we ought to do this in a web worker or similar, but
|
||||
// this serves as an easy solution for now.
|
||||
let prom = Promise.resolve();
|
||||
for (const userId of downloadUsers) {
|
||||
prom = prom.then(sleep(5)).then(() => {
|
||||
return this._processQueryResponseForUser(
|
||||
await sleep(5);
|
||||
try {
|
||||
await this._processQueryResponseForUser(
|
||||
userId, dk[userId], {
|
||||
master: masterKeys[userId],
|
||||
self_signing: ssks[userId],
|
||||
user_signing: usks[userId],
|
||||
},
|
||||
);
|
||||
});
|
||||
} catch (e) {
|
||||
// log the error but continue, so that one bad key
|
||||
// doesn't kill the whole process
|
||||
logger.error(`Error processing keys for ${userId}:`, e);
|
||||
}
|
||||
}
|
||||
|
||||
return prom;
|
||||
}).then(() => {
|
||||
logger.log('Completed key download for ' + downloadUsers);
|
||||
|
||||
@@ -794,7 +837,7 @@ class DeviceListUpdateSerialiser {
|
||||
}
|
||||
|
||||
async _processQueryResponseForUser(
|
||||
userId, dkResponse, crossSigningResponse, sskResponse,
|
||||
userId, dkResponse, crossSigningResponse,
|
||||
) {
|
||||
logger.log('got device keys for ' + userId + ':', dkResponse);
|
||||
logger.log('got cross-signing keys for ' + userId + ':', crossSigningResponse);
|
||||
@@ -812,6 +855,7 @@ class DeviceListUpdateSerialiser {
|
||||
|
||||
await _updateStoredDeviceKeysForUser(
|
||||
this._olmDevice, userId, userStore, dkResponse || {},
|
||||
this._baseApis.getUserId(), this._baseApis.deviceId,
|
||||
);
|
||||
|
||||
// put the updates into the object that will be returned as our results
|
||||
@@ -849,8 +893,9 @@ class DeviceListUpdateSerialiser {
|
||||
}
|
||||
|
||||
|
||||
async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
||||
userResult) {
|
||||
async function _updateStoredDeviceKeysForUser(
|
||||
_olmDevice, userId, userStore, userResult, localUserId, localDeviceId,
|
||||
) {
|
||||
let updated = false;
|
||||
|
||||
// remove any devices in the store which aren't in the response
|
||||
@@ -860,6 +905,13 @@ async function _updateStoredDeviceKeysForUser(_olmDevice, userId, userStore,
|
||||
}
|
||||
|
||||
if (!(deviceId in userResult)) {
|
||||
if (userId === localUserId && deviceId === localDeviceId) {
|
||||
logger.warn(
|
||||
`Local device ${deviceId} missing from sync, skipping removal`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.log("Device " + userId + ":" + deviceId +
|
||||
" has been removed");
|
||||
delete userStore[deviceId];
|
||||
|
||||
@@ -0,0 +1,357 @@
|
||||
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 {
|
||||
PREFIX_UNSTABLE,
|
||||
} from "../http-api";
|
||||
|
||||
/**
|
||||
* Builds an EncryptionSetupOperation by calling any of the add.. methods.
|
||||
* Once done, `buildOperation()` can be called which allows to apply to operation.
|
||||
*
|
||||
* This is used as a helper by Crypto to keep track of all the network requests
|
||||
* and other side-effects of bootstrapping, so it can be applied in one go (and retried in the future)
|
||||
* Also keeps track of all the private keys created during bootstrapping, so we don't need to prompt for them
|
||||
* more than once.
|
||||
*/
|
||||
export class EncryptionSetupBuilder {
|
||||
/**
|
||||
* @param {Object.<String, MatrixEvent>} accountData pre-existing account data, will only be read, not written.
|
||||
* @param {CryptoCallbacks} delegateCryptoCallbacks crypto callbacks to delegate to if the key isn't in cache yet
|
||||
*/
|
||||
constructor(accountData, delegateCryptoCallbacks) {
|
||||
this.accountDataClientAdapter = new AccountDataClientAdapter(accountData);
|
||||
this.crossSigningCallbacks = new CrossSigningCallbacks();
|
||||
this.ssssCryptoCallbacks = new SSSSCryptoCallbacks(delegateCryptoCallbacks);
|
||||
|
||||
this._crossSigningKeys = null;
|
||||
this._keySignatures = null;
|
||||
this._keyBackupInfo = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds new cross-signing public keys
|
||||
*
|
||||
* @param {function} authUpload Function called to await an interactive auth
|
||||
* flow when uploading device signing keys.
|
||||
* Args:
|
||||
* {function} A function that makes the request requiring auth. Receives
|
||||
* the auth data as an object. Can be called multiple times, first with
|
||||
* an empty authDict, to obtain the flows.
|
||||
* @param {Object} keys the new keys
|
||||
*/
|
||||
addCrossSigningKeys(authUpload, keys) {
|
||||
this._crossSigningKeys = {authUpload, keys};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the key backup info to be updated on the server
|
||||
*
|
||||
* Used either to create a new key backup, or add signatures
|
||||
* from the new MSK.
|
||||
*
|
||||
* @param {Object} keyBackupInfo as received from/sent to the server
|
||||
*/
|
||||
addSessionBackup(keyBackupInfo) {
|
||||
this._keyBackupInfo = keyBackupInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the session backup private key to be updated in the local cache
|
||||
*
|
||||
* Used after fixing the format of the key
|
||||
*
|
||||
* @param {Uint8Array} privateKey
|
||||
*/
|
||||
addSessionBackupPrivateKeyToCache(privateKey) {
|
||||
this._sessionBackupPrivateKey = privateKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add signatures from a given user and device/x-sign key
|
||||
* Used to sign the new cross-signing key with the device key
|
||||
*
|
||||
* @param {String} userId
|
||||
* @param {String} deviceId
|
||||
* @param {String} signature
|
||||
*/
|
||||
addKeySignature(userId, deviceId, signature) {
|
||||
if (!this._keySignatures) {
|
||||
this._keySignatures = {};
|
||||
}
|
||||
const userSignatures = this._keySignatures[userId] || {};
|
||||
this._keySignatures[userId] = userSignatures;
|
||||
userSignatures[deviceId] = signature;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @param {Object} content
|
||||
* @return {Promise}
|
||||
*/
|
||||
setAccountData(type, content) {
|
||||
return this.accountDataClientAdapter.setAccountData(type, content);
|
||||
}
|
||||
|
||||
/**
|
||||
* builds the operation containing all the parts that have been added to the builder
|
||||
* @return {EncryptionSetupOperation}
|
||||
*/
|
||||
buildOperation() {
|
||||
const accountData = this.accountDataClientAdapter._values;
|
||||
return new EncryptionSetupOperation(
|
||||
accountData,
|
||||
this._crossSigningKeys,
|
||||
this._keyBackupInfo,
|
||||
this._keySignatures,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the created keys locally.
|
||||
*
|
||||
* This does not yet store the operation in a way that it can be restored,
|
||||
* but that is the idea in the future.
|
||||
*
|
||||
* @param {Crypto} crypto
|
||||
* @return {Promise}
|
||||
*/
|
||||
async persist(crypto) {
|
||||
// store private keys in cache
|
||||
if (this._crossSigningKeys) {
|
||||
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(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
crypto._cryptoStore.storeCrossSigningKeys(
|
||||
txn, this._crossSigningKeys.keys);
|
||||
},
|
||||
);
|
||||
}
|
||||
// store session backup key in cache
|
||||
if (this._sessionBackupPrivateKey) {
|
||||
await crypto.storeSessionBackupPrivateKey(this._sessionBackupPrivateKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be created from EncryptionSetupBuilder, or
|
||||
* (in a follow-up PR, not implemented yet) restored from storage, to retry.
|
||||
*
|
||||
* It does not have knowledge of any private keys, unlike the builder.
|
||||
*/
|
||||
export class EncryptionSetupOperation {
|
||||
/**
|
||||
* @param {Map<String, Object>} accountData
|
||||
* @param {Object} crossSigningKeys
|
||||
* @param {Object} keyBackupInfo
|
||||
* @param {Object} keySignatures
|
||||
*/
|
||||
constructor(accountData, crossSigningKeys, keyBackupInfo, keySignatures) {
|
||||
this._accountData = accountData;
|
||||
this._crossSigningKeys = crossSigningKeys;
|
||||
this._keyBackupInfo = keyBackupInfo;
|
||||
this._keySignatures = keySignatures;
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the (remaining part of, in the future) operation by sending requests to the server.
|
||||
* @param {Crypto} crypto
|
||||
*/
|
||||
async apply(crypto) {
|
||||
const baseApis = crypto._baseApis;
|
||||
// upload cross-signing keys
|
||||
if (this._crossSigningKeys) {
|
||||
const keys = {};
|
||||
for (const [name, key] of Object.entries(this._crossSigningKeys.keys)) {
|
||||
keys[name + "_key"] = key;
|
||||
}
|
||||
|
||||
// We must only call `uploadDeviceSigningKeys` from inside this auth
|
||||
// helper to ensure we properly handle auth errors.
|
||||
await this._crossSigningKeys.authUpload(authDict => {
|
||||
return baseApis.uploadDeviceSigningKeys(authDict, keys);
|
||||
});
|
||||
|
||||
// pass the new keys to the main instance of our own CrossSigningInfo.
|
||||
crypto._crossSigningInfo.setKeys(this._crossSigningKeys.keys);
|
||||
}
|
||||
// set account data
|
||||
if (this._accountData) {
|
||||
for (const [type, content] of this._accountData) {
|
||||
await baseApis.setAccountData(type, content);
|
||||
}
|
||||
}
|
||||
// upload first cross-signing signatures with the new key
|
||||
// (e.g. signing our own device)
|
||||
if (this._keySignatures) {
|
||||
await baseApis.uploadKeySignatures(this._keySignatures);
|
||||
}
|
||||
// need to create/update key backup info
|
||||
if (this._keyBackupInfo) {
|
||||
if (this._keyBackupInfo.version) {
|
||||
// session backup signature
|
||||
// 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(
|
||||
undefined, "PUT", "/room_keys/version/" + this._keyBackupInfo.version,
|
||||
undefined, {
|
||||
algorithm: this._keyBackupInfo.algorithm,
|
||||
auth_data: this._keyBackupInfo.auth_data,
|
||||
},
|
||||
{prefix: PREFIX_UNSTABLE},
|
||||
);
|
||||
} else {
|
||||
// add new key backup
|
||||
await baseApis._http.authedRequest(
|
||||
undefined, "POST", "/room_keys/version",
|
||||
undefined, this._keyBackupInfo,
|
||||
{prefix: PREFIX_UNSTABLE},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Catches account data set by SecretStorage during bootstrapping by
|
||||
* implementing the methods related to account data in MatrixClient
|
||||
*/
|
||||
class AccountDataClientAdapter extends EventEmitter {
|
||||
/**
|
||||
* @param {Object.<String, MatrixEvent>} accountData existing account data
|
||||
*/
|
||||
constructor(accountData) {
|
||||
super();
|
||||
this._existingValues = accountData;
|
||||
this._values = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @return {Promise<Object>} the content of the account data
|
||||
*/
|
||||
getAccountDataFromServer(type) {
|
||||
return Promise.resolve(this.getAccountData(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @return {Object} the content of the account data
|
||||
*/
|
||||
getAccountData(type) {
|
||||
const modifiedValue = this._values.get(type);
|
||||
if (modifiedValue) {
|
||||
return modifiedValue;
|
||||
}
|
||||
const existingValue = this._existingValues[type];
|
||||
if (existingValue) {
|
||||
return existingValue.getContent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String} type
|
||||
* @param {Object} content
|
||||
* @return {Promise}
|
||||
*/
|
||||
setAccountData(type, content) {
|
||||
const lastEvent = this._values.get(type);
|
||||
this._values.set(type, content);
|
||||
// ensure accountData is emitted on the next tick,
|
||||
// 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});
|
||||
this.emit("accountData", event, lastEvent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Catches the private cross-signing keys set during bootstrapping
|
||||
* by both cache callbacks (see createCryptoStoreCacheCallbacks) as non-cache callbacks.
|
||||
* See CrossSigningInfo constructor
|
||||
*/
|
||||
class CrossSigningCallbacks {
|
||||
constructor() {
|
||||
this.privateKeys = new Map();
|
||||
}
|
||||
|
||||
// cache callbacks
|
||||
getCrossSigningKeyCache(type, expectedPublicKey) {
|
||||
return this.getCrossSigningKey(type, expectedPublicKey);
|
||||
}
|
||||
|
||||
storeCrossSigningKeyCache(type, key) {
|
||||
this.privateKeys.set(type, key);
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// non-cache callbacks
|
||||
getCrossSigningKey(type, _expectedPubkey) {
|
||||
return Promise.resolve(this.privateKeys.get(type));
|
||||
}
|
||||
|
||||
saveCrossSigningKeys(privateKeys) {
|
||||
for (const [type, privateKey] of Object.entries(privateKeys)) {
|
||||
this.privateKeys.set(type, privateKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Catches the 4S private key set during bootstrapping by implementing
|
||||
* the SecretStorage crypto callbacks
|
||||
*/
|
||||
class SSSSCryptoCallbacks {
|
||||
constructor(delegateCryptoCallbacks) {
|
||||
this._privateKeys = new Map();
|
||||
this._delegateCryptoCallbacks = delegateCryptoCallbacks;
|
||||
}
|
||||
|
||||
async getSecretStorageKey({ keys }, name) {
|
||||
for (const keyId of Object.keys(keys)) {
|
||||
const privateKey = this._privateKeys.get(keyId);
|
||||
if (privateKey) {
|
||||
return [keyId, privateKey];
|
||||
}
|
||||
}
|
||||
// if we don't have the key cached yet, ask
|
||||
// for it to the general crypto callbacks and cache it
|
||||
if (this._delegateCryptoCallbacks) {
|
||||
const result = await this._delegateCryptoCallbacks.
|
||||
getSecretStorageKey({keys}, name);
|
||||
if (result) {
|
||||
const [keyId, privateKey] = result;
|
||||
this._privateKeys.set(keyId, privateKey);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
addPrivateKey(keyId, keyInfo, privKey) {
|
||||
this._privateKeys.set(keyId, privKey);
|
||||
// Also pass along to application to cache if it wishes
|
||||
if (
|
||||
this._delegateCryptoCallbacks &&
|
||||
this._delegateCryptoCallbacks.cacheSecretStorageKey
|
||||
) {
|
||||
this._delegateCryptoCallbacks.cacheSecretStorageKey(keyId, keyInfo, privKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
+104
-12
@@ -36,9 +36,15 @@ function checkPayloadLength(payloadString) {
|
||||
// Note that even if we manage to do the encryption, the message send may fail,
|
||||
// because by the time we've wrapped the ciphertext in the event object, it may
|
||||
// exceed 65K. But at least we won't just fail with "abort()" in that case.
|
||||
throw new Error("Message too long (" + payloadString.length + " bytes). " +
|
||||
const err = new Error("Message too long (" + payloadString.length + " bytes). " +
|
||||
"The maximum for an encrypted message is " +
|
||||
MAX_PLAINTEXT_LENGTH + " bytes.");
|
||||
// TODO: [TypeScript] We should have our own error types
|
||||
err.data = {
|
||||
errcode: "M_TOO_LARGE",
|
||||
error: "Payload too large for encrypted message",
|
||||
};
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +111,9 @@ export function OlmDevice(cryptoStore) {
|
||||
// Keep track of sessions that we're starting, so that we don't start
|
||||
// multiple sessions for the same device at the same time.
|
||||
this._sessionsInProgress = {};
|
||||
|
||||
// Used by olm to serialise prekey message decryptions
|
||||
this._olmPrekeyPromise = Promise.resolve();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -135,7 +144,7 @@ OlmDevice.prototype.init = async function(opts = {}) {
|
||||
try {
|
||||
if (fromExportedDevice) {
|
||||
if (pickleKey) {
|
||||
console.warn(
|
||||
logger.warn(
|
||||
'ignoring opts.pickleKey'
|
||||
+ ' because opts.fromExportedDevice is present.',
|
||||
);
|
||||
@@ -468,6 +477,36 @@ OlmDevice.prototype.generateOneTimeKeys = function(numKeys) {
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a new fallback keys
|
||||
*
|
||||
* @return {Promise} Resolved once the account is saved back having generated the key
|
||||
*/
|
||||
OlmDevice.prototype.generateFallbackKey = async function() {
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this._getAccount(txn, (account) => {
|
||||
account.generate_fallback_key();
|
||||
this._storeAccount(txn, account);
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
OlmDevice.prototype.getFallbackKey = async function() {
|
||||
let result;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this._getAccount(txn, (account) => {
|
||||
result = JSON.parse(account.fallback_key());
|
||||
});
|
||||
},
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a new outbound session
|
||||
*
|
||||
@@ -506,6 +545,7 @@ OlmDevice.prototype.createOutboundSession = async function(
|
||||
}
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[createOutboundSession]"),
|
||||
);
|
||||
return newSessionId;
|
||||
};
|
||||
@@ -566,6 +606,7 @@ OlmDevice.prototype.createInboundSession = async function(
|
||||
}
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[createInboundSession]"),
|
||||
);
|
||||
|
||||
return result;
|
||||
@@ -580,8 +621,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) {
|
||||
@@ -599,6 +642,7 @@ OlmDevice.prototype.getSessionIdsForDevice = async function(theirDeviceIdentityK
|
||||
},
|
||||
);
|
||||
},
|
||||
log,
|
||||
);
|
||||
|
||||
return sessionIds;
|
||||
@@ -612,13 +656,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) {
|
||||
@@ -658,11 +703,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) {
|
||||
@@ -688,6 +738,7 @@ OlmDevice.prototype.getSessionInfoForDevice = async function(deviceIdentityKey,
|
||||
}
|
||||
});
|
||||
},
|
||||
log,
|
||||
);
|
||||
|
||||
return info;
|
||||
@@ -722,6 +773,7 @@ OlmDevice.prototype.encryptMessage = async function(
|
||||
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[encryptMessage]"),
|
||||
);
|
||||
return res;
|
||||
};
|
||||
@@ -755,6 +807,7 @@ OlmDevice.prototype.decryptMessage = async function(
|
||||
this._saveSession(theirDeviceIdentityKey, sessionInfo, txn);
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[decryptMessage]"),
|
||||
);
|
||||
return payloadString;
|
||||
};
|
||||
@@ -786,6 +839,7 @@ OlmDevice.prototype.matchesSession = async function(
|
||||
matches = sessionInfo.session.matches_inbound(ciphertext);
|
||||
});
|
||||
},
|
||||
logger.withPrefix("[matchesSession]"),
|
||||
);
|
||||
return matches;
|
||||
};
|
||||
@@ -983,16 +1037,18 @@ OlmDevice.prototype._getInboundGroupSession = function(
|
||||
* @param {Object<string, string>} keysClaimed Other keys the sender claims.
|
||||
* @param {boolean} exportFormat true if the megolm keys are in export format
|
||||
* (ie, they lack an ed25519 signature)
|
||||
* @param {Object} [extraSessionData={}] any other data to be include with the session
|
||||
*/
|
||||
OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
roomId, senderKey, forwardingCurve25519KeyChain,
|
||||
sessionId, sessionKey, keysClaimed,
|
||||
exportFormat,
|
||||
exportFormat, extraSessionData = {},
|
||||
) {
|
||||
await this._cryptoStore.doTxn(
|
||||
'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(
|
||||
@@ -1019,9 +1075,14 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
+ senderKey + "/" + sessionId,
|
||||
);
|
||||
if (existingSession.first_known_index()
|
||||
<= session.first_known_index()) {
|
||||
<= session.first_known_index()
|
||||
&& !(existingSession.first_known_index() == session.first_known_index()
|
||||
&& !extraSessionData.untrusted
|
||||
&& existingSessionData.untrusted)) {
|
||||
// existing session has lower index (i.e. can
|
||||
// decrypt more), so keep it
|
||||
// decrypt more), or they have the same index and
|
||||
// the new sessions trust does not win over the old
|
||||
// sessions trust, so keep it
|
||||
logger.log(
|
||||
`Keeping existing megolm session ${sessionId}`,
|
||||
);
|
||||
@@ -1029,22 +1090,34 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
}
|
||||
}
|
||||
|
||||
const sessionData = {
|
||||
logger.info(
|
||||
"Storing megolm session " + senderKey + "/" + sessionId +
|
||||
" with first index " + session.first_known_index(),
|
||||
);
|
||||
|
||||
const sessionData = Object.assign({}, extraSessionData, {
|
||||
room_id: roomId,
|
||||
session: session.pickle(this._pickleKey),
|
||||
keysClaimed: keysClaimed,
|
||||
forwardingCurve25519KeyChain: forwardingCurve25519KeyChain,
|
||||
};
|
||||
});
|
||||
|
||||
this._cryptoStore.storeEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, sessionData, txn,
|
||||
);
|
||||
|
||||
if (!existingSession && extraSessionData.sharedHistory) {
|
||||
this._cryptoStore.addSharedHistoryInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
session.free();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
logger.withPrefix("[addInboundGroupSession]"),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1210,10 +1283,12 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
forwardingCurve25519KeyChain: (
|
||||
sessionData.forwardingCurve25519KeyChain || []
|
||||
),
|
||||
untrusted: sessionData.untrusted,
|
||||
};
|
||||
},
|
||||
);
|
||||
},
|
||||
logger.withPrefix("[decryptGroupMessage]"),
|
||||
);
|
||||
|
||||
if (error) {
|
||||
@@ -1259,6 +1334,7 @@ OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, se
|
||||
},
|
||||
);
|
||||
},
|
||||
logger.withPrefix("[hasInboundSessionKeys]"),
|
||||
);
|
||||
|
||||
return result;
|
||||
@@ -1314,10 +1390,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;
|
||||
@@ -1345,10 +1423,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
|
||||
// =========
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ const SEND_KEY_REQUESTS_DELAY_MS = 500;
|
||||
*
|
||||
* @enum {number}
|
||||
*/
|
||||
const ROOM_KEY_REQUEST_STATES = {
|
||||
export const ROOM_KEY_REQUEST_STATES = {
|
||||
/** request not yet sent */
|
||||
UNSENT: 0,
|
||||
|
||||
@@ -97,10 +97,6 @@ export class OutgoingRoomKeyRequestManager {
|
||||
*/
|
||||
start() {
|
||||
this._clientRunning = true;
|
||||
|
||||
// set the timer going, to handle any requests which didn't get sent
|
||||
// on the previous run of the client.
|
||||
this._startTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -113,7 +109,14 @@ export class OutgoingRoomKeyRequestManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send off a room key request, if we haven't already done so.
|
||||
* Send any requests that have been queued
|
||||
*/
|
||||
sendQueuedRequests() {
|
||||
this._startTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue up a room key request, if we haven't already queued or sent one.
|
||||
*
|
||||
* The `requestBody` is compared (with a deep-equality check) against
|
||||
* previous queued or sent requests and if it matches, no change is made.
|
||||
@@ -129,7 +132,7 @@ export class OutgoingRoomKeyRequestManager {
|
||||
* pending list (or we have established that a similar request already
|
||||
* exists)
|
||||
*/
|
||||
async sendRoomKeyRequest(requestBody, recipients, resend=false) {
|
||||
async queueRoomKeyRequest(requestBody, recipients, resend=false) {
|
||||
const req = await this._cryptoStore.getOutgoingRoomKeyRequest(
|
||||
requestBody,
|
||||
);
|
||||
@@ -184,7 +187,7 @@ export class OutgoingRoomKeyRequestManager {
|
||||
// in state ROOM_KEY_REQUEST_STATES.SENT, so we must have
|
||||
// raced with another tab to mark the request cancelled.
|
||||
// Try again, to make sure the request is resent.
|
||||
return await this.sendRoomKeyRequest(
|
||||
return await this.queueRoomKeyRequest(
|
||||
requestBody, recipients, resend,
|
||||
);
|
||||
}
|
||||
@@ -220,9 +223,6 @@ export class OutgoingRoomKeyRequestManager {
|
||||
throw new Error('unhandled state: ' + req.state);
|
||||
}
|
||||
}
|
||||
// some of the branches require the timer to be started. Just start it
|
||||
// all the time, because it doesn't hurt to start it.
|
||||
this._startTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -327,6 +327,21 @@ export class OutgoingRoomKeyRequestManager {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find anything in `sent` state, and kick it around the loop again.
|
||||
* This is intended for situations where something substantial has changed, and we
|
||||
* don't really expect the other end to even care about the cancellation.
|
||||
* For example, after initialization or self-verification.
|
||||
* @return {Promise} An array of `queueRoomKeyRequest` outputs.
|
||||
*/
|
||||
async cancelAndResendAllOutgoingRequests() {
|
||||
const outgoings = await this._cryptoStore.getAllOutgoingRoomKeyRequestsByState(
|
||||
ROOM_KEY_REQUEST_STATES.SENT,
|
||||
);
|
||||
return Promise.all(outgoings.map(({ requestBody, recipients }) =>
|
||||
this.queueRoomKeyRequest(requestBody, recipients, true)));
|
||||
}
|
||||
|
||||
// start the background timer to send queued requests, if the timer isn't
|
||||
// already running
|
||||
_startTimer() {
|
||||
@@ -366,15 +381,12 @@ export class OutgoingRoomKeyRequestManager {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
logger.log("Looking for queued outgoing room key requests");
|
||||
|
||||
return this._cryptoStore.getOutgoingRoomKeyRequestByState([
|
||||
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING,
|
||||
ROOM_KEY_REQUEST_STATES.CANCELLATION_PENDING_AND_WILL_RESEND,
|
||||
ROOM_KEY_REQUEST_STATES.UNSENT,
|
||||
]).then((req) => {
|
||||
if (!req) {
|
||||
logger.log("No more outgoing room key requests");
|
||||
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||
return;
|
||||
}
|
||||
@@ -398,7 +410,6 @@ export class OutgoingRoomKeyRequestManager {
|
||||
}).catch((e) => {
|
||||
logger.error("Error sending room key request; will retry later.", e);
|
||||
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||
this._startTimer();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
+137
-183
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -17,21 +17,24 @@ limitations under the License.
|
||||
import {EventEmitter} from 'events';
|
||||
import {logger} from '../logger';
|
||||
import * as olmlib from './olmlib';
|
||||
import {pkVerify} from './olmlib';
|
||||
import {randomString} from '../randomstring';
|
||||
import {encryptAES, decryptAES} from './aes';
|
||||
import {encodeBase64} from "./olmlib";
|
||||
|
||||
export const SECRET_STORAGE_ALGORITHM_V1 = "m.secret_storage.v1.curve25519-aes-sha2";
|
||||
export const SECRET_STORAGE_ALGORITHM_V1_AES
|
||||
= "m.secret_storage.v1.aes-hmac-sha2";
|
||||
|
||||
const ZERO_STR = "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0";
|
||||
|
||||
/**
|
||||
* Implements Secure Secret Storage and Sharing (MSC1946)
|
||||
* @module crypto/SecretStorage
|
||||
*/
|
||||
export class SecretStorage extends EventEmitter {
|
||||
constructor(baseApis, cryptoCallbacks, crossSigningInfo) {
|
||||
constructor(baseApis, cryptoCallbacks) {
|
||||
super();
|
||||
this._baseApis = baseApis;
|
||||
this._cryptoCallbacks = cryptoCallbacks;
|
||||
this._crossSigningInfo = crossSigningInfo;
|
||||
this._requests = {};
|
||||
this._incomingRequests = {};
|
||||
}
|
||||
@@ -45,7 +48,7 @@ export class SecretStorage extends EventEmitter {
|
||||
}
|
||||
|
||||
setDefaultKeyId(keyId) {
|
||||
return new Promise((resolve) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const listener = (ev) => {
|
||||
if (
|
||||
ev.getType() === 'm.secret_storage.default_key' &&
|
||||
@@ -57,10 +60,15 @@ export class SecretStorage extends EventEmitter {
|
||||
};
|
||||
this._baseApis.on('accountData', listener);
|
||||
|
||||
this._baseApis.setAccountData(
|
||||
'm.secret_storage.default_key',
|
||||
{ key: keyId },
|
||||
);
|
||||
try {
|
||||
await this._baseApis.setAccountData(
|
||||
'm.secret_storage.default_key',
|
||||
{ key: keyId },
|
||||
);
|
||||
} catch (e) {
|
||||
this._baseApis.removeListener('accountData', listener);
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -73,39 +81,29 @@ export class SecretStorage extends EventEmitter {
|
||||
* @param {string} [keyId] the ID of the key. If not given, a random
|
||||
* ID will be generated.
|
||||
*
|
||||
* @return {string} the ID of the key
|
||||
* @return {object} An object with:
|
||||
* keyId: {string} the ID of the key
|
||||
* keyInfo: {object} details about the key (iv, mac, passphrase)
|
||||
*/
|
||||
async addKey(algorithm, opts, keyId) {
|
||||
const keyData = {algorithm};
|
||||
const keyInfo = {algorithm};
|
||||
|
||||
if (!opts) opts = {};
|
||||
|
||||
if (opts.name) {
|
||||
keyData.name = opts.name;
|
||||
keyInfo.name = opts.name;
|
||||
}
|
||||
|
||||
switch (algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
{
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
try {
|
||||
const { passphrase, pubkey } = opts;
|
||||
// Copies in public key details of the form generated by
|
||||
// the Crypto module's `createRecoveryKeyFromPassphrase`.
|
||||
if (passphrase && pubkey) {
|
||||
keyData.passphrase = passphrase;
|
||||
keyData.pubkey = pubkey;
|
||||
} else if (pubkey) {
|
||||
keyData.pubkey = pubkey;
|
||||
} else {
|
||||
keyData.pubkey = decryption.generate_key();
|
||||
}
|
||||
} finally {
|
||||
decryption.free();
|
||||
if (algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
if (opts.passphrase) {
|
||||
keyInfo.passphrase = opts.passphrase;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (opts.key) {
|
||||
const {iv, mac} = await SecretStorage._calculateKeyCheck(opts.key);
|
||||
keyInfo.iv = iv;
|
||||
keyInfo.mac = mac;
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Unknown key algorithm ${opts.algorithm}`);
|
||||
}
|
||||
|
||||
@@ -119,40 +117,36 @@ export class SecretStorage extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
await this._crossSigningInfo.signObject(keyData, 'master');
|
||||
|
||||
await this._baseApis.setAccountData(
|
||||
`m.secret_storage.key.${keyId}`, keyData,
|
||||
`m.secret_storage.key.${keyId}`, keyInfo,
|
||||
);
|
||||
|
||||
return keyId;
|
||||
return {
|
||||
keyId,
|
||||
keyInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a given secret storage key with the cross-signing master key.
|
||||
* Get the key information for a given ID.
|
||||
*
|
||||
* @param {string} [keyId = default key's ID] The ID of the key to sign.
|
||||
* Defaults to the default key ID if not provided.
|
||||
* @param {string} [keyId = default key's ID] The ID of the key to check
|
||||
* for. Defaults to the default key ID if not provided.
|
||||
* @returns {Array?} If the key was found, the return value is an array of
|
||||
* the form [keyId, keyInfo]. Otherwise, null is returned.
|
||||
*/
|
||||
async signKey(keyId) {
|
||||
async getKey(keyId) {
|
||||
if (!keyId) {
|
||||
keyId = await this.getDefaultKeyId();
|
||||
}
|
||||
if (!keyId) {
|
||||
throw new Error("signKey requires a key ID");
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
||||
`m.secret_storage.key.${keyId}`,
|
||||
);
|
||||
if (!keyInfo) {
|
||||
throw new Error(`Key ${keyId} does not exist in account data`);
|
||||
}
|
||||
|
||||
await this._crossSigningInfo.signObject(keyInfo, 'master');
|
||||
await this._baseApis.setAccountData(
|
||||
`m.secret_storage.key.${keyId}`, keyInfo,
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
return keyInfo ? [keyId, keyInfo] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,16 +157,33 @@ export class SecretStorage extends EventEmitter {
|
||||
* @return {boolean} Whether we have the key.
|
||||
*/
|
||||
async hasKey(keyId) {
|
||||
if (!keyId) {
|
||||
keyId = await this.getDefaultKeyId();
|
||||
}
|
||||
if (!keyId) {
|
||||
return false;
|
||||
}
|
||||
return !!(await this.getKey(keyId));
|
||||
}
|
||||
|
||||
return !!this._baseApis.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
/**
|
||||
* Check whether a key matches what we expect based on the key info
|
||||
*
|
||||
* @param {Uint8Array} key the key to check
|
||||
* @param {object} info the key info
|
||||
*
|
||||
* @return {boolean} whether or not the key matches
|
||||
*/
|
||||
async checkKey(key, info) {
|
||||
if (info.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
if (info.mac) {
|
||||
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
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
throw new Error("Unknown algorithm");
|
||||
}
|
||||
}
|
||||
|
||||
static async _calculateKeyCheck(key, iv) {
|
||||
return await encryptAES(ZERO_STR, key, "", iv);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,27 +218,12 @@ export class SecretStorage extends EventEmitter {
|
||||
throw new Error("Unknown key: " + keyId);
|
||||
}
|
||||
|
||||
// check signature of key info
|
||||
pkVerify(
|
||||
keyInfo,
|
||||
this._crossSigningInfo.getId('master'),
|
||||
this._crossSigningInfo.userId,
|
||||
);
|
||||
|
||||
// encrypt secret, based on the algorithm
|
||||
switch (keyInfo.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
{
|
||||
const encryption = new global.Olm.PkEncryption();
|
||||
try {
|
||||
encryption.set_recipient_key(keyInfo.pubkey);
|
||||
encrypted[keyId] = encryption.encrypt(secret);
|
||||
} finally {
|
||||
encryption.free();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
const keys = {[keyId]: keyInfo};
|
||||
const [, encryption] = await this._getSecretStorageKey(keys, name);
|
||||
encrypted[keyId] = await encryption.encrypt(secret);
|
||||
} else {
|
||||
logger.warn("unknown algorithm for secret storage key " + keyId
|
||||
+ ": " + keyInfo.algorithm);
|
||||
// do nothing if we don't understand the encryption algorithm
|
||||
@@ -238,29 +234,6 @@ export class SecretStorage extends EventEmitter {
|
||||
await this._baseApis.setAccountData(name, {encrypted});
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a secret defined to be the same as the given key.
|
||||
* No secret information will be stored, instead the secret will
|
||||
* be stored with a marker to say that the contents of the secret is
|
||||
* the value of the given key.
|
||||
* This is useful for migration from systems that predate SSSS such as
|
||||
* key backup.
|
||||
*
|
||||
* @param {string} name The name of the secret
|
||||
* @param {string} keyId The ID of the key whose value will be the
|
||||
* value of the secret
|
||||
* @returns {Promise} resolved when account data is saved
|
||||
*/
|
||||
storePassthrough(name, keyId) {
|
||||
return this._baseApis.setAccountData(name, {
|
||||
encrypted: {
|
||||
[keyId]: {
|
||||
passthrough: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Temporary method to fix up existing accounts where secrets
|
||||
* are incorrectly stored without the 'encrypted' level
|
||||
@@ -279,7 +252,7 @@ export class SecretStorage extends EventEmitter {
|
||||
) {
|
||||
const hasKey = await this.hasKey(keys[0]);
|
||||
if (hasKey) {
|
||||
console.log("Fixing up passthrough secret: " + name);
|
||||
logger.log("Fixing up passthrough secret: " + name);
|
||||
await this.storePassthrough(name, keys[0]);
|
||||
const newData = await this._baseApis.getAccountDataFromServer(name);
|
||||
return newData;
|
||||
@@ -316,22 +289,19 @@ export class SecretStorage extends EventEmitter {
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
const encInfo = secretInfo.encrypted[keyId];
|
||||
switch (keyInfo.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
if (
|
||||
keyInfo.pubkey && (
|
||||
(encInfo.ciphertext && encInfo.mac && encInfo.ephemeral) ||
|
||||
encInfo.passthrough
|
||||
)
|
||||
) {
|
||||
// only use keys we understand the encryption algorithm of
|
||||
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
|
||||
keys[keyId] = keyInfo;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// do nothing if we don't understand the encryption algorithm
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(keys).length === 0) {
|
||||
throw new Error(`Could not decrypt ${name} because none of ` +
|
||||
`the keys it is encrypted with are for a supported algorithm`);
|
||||
}
|
||||
|
||||
let keyId;
|
||||
let decryption;
|
||||
try {
|
||||
@@ -341,18 +311,13 @@ export class SecretStorage extends EventEmitter {
|
||||
const encInfo = secretInfo.encrypted[keyId];
|
||||
|
||||
// We don't actually need the decryption object if it's a passthrough
|
||||
// since we just want to return the key itself.
|
||||
if (encInfo.passthrough) return decryption.get_private_key();
|
||||
// since we just want to return the key itself. It must be base64
|
||||
// encoded, since this is how a key would normally be stored.
|
||||
if (encInfo.passthrough) return encodeBase64(decryption.get_private_key());
|
||||
|
||||
// decrypt secret
|
||||
switch (keys[keyId].algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
return decryption.decrypt(
|
||||
encInfo.ephemeral, encInfo.mac, encInfo.ciphertext,
|
||||
);
|
||||
}
|
||||
return await decryption.decrypt(encInfo);
|
||||
} finally {
|
||||
if (decryption) decryption.free();
|
||||
if (decryption && decryption.free) decryption.free();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,55 +327,43 @@ export class SecretStorage extends EventEmitter {
|
||||
* @param {string} name the name of the secret
|
||||
* @param {boolean} checkKey check if the secret is encrypted by a trusted key
|
||||
*
|
||||
* @return {boolean} whether or not the secret is stored
|
||||
* @return {object?} map of key name to key info the secret is encrypted
|
||||
* with, or null if it is not present or not encrypted with a trusted
|
||||
* key
|
||||
*/
|
||||
async isStored(name, checkKey) {
|
||||
// check if secret exists
|
||||
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
|
||||
if (!secretInfo) return false;
|
||||
if (!secretInfo) return null;
|
||||
if (!secretInfo.encrypted) {
|
||||
// try to fix it up
|
||||
secretInfo = await this._fixupStoredSecret(name, secretInfo);
|
||||
if (!secretInfo || !secretInfo.encrypted) {
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkKey === undefined) checkKey = true;
|
||||
|
||||
// check if secret is encrypted by a known/trusted secret and
|
||||
// encryption looks sane
|
||||
const ret = {};
|
||||
|
||||
// filter secret encryption keys with supported algorithm
|
||||
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
||||
// get key information from key storage
|
||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
if (!keyInfo) return false;
|
||||
if (!keyInfo) continue;
|
||||
const encInfo = secretInfo.encrypted[keyId];
|
||||
if (checkKey) {
|
||||
pkVerify(
|
||||
keyInfo,
|
||||
this._crossSigningInfo.getId('master'),
|
||||
this._crossSigningInfo.userId,
|
||||
);
|
||||
}
|
||||
|
||||
// We don't actually need the decryption object if it's a passthrough
|
||||
// since we just want to return the key itself.
|
||||
if (encInfo.passthrough) return true;
|
||||
|
||||
switch (keyInfo.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
|
||||
&& encInfo.ephemeral) {
|
||||
return true;
|
||||
// only use keys we understand the encryption algorithm of
|
||||
if (keyInfo.algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
|
||||
ret[keyId] = keyInfo;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// do nothing if we don't understand the encryption algorithm
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return Object.keys(ret).length ? ret : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -425,6 +378,7 @@ export class SecretStorage extends EventEmitter {
|
||||
const requestId = this._baseApis.makeTxnId();
|
||||
|
||||
const requestControl = this._requests[requestId] = {
|
||||
name,
|
||||
devices,
|
||||
};
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
@@ -462,6 +416,7 @@ export class SecretStorage extends EventEmitter {
|
||||
for (const device of devices) {
|
||||
toDevice[device] = requestData;
|
||||
}
|
||||
logger.info(`Request secret ${name} from ${devices}, id ${requestId}`);
|
||||
this._baseApis.sendToDevice("m.secret.request", {
|
||||
[this._baseApis.getUserId()]: toDevice,
|
||||
});
|
||||
@@ -507,14 +462,15 @@ export class SecretStorage extends EventEmitter {
|
||||
if (!this._cryptoCallbacks.onSecretRequested) {
|
||||
return;
|
||||
}
|
||||
const secret = await this._cryptoCallbacks.onSecretRequested({
|
||||
user_id: sender,
|
||||
device_id: deviceId,
|
||||
request_id: content.request_id,
|
||||
name: content.name,
|
||||
device_trust: this._baseApis.checkDeviceTrust(sender, deviceId),
|
||||
});
|
||||
const secret = await this._cryptoCallbacks.onSecretRequested(
|
||||
sender,
|
||||
deviceId,
|
||||
content.request_id,
|
||||
content.name,
|
||||
this._baseApis.checkDeviceTrust(sender, deviceId),
|
||||
);
|
||||
if (secret) {
|
||||
logger.info(`Preparing ${content.name} secret for ${deviceId}`);
|
||||
const payload = {
|
||||
type: "m.secret.send",
|
||||
content: {
|
||||
@@ -532,7 +488,7 @@ export class SecretStorage extends EventEmitter {
|
||||
this._baseApis,
|
||||
{
|
||||
[sender]: [
|
||||
await this._baseApis.getStoredDevice(sender, deviceId),
|
||||
this._baseApis.getStoredDevice(sender, deviceId),
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -542,7 +498,7 @@ export class SecretStorage extends EventEmitter {
|
||||
this._baseApis.deviceId,
|
||||
this._baseApis._crypto._olmDevice,
|
||||
sender,
|
||||
this._baseApis._crypto.getStoredDevice(sender, deviceId),
|
||||
this._baseApis.getStoredDevice(sender, deviceId),
|
||||
payload,
|
||||
);
|
||||
const contentMap = {
|
||||
@@ -551,7 +507,10 @@ export class SecretStorage extends EventEmitter {
|
||||
},
|
||||
};
|
||||
|
||||
logger.info(`Sending ${content.name} secret for ${deviceId}`);
|
||||
this._baseApis.sendToDevice("m.room.encrypted", contentMap);
|
||||
} else {
|
||||
logger.info(`Request denied for ${content.name} secret for ${deviceId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -563,7 +522,7 @@ export class SecretStorage extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
const content = event.getContent();
|
||||
logger.log("got secret share for request ", content.request_id);
|
||||
logger.log("got secret share for request", content.request_id);
|
||||
const requestControl = this._requests[content.request_id];
|
||||
if (requestControl) {
|
||||
// make sure that the device that sent it is one of the devices that
|
||||
@@ -583,6 +542,10 @@ export class SecretStorage extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log(
|
||||
`Successfully received secret ${requestControl.name} ` +
|
||||
`from ${deviceInfo.deviceId}`,
|
||||
);
|
||||
requestControl.resolve(content.secret);
|
||||
}
|
||||
}
|
||||
@@ -606,27 +569,18 @@ export class SecretStorage extends EventEmitter {
|
||||
throw new Error("App returned unknown key from getSecretStorageKey!");
|
||||
}
|
||||
|
||||
switch (keys[keyId].algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
{
|
||||
const decryption = new global.Olm.PkDecryption();
|
||||
let pubkey;
|
||||
try {
|
||||
pubkey = decryption.init_with_private_key(privateKey);
|
||||
} catch (e) {
|
||||
decryption.free();
|
||||
throw new Error("getSecretStorageKey callback returned invalid key");
|
||||
}
|
||||
if (pubkey !== keys[keyId].pubkey) {
|
||||
decryption.free();
|
||||
throw new Error(
|
||||
"getSecretStorageKey callback returned incorrect key",
|
||||
);
|
||||
}
|
||||
return [keyId, decryption];
|
||||
}
|
||||
default:
|
||||
throw new Error("Unknown key type: " + keys[keyId].algorithm);
|
||||
if (keys[keyId].algorithm === SECRET_STORAGE_ALGORITHM_V1_AES) {
|
||||
const decryption = {
|
||||
encrypt: async function(secret) {
|
||||
return await encryptAES(secret, privateKey, name);
|
||||
},
|
||||
decrypt: async function(encInfo) {
|
||||
return await decryptAES(encInfo, privateKey, name);
|
||||
},
|
||||
};
|
||||
return [keyId, decryption];
|
||||
} else {
|
||||
throw new Error("Unknown key type: " + keys[keyId].algorithm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {getCrypto} from '../utils';
|
||||
import {decodeBase64, encodeBase64} from './olmlib';
|
||||
|
||||
const subtleCrypto = (typeof window !== "undefined" && window.crypto) ?
|
||||
(window.crypto.subtle || window.crypto.webkitSubtle) : null;
|
||||
|
||||
// salt for HKDF, with 8 bytes of zeros
|
||||
const zerosalt = new Uint8Array(8);
|
||||
|
||||
/**
|
||||
* encrypt a string in Node.js
|
||||
*
|
||||
* @param {string} data the plaintext to encrypt
|
||||
* @param {Uint8Array} key the encryption key to use
|
||||
* @param {string} name the name of the secret
|
||||
* @param {string} ivStr the initialization vector to use
|
||||
*/
|
||||
async function encryptNode(data, key, name, ivStr) {
|
||||
const crypto = getCrypto();
|
||||
if (!crypto) {
|
||||
throw new Error("No usable crypto implementation");
|
||||
}
|
||||
|
||||
let iv;
|
||||
if (ivStr) {
|
||||
iv = decodeBase64(ivStr);
|
||||
} else {
|
||||
iv = crypto.randomBytes(16);
|
||||
}
|
||||
|
||||
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
|
||||
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
||||
// of a single bit of iv is a price we have to pay.
|
||||
iv[8] &= 0x7f;
|
||||
|
||||
const [aesKey, hmacKey] = deriveKeysNode(key, name);
|
||||
|
||||
const cipher = crypto.createCipheriv("aes-256-ctr", aesKey, iv);
|
||||
const ciphertext = cipher.update(data, "utf-8", "base64")
|
||||
+ cipher.final("base64");
|
||||
|
||||
const hmac = crypto.createHmac("sha256", hmacKey)
|
||||
.update(ciphertext, "base64").digest("base64");
|
||||
|
||||
return {
|
||||
iv: encodeBase64(iv),
|
||||
ciphertext: ciphertext,
|
||||
mac: hmac,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* decrypt a string in Node.js
|
||||
*
|
||||
* @param {object} data the encrypted data
|
||||
* @param {string} data.ciphertext the ciphertext in base64
|
||||
* @param {string} data.iv the initialization vector in base64
|
||||
* @param {string} data.mac the HMAC in base64
|
||||
* @param {Uint8Array} key the encryption key to use
|
||||
* @param {string} name the name of the secret
|
||||
*/
|
||||
async function decryptNode(data, key, name) {
|
||||
const crypto = getCrypto();
|
||||
if (!crypto) {
|
||||
throw new Error("No usable crypto implementation");
|
||||
}
|
||||
|
||||
const [aesKey, hmacKey] = deriveKeysNode(key, name);
|
||||
|
||||
const hmac = crypto.createHmac("sha256", hmacKey)
|
||||
.update(data.ciphertext, "base64").digest("base64").replace(/=+$/g, '');
|
||||
|
||||
if (hmac !== data.mac.replace(/=+$/g, '')) {
|
||||
throw new Error(`Error decrypting secret ${name}: bad MAC`);
|
||||
}
|
||||
|
||||
const decipher = crypto.createDecipheriv(
|
||||
"aes-256-ctr", aesKey, decodeBase64(data.iv),
|
||||
);
|
||||
return decipher.update(data.ciphertext, "base64", "utf-8")
|
||||
+ decipher.final("utf-8");
|
||||
}
|
||||
|
||||
function deriveKeysNode(key, name) {
|
||||
const crypto = getCrypto();
|
||||
const prk = crypto.createHmac("sha256", zerosalt)
|
||||
.update(key).digest();
|
||||
|
||||
const b = Buffer.alloc(1, 1);
|
||||
const aesKey = crypto.createHmac("sha256", prk)
|
||||
.update(name, "utf-8").update(b).digest();
|
||||
b[0] = 2;
|
||||
const hmacKey = crypto.createHmac("sha256", prk)
|
||||
.update(aesKey).update(name, "utf-8").update(b).digest();
|
||||
|
||||
return [aesKey, hmacKey];
|
||||
}
|
||||
|
||||
/**
|
||||
* encrypt a string in Node.js
|
||||
*
|
||||
* @param {string} data the plaintext to encrypt
|
||||
* @param {Uint8Array} key the encryption key to use
|
||||
* @param {string} name the name of the secret
|
||||
* @param {string} ivStr the initialization vector to use
|
||||
*/
|
||||
async function encryptBrowser(data, key, name, ivStr) {
|
||||
let iv;
|
||||
if (ivStr) {
|
||||
iv = decodeBase64(ivStr);
|
||||
} else {
|
||||
iv = new Uint8Array(16);
|
||||
window.crypto.getRandomValues(iv);
|
||||
}
|
||||
|
||||
// clear bit 63 of the IV to stop us hitting the 64-bit counter boundary
|
||||
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
||||
// of a single bit of iv is a price we have to pay.
|
||||
iv[8] &= 0x7f;
|
||||
|
||||
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
|
||||
const encodedData = new TextEncoder().encode(data);
|
||||
|
||||
const ciphertext = await subtleCrypto.encrypt(
|
||||
{
|
||||
name: "AES-CTR",
|
||||
counter: iv,
|
||||
length: 64,
|
||||
},
|
||||
aesKey,
|
||||
encodedData,
|
||||
);
|
||||
|
||||
const hmac = await subtleCrypto.sign(
|
||||
{name: 'HMAC'},
|
||||
hmacKey,
|
||||
ciphertext,
|
||||
);
|
||||
|
||||
return {
|
||||
iv: encodeBase64(iv),
|
||||
ciphertext: encodeBase64(ciphertext),
|
||||
mac: encodeBase64(hmac),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* decrypt a string in the browser
|
||||
*
|
||||
* @param {object} data the encrypted data
|
||||
* @param {string} data.ciphertext the ciphertext in base64
|
||||
* @param {string} data.iv the initialization vector in base64
|
||||
* @param {string} data.mac the HMAC in base64
|
||||
* @param {Uint8Array} key the encryption key to use
|
||||
* @param {string} name the name of the secret
|
||||
*/
|
||||
async function decryptBrowser(data, key, name) {
|
||||
const [aesKey, hmacKey] = await deriveKeysBrowser(key, name);
|
||||
|
||||
const ciphertext = decodeBase64(data.ciphertext);
|
||||
|
||||
if (!await subtleCrypto.verify(
|
||||
{name: "HMAC"},
|
||||
hmacKey,
|
||||
decodeBase64(data.mac),
|
||||
ciphertext,
|
||||
)) {
|
||||
throw new Error(`Error decrypting secret ${name}: bad MAC`);
|
||||
}
|
||||
|
||||
const plaintext = await subtleCrypto.decrypt(
|
||||
{
|
||||
name: "AES-CTR",
|
||||
counter: decodeBase64(data.iv),
|
||||
length: 64,
|
||||
},
|
||||
aesKey,
|
||||
ciphertext,
|
||||
);
|
||||
|
||||
return new TextDecoder().decode(new Uint8Array(plaintext));
|
||||
}
|
||||
|
||||
async function deriveKeysBrowser(key, name) {
|
||||
const hkdfkey = await subtleCrypto.importKey(
|
||||
'raw',
|
||||
key,
|
||||
{name: "HKDF"},
|
||||
false,
|
||||
["deriveBits"],
|
||||
);
|
||||
const keybits = await subtleCrypto.deriveBits(
|
||||
{
|
||||
name: "HKDF",
|
||||
salt: zerosalt,
|
||||
info: (new TextEncoder().encode(name)),
|
||||
hash: "SHA-256",
|
||||
},
|
||||
hkdfkey,
|
||||
512,
|
||||
);
|
||||
|
||||
const aesKey = keybits.slice(0, 32);
|
||||
const hmacKey = keybits.slice(32);
|
||||
|
||||
const aesProm = subtleCrypto.importKey(
|
||||
'raw',
|
||||
aesKey,
|
||||
{name: 'AES-CTR'},
|
||||
false,
|
||||
['encrypt', 'decrypt'],
|
||||
);
|
||||
|
||||
const hmacProm = subtleCrypto.importKey(
|
||||
'raw',
|
||||
hmacKey,
|
||||
{
|
||||
name: 'HMAC',
|
||||
hash: {name: 'SHA-256'},
|
||||
},
|
||||
false,
|
||||
['sign', 'verify'],
|
||||
);
|
||||
|
||||
return await Promise.all([aesProm, hmacProm]);
|
||||
}
|
||||
|
||||
export function encryptAES(...args) {
|
||||
return subtleCrypto ? encryptBrowser(...args) : encryptNode(...args);
|
||||
}
|
||||
|
||||
export function decryptAES(...args) {
|
||||
return subtleCrypto ? decryptBrowser(...args) : decryptNode(...args);
|
||||
}
|
||||
|
||||
@@ -60,6 +60,15 @@ export class EncryptionAlgorithm {
|
||||
this._roomId = params.roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform any background tasks that can be done before a message is ready to
|
||||
* send, in order to speed up sending of the message.
|
||||
*
|
||||
* @param {module:models/room} room the room the event is in
|
||||
*/
|
||||
prepareToEncrypt(room) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a message event
|
||||
*
|
||||
|
||||
+517
-226
File diff suppressed because it is too large
Load Diff
@@ -264,6 +264,25 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
|
||||
*/
|
||||
OlmDecryption.prototype._decryptMessage = async function(
|
||||
theirDeviceIdentityKey, message,
|
||||
) {
|
||||
// This is a wrapper that serialises decryptions of prekey messages, because
|
||||
// otherwise we race between deciding we have no active sessions for the message
|
||||
// and creating a new one, which we can only do once because it removes the OTK.
|
||||
if (message.type !== 0) {
|
||||
// not a prekey message: we can safely just try & decrypt it
|
||||
return this._reallyDecryptMessage(theirDeviceIdentityKey, message);
|
||||
} else {
|
||||
const myPromise = this._olmDevice._olmPrekeyPromise.then(() => {
|
||||
return this._reallyDecryptMessage(theirDeviceIdentityKey, message);
|
||||
});
|
||||
// we want the error, but don't propagate it to the next decryption
|
||||
this._olmDevice._olmPrekeyPromise = myPromise.catch(() => {});
|
||||
return await myPromise;
|
||||
}
|
||||
};
|
||||
|
||||
OlmDecryption.prototype._reallyDecryptMessage = async function(
|
||||
theirDeviceIdentityKey, message,
|
||||
) {
|
||||
const sessionIds = await this._olmDevice.getSessionIdsForDevice(
|
||||
theirDeviceIdentityKey,
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {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';
|
||||
|
||||
// FIXME: these types should eventually go in a different file
|
||||
type Signatures = Record<string, Record<string, string>>;
|
||||
|
||||
interface DeviceKeys {
|
||||
algorithms: Array<string>;
|
||||
device_id: string; // eslint-disable-line camelcase
|
||||
user_id: string; // eslint-disable-line camelcase
|
||||
keys: Record<string, string>;
|
||||
signatures?: Signatures;
|
||||
}
|
||||
|
||||
interface OneTimeKey {
|
||||
key: string;
|
||||
fallback?: boolean;
|
||||
signatures?: Signatures;
|
||||
}
|
||||
|
||||
export const DEHYDRATION_ALGORITHM = "org.matrix.msc2697.v1.olm.libolm_pickle";
|
||||
|
||||
const oneweek = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
export class DehydrationManager {
|
||||
private inProgress = false;
|
||||
private timeoutId: any;
|
||||
private key: Uint8Array;
|
||||
private keyInfo: {[props: string]: any};
|
||||
private deviceDisplayName: string;
|
||||
constructor(private crypto) {
|
||||
this.getDehydrationKeyFromCache();
|
||||
}
|
||||
async getDehydrationKeyFromCache(): Promise<void> {
|
||||
return await this.crypto._cryptoStore.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this.crypto._cryptoStore.getSecretStorePrivateKey(
|
||||
txn,
|
||||
async (result) => {
|
||||
if (result) {
|
||||
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;
|
||||
this.deviceDisplayName = deviceDisplayName;
|
||||
const now = Date.now();
|
||||
const delay = Math.max(1, time + oneweek - now);
|
||||
this.timeoutId = global.setTimeout(
|
||||
this.dehydrateDevice.bind(this), delay,
|
||||
);
|
||||
}
|
||||
},
|
||||
"dehydration",
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** set the key, and queue periodic dehydration to the server in the background */
|
||||
async setKeyAndQueueDehydration(
|
||||
key: Uint8Array, keyInfo: {[props: string]: any} = {},
|
||||
deviceDisplayName: string = undefined,
|
||||
): Promise<void> {
|
||||
const matches = await this.setKey(key, keyInfo, deviceDisplayName);
|
||||
if (!matches) {
|
||||
// start dehydration in the background
|
||||
this.dehydrateDevice();
|
||||
}
|
||||
}
|
||||
|
||||
async setKey(
|
||||
key: Uint8Array, keyInfo: {[props: string]: any} = {},
|
||||
deviceDisplayName: string = undefined,
|
||||
): Promise<boolean> {
|
||||
if (!key) {
|
||||
// unsetting the key -- cancel any pending dehydration task
|
||||
if (this.timeoutId) {
|
||||
global.clearTimeout(this.timeoutId);
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
// clear storage
|
||||
await this.crypto._cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this.crypto._cryptoStore.storeSecretStorePrivateKey(
|
||||
txn, "dehydration", null,
|
||||
);
|
||||
},
|
||||
);
|
||||
this.key = undefined;
|
||||
this.keyInfo = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if it's the same key as before. If it's different,
|
||||
// dehydrate a new device. If it's the same, we can keep the same
|
||||
// device. (Assume that keyInfo and deviceDisplayName will be the
|
||||
// same if the key is the same.)
|
||||
let matches: boolean = this.key && key.length == this.key.length;
|
||||
for (let i = 0; matches && i < key.length; i++) {
|
||||
if (key[i] != this.key[i]) {
|
||||
matches = false;
|
||||
}
|
||||
}
|
||||
if (!matches) {
|
||||
this.key = key;
|
||||
this.keyInfo = keyInfo;
|
||||
this.deviceDisplayName = deviceDisplayName;
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
/** returns the device id of the newly created dehydrated device */
|
||||
async dehydrateDevice(): Promise<string> {
|
||||
if (this.inProgress) {
|
||||
logger.log("Dehydration already in progress -- not starting new dehydration");
|
||||
return;
|
||||
}
|
||||
this.inProgress = true;
|
||||
if (this.timeoutId) {
|
||||
global.clearTimeout(this.timeoutId);
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
try {
|
||||
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(
|
||||
'readwrite',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
this.crypto._cryptoStore.storeSecretStorePrivateKey(
|
||||
txn, "dehydration",
|
||||
{
|
||||
keyInfo: this.keyInfo,
|
||||
key,
|
||||
deviceDisplayName: this.deviceDisplayName,
|
||||
time: Date.now(),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
logger.log("Attempting to dehydrate device");
|
||||
|
||||
logger.log("Creating account");
|
||||
// create the account and all the necessary keys
|
||||
const account = new global.Olm.Account();
|
||||
account.create();
|
||||
const e2eKeys = JSON.parse(account.identity_keys());
|
||||
|
||||
const maxKeys = account.max_number_of_one_time_keys();
|
||||
// FIXME: generate in small batches?
|
||||
account.generate_one_time_keys(maxKeys / 2);
|
||||
account.generate_fallback_key();
|
||||
const otks: Record<string, string> = JSON.parse(account.one_time_keys());
|
||||
const fallbacks: Record<string, string> = JSON.parse(account.fallback_key());
|
||||
account.mark_keys_as_published();
|
||||
|
||||
// dehydrate the account and store it on the server
|
||||
const pickledAccount = account.pickle(new Uint8Array(this.key));
|
||||
|
||||
const deviceData: {[props: string]: any} = {
|
||||
algorithm: DEHYDRATION_ALGORITHM,
|
||||
account: pickledAccount,
|
||||
};
|
||||
if (this.keyInfo.passphrase) {
|
||||
deviceData.passphrase = this.keyInfo.passphrase;
|
||||
}
|
||||
|
||||
logger.log("Uploading account to server");
|
||||
const dehydrateResult = await this.crypto._baseApis._http.authedRequest(
|
||||
undefined,
|
||||
"PUT",
|
||||
"/dehydrated_device",
|
||||
undefined,
|
||||
{
|
||||
device_data: deviceData,
|
||||
initial_device_display_name: this.deviceDisplayName,
|
||||
},
|
||||
{
|
||||
prefix: "/_matrix/client/unstable/org.matrix.msc2697.v2",
|
||||
},
|
||||
);
|
||||
|
||||
// send the keys to the server
|
||||
const deviceId = dehydrateResult.device_id;
|
||||
logger.log("Preparing device keys", deviceId);
|
||||
const deviceKeys: DeviceKeys = {
|
||||
algorithms: this.crypto._supportedAlgorithms,
|
||||
device_id: deviceId,
|
||||
user_id: this.crypto._userId,
|
||||
keys: {
|
||||
[`ed25519:${deviceId}`]: e2eKeys.ed25519,
|
||||
[`curve25519:${deviceId}`]: e2eKeys.curve25519,
|
||||
},
|
||||
};
|
||||
const deviceSignature = account.sign(anotherjson.stringify(deviceKeys));
|
||||
deviceKeys.signatures = {
|
||||
[this.crypto._userId]: {
|
||||
[`ed25519:${deviceId}`]: deviceSignature,
|
||||
},
|
||||
};
|
||||
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 signature = account.sign(anotherjson.stringify(k));
|
||||
k.signatures = {
|
||||
[this.crypto._userId]: {
|
||||
[`ed25519:${deviceId}`]: signature,
|
||||
},
|
||||
};
|
||||
oneTimeKeys[`signed_curve25519:${keyId}`] = k;
|
||||
}
|
||||
|
||||
logger.log("Preparing fallback keys");
|
||||
const fallbackKeys = {};
|
||||
for (const [keyId, key] of Object.entries(fallbacks.curve25519)) {
|
||||
const k: OneTimeKey = {key, fallback: true};
|
||||
const signature = account.sign(anotherjson.stringify(k));
|
||||
k.signatures = {
|
||||
[this.crypto._userId]: {
|
||||
[`ed25519:${deviceId}`]: signature,
|
||||
},
|
||||
};
|
||||
fallbackKeys[`signed_curve25519:${keyId}`] = k;
|
||||
}
|
||||
|
||||
logger.log("Uploading keys to server");
|
||||
await this.crypto._baseApis._http.authedRequest(
|
||||
undefined,
|
||||
"POST",
|
||||
"/keys/upload/" + encodeURI(deviceId),
|
||||
undefined,
|
||||
{
|
||||
"device_keys": deviceKeys,
|
||||
"one_time_keys": oneTimeKeys,
|
||||
"org.matrix.msc2732.fallback_keys": fallbackKeys,
|
||||
},
|
||||
);
|
||||
logger.log("Done dehydrating");
|
||||
|
||||
// dehydrate again in a week
|
||||
this.timeoutId = global.setTimeout(
|
||||
this.dehydrateDevice.bind(this), oneweek,
|
||||
);
|
||||
|
||||
return deviceId;
|
||||
} finally {
|
||||
this.inProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private stop() {
|
||||
if (this.timeoutId) {
|
||||
global.clearTimeout(this.timeoutId);
|
||||
this.timeoutId = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
+1154
-330
File diff suppressed because it is too large
Load Diff
@@ -19,6 +19,8 @@ import {randomString} from '../randomstring';
|
||||
|
||||
const DEFAULT_ITERATIONS = 500000;
|
||||
|
||||
const DEFAULT_BITSIZE = 256;
|
||||
|
||||
export async function keyFromAuthData(authData, password) {
|
||||
if (!global.Olm) {
|
||||
throw new Error("Olm is not available");
|
||||
@@ -34,6 +36,7 @@ export async function keyFromAuthData(authData, password) {
|
||||
return await deriveKey(
|
||||
password, authData.private_key_salt,
|
||||
authData.private_key_iterations,
|
||||
authData.private_key_bits || DEFAULT_BITSIZE,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,12 +47,12 @@ export async function keyFromPassphrase(password) {
|
||||
|
||||
const salt = randomString(32);
|
||||
|
||||
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS);
|
||||
const key = await deriveKey(password, salt, DEFAULT_ITERATIONS, DEFAULT_BITSIZE);
|
||||
|
||||
return { key, salt, iterations: DEFAULT_ITERATIONS };
|
||||
}
|
||||
|
||||
export async function deriveKey(password, salt, iterations) {
|
||||
export async function deriveKey(password, salt, iterations, numBits = DEFAULT_BITSIZE) {
|
||||
const subtleCrypto = global.crypto.subtle;
|
||||
const TextEncoder = global.TextEncoder;
|
||||
if (!subtleCrypto || !TextEncoder) {
|
||||
@@ -73,7 +76,7 @@ export async function deriveKey(password, salt, iterations) {
|
||||
hash: 'SHA-512',
|
||||
},
|
||||
key,
|
||||
global.Olm.PRIVATE_KEY_LENGTH * 8,
|
||||
numBits,
|
||||
);
|
||||
|
||||
return new Uint8Array(keybits);
|
||||
|
||||
+169
-48
@@ -113,6 +113,57 @@ export async function encryptMessageForDevice(
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the existing olm sessions for the given devices, and the devices that
|
||||
* don't have olm sessions.
|
||||
*
|
||||
* @param {module:crypto/OlmDevice} olmDevice
|
||||
*
|
||||
* @param {module:base-apis~MatrixBaseApis} baseApis
|
||||
*
|
||||
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||
* map from userid to list of devices to ensure sessions for
|
||||
*
|
||||
* @return {Promise} resolves to an array. The first element of the array is a
|
||||
* a map of user IDs to arrays of deviceInfo, representing the devices that
|
||||
* don't have established olm sessions. The second element of the array is
|
||||
* a map from userId to deviceId to {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
export async function getExistingOlmSessions(
|
||||
olmDevice, baseApis, devicesByUser,
|
||||
) {
|
||||
const devicesWithoutSession = {};
|
||||
const sessions = {};
|
||||
|
||||
const promises = [];
|
||||
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
for (const deviceInfo of devices) {
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
promises.push((async () => {
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(
|
||||
key, true,
|
||||
);
|
||||
if (sessionId === null) {
|
||||
devicesWithoutSession[userId] = devicesWithoutSession[userId] || [];
|
||||
devicesWithoutSession[userId].push(deviceInfo);
|
||||
} else {
|
||||
sessions[userId] = sessions[userId] || {};
|
||||
sessions[userId][deviceId] = {
|
||||
device: deviceInfo,
|
||||
sessionId: sessionId,
|
||||
};
|
||||
}
|
||||
})());
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return [devicesWithoutSession, sessions];
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to make sure we have established olm sessions for the given devices.
|
||||
*
|
||||
@@ -123,63 +174,109 @@ export async function encryptMessageForDevice(
|
||||
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||
* map from userid to list of devices to ensure sessions for
|
||||
*
|
||||
* @param {boolean} force If true, establish a new session even if one already exists.
|
||||
* Optional.
|
||||
* @param {boolean} [force=false] If true, establish a new session even if one
|
||||
* already exists.
|
||||
*
|
||||
* @param {Number} [otkTimeout] The timeout in milliseconds when requesting
|
||||
* one-time keys for establishing new olm sessions.
|
||||
*
|
||||
* @param {Array} [failedServers] An array to fill with remote servers that
|
||||
* failed to respond to one-time-key requests.
|
||||
*
|
||||
* @param {Logger} [log] A possibly customised log
|
||||
*
|
||||
* @return {Promise} resolves once the sessions are complete, to
|
||||
* an Object mapping from userId to deviceId to
|
||||
* {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
export async function ensureOlmSessionsForDevices(
|
||||
olmDevice, baseApis, devicesByUser, force,
|
||||
olmDevice, baseApis, devicesByUser, force, otkTimeout, failedServers, log,
|
||||
) {
|
||||
if (typeof force === "number") {
|
||||
log = failedServers;
|
||||
failedServers = otkTimeout;
|
||||
otkTimeout = force;
|
||||
force = false;
|
||||
}
|
||||
if (!log) {
|
||||
log = logger;
|
||||
}
|
||||
|
||||
const devicesWithoutSession = [
|
||||
// [userId, deviceId], ...
|
||||
];
|
||||
const result = {};
|
||||
const resolveSession = {};
|
||||
|
||||
for (const userId in devicesByUser) {
|
||||
if (!devicesByUser.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
result[userId] = {};
|
||||
const devices = devicesByUser[userId];
|
||||
for (let j = 0; j < devices.length; j++) {
|
||||
const deviceInfo = devices[j];
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
// Mark all sessions this task intends to update as in progress. It is
|
||||
// important to do this for all devices this task cares about in a single
|
||||
// synchronous operation, as otherwise it is possible to have deadlocks
|
||||
// where multiple tasks wait indefinitely on another task to update some set
|
||||
// of common devices.
|
||||
for (const [, devices] of Object.entries(devicesByUser)) {
|
||||
for (const deviceInfo of devices) {
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
|
||||
if (key === olmDevice.deviceCurve25519Key) {
|
||||
// We don't start sessions with ourself, so there's no need to
|
||||
// mark it in progress.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!olmDevice._sessionsInProgress[key]) {
|
||||
// pre-emptively mark the session as in-progress to avoid race
|
||||
// conditions. If we find that we already have a session, then
|
||||
// we'll resolve
|
||||
olmDevice._sessionsInProgress[key] = new Promise(
|
||||
(resolve, reject) => {
|
||||
resolveSession[key] = {
|
||||
resolve: (...args) => {
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
resolve(...args);
|
||||
},
|
||||
reject: (...args) => {
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
reject(...args);
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
olmDevice._sessionsInProgress[key] = new Promise(resolve => {
|
||||
resolveSession[key] = (...args) => {
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
resolve(...args);
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
result[userId] = {};
|
||||
for (const deviceInfo of devices) {
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
|
||||
if (key === olmDevice.deviceCurve25519Key) {
|
||||
// We should never be trying to start a session with ourself.
|
||||
// Apart from talking to yourself being the first sign of madness,
|
||||
// olm sessions can't do this because they get confused when
|
||||
// they get a message and see that the 'other side' has started a
|
||||
// new chain when this side has an active sender chain.
|
||||
// If you see this message being logged in the wild, we should find
|
||||
// the thing that is trying to send Olm messages to itself and fix it.
|
||||
log.info("Attempted to start session with ourself! Ignoring");
|
||||
// We must fill in the section in the return value though, as callers
|
||||
// expect it to be there.
|
||||
result[userId][deviceId] = {
|
||||
device: deviceInfo,
|
||||
sessionId: null,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
const forWhom = `for ${key} (${userId}:${deviceId})`;
|
||||
const sessionId = await olmDevice.getSessionIdForDevice(
|
||||
key, resolveSession[key],
|
||||
key, resolveSession[key], log,
|
||||
);
|
||||
if (sessionId !== null && resolveSession[key]) {
|
||||
// we found a session, but we had marked the session as
|
||||
// in-progress, so unmark it and unblock anything that was
|
||||
// waiting
|
||||
delete olmDevice._sessionsInProgress[key];
|
||||
resolveSession[key].resolve();
|
||||
delete resolveSession[key];
|
||||
// in-progress, so resolve it now, which will unmark it and
|
||||
// unblock anything that was waiting
|
||||
resolveSession[key]();
|
||||
}
|
||||
if (sessionId === null || force) {
|
||||
if (force) {
|
||||
log.info(`Forcing new Olm session ${forWhom}`);
|
||||
} else {
|
||||
log.info(`Making new Olm session ${forWhom}`);
|
||||
}
|
||||
devicesWithoutSession.push([userId, deviceId]);
|
||||
}
|
||||
result[userId][deviceId] = {
|
||||
@@ -195,30 +292,41 @@ export async function ensureOlmSessionsForDevices(
|
||||
|
||||
const oneTimeKeyAlgorithm = "signed_curve25519";
|
||||
let res;
|
||||
let taskDetail = `one-time keys for ${devicesWithoutSession.length} devices`;
|
||||
try {
|
||||
log.debug(`Claiming ${taskDetail}`);
|
||||
res = await baseApis.claimOneTimeKeys(
|
||||
devicesWithoutSession, oneTimeKeyAlgorithm,
|
||||
devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout,
|
||||
);
|
||||
log.debug(`Claimed ${taskDetail}`);
|
||||
} catch (e) {
|
||||
for (const resolver of Object.values(resolveSession)) {
|
||||
resolver.resolve();
|
||||
resolver();
|
||||
}
|
||||
logger.log("failed to claim one-time keys", e, devicesWithoutSession);
|
||||
log.log(`Failed to claim ${taskDetail}`, e, devicesWithoutSession);
|
||||
throw e;
|
||||
}
|
||||
|
||||
const otk_res = res.one_time_keys || {};
|
||||
if (failedServers && "failures" in res) {
|
||||
failedServers.push(...Object.keys(res.failures));
|
||||
}
|
||||
|
||||
const otkResult = res.one_time_keys || {};
|
||||
const promises = [];
|
||||
for (const userId in devicesByUser) {
|
||||
if (!devicesByUser.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
const userRes = otk_res[userId] || {};
|
||||
const devices = devicesByUser[userId];
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
const userRes = otkResult[userId] || {};
|
||||
for (let j = 0; j < devices.length; j++) {
|
||||
const deviceInfo = devices[j];
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
|
||||
if (key === olmDevice.deviceCurve25519Key) {
|
||||
// We've already logged about this above. Skip here too
|
||||
// otherwise we'll log saying there are no one-time keys
|
||||
// which will be confusing.
|
||||
continue;
|
||||
}
|
||||
|
||||
if (result[userId][deviceId].sessionId && !force) {
|
||||
// we already have a result for this device
|
||||
continue;
|
||||
@@ -233,11 +341,12 @@ export async function ensureOlmSessionsForDevices(
|
||||
}
|
||||
|
||||
if (!oneTimeKey) {
|
||||
const msg = "No one-time keys (alg=" + oneTimeKeyAlgorithm +
|
||||
") for device " + userId + ":" + deviceId;
|
||||
logger.warn(msg);
|
||||
log.warn(
|
||||
`No one-time keys (alg=${oneTimeKeyAlgorithm}) ` +
|
||||
`for device ${userId}:${deviceId}`,
|
||||
);
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key].resolve();
|
||||
resolveSession[key]();
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -247,12 +356,12 @@ export async function ensureOlmSessionsForDevices(
|
||||
olmDevice, oneTimeKey, userId, deviceInfo,
|
||||
).then((sid) => {
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key].resolve(sid);
|
||||
resolveSession[key](sid);
|
||||
}
|
||||
result[userId][deviceId].sessionId = sid;
|
||||
}, (e) => {
|
||||
if (resolveSession[key]) {
|
||||
resolveSession[key].resolve();
|
||||
resolveSession[key]();
|
||||
}
|
||||
throw e;
|
||||
}),
|
||||
@@ -260,7 +369,10 @@ export async function ensureOlmSessionsForDevices(
|
||||
}
|
||||
}
|
||||
|
||||
taskDetail = `Olm sessions for ${promises.length} devices`;
|
||||
log.debug(`Starting ${taskDetail}`);
|
||||
await Promise.all(promises);
|
||||
log.debug(`Started ${taskDetail}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -407,6 +519,15 @@ export function encodeBase64(uint8Array) {
|
||||
return Buffer.from(uint8Array).toString("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a typed array of uint8 as unpadded base64.
|
||||
* @param {Uint8Array} uint8Array The data to encode.
|
||||
* @return {string} The unpadded base64.
|
||||
*/
|
||||
export function encodeUnpaddedBase64(uint8Array) {
|
||||
return encodeBase64(uint8Array).replace(/=+$/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a base64 string to a typed array of uint8.
|
||||
* @param {string} base64 The base64 to decode.
|
||||
|
||||
@@ -59,8 +59,8 @@ export function decodeRecoveryKey(recoverykey) {
|
||||
throw new Error("Incorrect length");
|
||||
}
|
||||
|
||||
return result.slice(
|
||||
return Uint8Array.from(result.slice(
|
||||
OLM_RECOVERY_KEY_PREFIX.length,
|
||||
OLM_RECOVERY_KEY_PREFIX.length + global.Olm.PRIVATE_KEY_LENGTH,
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ limitations under the License.
|
||||
import {logger} from '../../logger';
|
||||
import * as utils from "../../utils";
|
||||
|
||||
export const VERSION = 9;
|
||||
export const VERSION = 10;
|
||||
const PROFILE_TRANSACTIONS = false;
|
||||
|
||||
/**
|
||||
* Implementation of a CryptoStore which is backed by an existing
|
||||
@@ -34,6 +35,7 @@ export class Backend {
|
||||
*/
|
||||
constructor(db) {
|
||||
this._db = db;
|
||||
this._nextTxnId = 0;
|
||||
|
||||
// make sure we close the db on `onversionchange` - otherwise
|
||||
// attempts to delete the database will block (and subsequent
|
||||
@@ -131,7 +133,7 @@ export class Backend {
|
||||
|
||||
cursorReq.onsuccess = (ev) => {
|
||||
const cursor = ev.target.result;
|
||||
if(!cursor) {
|
||||
if (!cursor) {
|
||||
// no match found
|
||||
callback(null);
|
||||
return;
|
||||
@@ -203,6 +205,23 @@ export class Backend {
|
||||
return promiseifyTxn(txn).then(() => result);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Number} wantedState
|
||||
* @return {Promise<Array<*>>} All elements in a given state
|
||||
*/
|
||||
getAllOutgoingRoomKeyRequestsByState(wantedState) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
const index = store.index("state");
|
||||
const request = index.getAll(wantedState);
|
||||
|
||||
request.onsuccess = (ev) => resolve(ev.target.result);
|
||||
request.onerror = (ev) => reject(ev.target.error);
|
||||
});
|
||||
}
|
||||
|
||||
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
|
||||
let stateIndex = 0;
|
||||
const results = [];
|
||||
@@ -341,18 +360,39 @@ export class Backend {
|
||||
};
|
||||
}
|
||||
|
||||
getSecretStorePrivateKey(txn, func, type) {
|
||||
const objectStore = txn.objectStore("account");
|
||||
const getReq = objectStore.get(`ssss_cache:${type}`);
|
||||
getReq.onsuccess = function() {
|
||||
try {
|
||||
func(getReq.result || null);
|
||||
} catch (e) {
|
||||
abortWithException(txn, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
storeCrossSigningKeys(txn, keys) {
|
||||
const objectStore = txn.objectStore("account");
|
||||
objectStore.put(keys, "crossSigningKeys");
|
||||
}
|
||||
|
||||
storeSecretStorePrivateKey(txn, type, key) {
|
||||
const objectStore = txn.objectStore("account");
|
||||
objectStore.put(key, `ssss_cache:${type}`);
|
||||
}
|
||||
|
||||
// Olm Sessions
|
||||
|
||||
countEndToEndSessions(txn, func) {
|
||||
const objectStore = txn.objectStore("sessions");
|
||||
const countReq = objectStore.count();
|
||||
countReq.onsuccess = function() {
|
||||
func(countReq.result);
|
||||
try {
|
||||
func(countReq.result);
|
||||
} catch (e) {
|
||||
abortWithException(txn, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -402,16 +442,16 @@ export class Backend {
|
||||
const objectStore = txn.objectStore("sessions");
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function() {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
func(cursor.value);
|
||||
cursor.continue();
|
||||
} else {
|
||||
try {
|
||||
try {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
func(cursor.value);
|
||||
cursor.continue();
|
||||
} else {
|
||||
func(null);
|
||||
} catch (e) {
|
||||
abortWithException(txn, e);
|
||||
}
|
||||
} catch (e) {
|
||||
abortWithException(txn, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -719,10 +759,59 @@ export class Backend {
|
||||
}));
|
||||
}
|
||||
|
||||
doTxn(mode, stores, func) {
|
||||
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
|
||||
if (!txn) {
|
||||
txn = this._db.transaction(
|
||||
"shared_history_inbound_group_sessions", "readwrite",
|
||||
);
|
||||
}
|
||||
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
|
||||
const req = objectStore.get([roomId]);
|
||||
req.onsuccess = () => {
|
||||
const {sessions} = req.result || {sessions: []};
|
||||
sessions.push([senderKey, sessionId]);
|
||||
objectStore.put({roomId, sessions});
|
||||
};
|
||||
}
|
||||
|
||||
getSharedHistoryInboundGroupSessions(roomId, txn) {
|
||||
if (!txn) {
|
||||
txn = this._db.transaction(
|
||||
"shared_history_inbound_group_sessions", "readonly",
|
||||
);
|
||||
}
|
||||
const objectStore = txn.objectStore("shared_history_inbound_group_sessions");
|
||||
const req = objectStore.get([roomId]);
|
||||
return new Promise((resolve, reject) => {
|
||||
req.onsuccess = () => {
|
||||
const {sessions} = req.result || {sessions: []};
|
||||
resolve(sessions);
|
||||
};
|
||||
req.onerror = reject;
|
||||
});
|
||||
}
|
||||
|
||||
doTxn(mode, stores, func, log = logger) {
|
||||
let startTime;
|
||||
let description;
|
||||
if (PROFILE_TRANSACTIONS) {
|
||||
const txnId = this._nextTxnId++;
|
||||
startTime = Date.now();
|
||||
description = `${mode} crypto store transaction ${txnId} in ${stores}`;
|
||||
log.debug(`Starting ${description}`);
|
||||
}
|
||||
const txn = this._db.transaction(stores, mode);
|
||||
const promise = promiseifyTxn(txn);
|
||||
const result = func(txn);
|
||||
if (PROFILE_TRANSACTIONS) {
|
||||
promise.then(() => {
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
log.debug(`Finished ${description}, took ${elapsedTime} ms`);
|
||||
}, () => {
|
||||
const elapsedTime = Date.now() - startTime;
|
||||
log.error(`Failed ${description}, took ${elapsedTime} ms`);
|
||||
});
|
||||
}
|
||||
return promise.then(() => {
|
||||
return result;
|
||||
});
|
||||
@@ -777,6 +866,11 @@ export function upgradeDatabase(db, oldVersion) {
|
||||
keyPath: ["userId", "deviceId"],
|
||||
});
|
||||
}
|
||||
if (oldVersion < 10) {
|
||||
db.createObjectStore("shared_history_inbound_group_sessions", {
|
||||
keyPath: ["roomId"],
|
||||
});
|
||||
}
|
||||
// Expand as needed.
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ export class IndexedDBCryptoStore {
|
||||
this._indexedDB = indexedDB;
|
||||
this._dbName = dbName;
|
||||
this._backendPromise = null;
|
||||
this._backend = null;
|
||||
}
|
||||
|
||||
static exists(indexedDB, dbName) {
|
||||
@@ -56,10 +57,12 @@ export class IndexedDBCryptoStore {
|
||||
* Ensure the database exists and is up-to-date, or fall back to
|
||||
* a local storage or in-memory store.
|
||||
*
|
||||
* This must be called before the store can be used.
|
||||
*
|
||||
* @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend,
|
||||
* or a MemoryCryptoStore
|
||||
*/
|
||||
_connect() {
|
||||
startup() {
|
||||
if (this._backendPromise) {
|
||||
return this._backendPromise;
|
||||
}
|
||||
@@ -135,6 +138,8 @@ export class IndexedDBCryptoStore {
|
||||
);
|
||||
return new MemoryCryptoStore();
|
||||
}
|
||||
}).then(backend => {
|
||||
this._backend = backend;
|
||||
});
|
||||
|
||||
return this._backendPromise;
|
||||
@@ -189,9 +194,7 @@ export class IndexedDBCryptoStore {
|
||||
* same instance as passed in, or the existing one.
|
||||
*/
|
||||
getOrAddOutgoingRoomKeyRequest(request) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.getOrAddOutgoingRoomKeyRequest(request);
|
||||
});
|
||||
return this._backend.getOrAddOutgoingRoomKeyRequest(request);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -205,9 +208,7 @@ export class IndexedDBCryptoStore {
|
||||
* not found
|
||||
*/
|
||||
getOutgoingRoomKeyRequest(requestBody) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.getOutgoingRoomKeyRequest(requestBody);
|
||||
});
|
||||
return this._backend.getOutgoingRoomKeyRequest(requestBody);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,9 +222,18 @@ export class IndexedDBCryptoStore {
|
||||
* requests in those states, an arbitrary one is chosen.
|
||||
*/
|
||||
getOutgoingRoomKeyRequestByState(wantedStates) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.getOutgoingRoomKeyRequestByState(wantedStates);
|
||||
});
|
||||
return this._backend.getOutgoingRoomKeyRequestByState(wantedStates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for room key requests by state –
|
||||
* unlike above, return a list of all entries in one state.
|
||||
*
|
||||
* @param {Number} wantedState
|
||||
* @return {Promise<Array<*>>} Returns an array of requests in the given state
|
||||
*/
|
||||
getAllOutgoingRoomKeyRequestsByState(wantedState) {
|
||||
return this._backend.getAllOutgoingRoomKeyRequestsByState(wantedState);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -237,11 +247,9 @@ export class IndexedDBCryptoStore {
|
||||
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
|
||||
*/
|
||||
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.getOutgoingRoomKeyRequestsByTarget(
|
||||
userId, deviceId, wantedStates,
|
||||
);
|
||||
});
|
||||
return this._backend.getOutgoingRoomKeyRequestsByTarget(
|
||||
userId, deviceId, wantedStates,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -257,11 +265,9 @@ export class IndexedDBCryptoStore {
|
||||
* updated request, or null if no matching row was found
|
||||
*/
|
||||
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.updateOutgoingRoomKeyRequest(
|
||||
requestId, expectedState, updates,
|
||||
);
|
||||
});
|
||||
return this._backend.updateOutgoingRoomKeyRequest(
|
||||
requestId, expectedState, updates,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,9 +280,7 @@ export class IndexedDBCryptoStore {
|
||||
* @returns {Promise} resolves once the operation is completed
|
||||
*/
|
||||
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
|
||||
});
|
||||
return this._backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
|
||||
}
|
||||
|
||||
// Olm Account
|
||||
@@ -289,9 +293,7 @@ export class IndexedDBCryptoStore {
|
||||
* @param {function(string)} func Called with the account pickle
|
||||
*/
|
||||
getAccount(txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getAccount(txn, func);
|
||||
});
|
||||
this._backend.getAccount(txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,9 +304,7 @@ export class IndexedDBCryptoStore {
|
||||
* @param {string} newData The new account pickle to store.
|
||||
*/
|
||||
storeAccount(txn, newData) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeAccount(txn, newData);
|
||||
});
|
||||
this._backend.storeAccount(txn, newData);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -316,9 +316,16 @@ export class IndexedDBCryptoStore {
|
||||
* { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed
|
||||
*/
|
||||
getCrossSigningKeys(txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getCrossSigningKeys(txn, func);
|
||||
});
|
||||
this._backend.getCrossSigningKeys(txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
* @param {function(string)} func Called with the private key
|
||||
* @param {string} type A key type
|
||||
*/
|
||||
getSecretStorePrivateKey(txn, func, type) {
|
||||
this._backend.getSecretStorePrivateKey(txn, func, type);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -328,9 +335,18 @@ export class IndexedDBCryptoStore {
|
||||
* @param {string} keys keys object as getCrossSigningKeys()
|
||||
*/
|
||||
storeCrossSigningKeys(txn, keys) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeCrossSigningKeys(txn, keys);
|
||||
});
|
||||
this._backend.storeCrossSigningKeys(txn, keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the cross-signing private keys back to the store
|
||||
*
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
* @param {string} type The type of cross-signing private key to store
|
||||
* @param {string} key keys object as getCrossSigningKeys()
|
||||
*/
|
||||
storeSecretStorePrivateKey(txn, type, key) {
|
||||
this._backend.storeSecretStorePrivateKey(txn, type, key);
|
||||
}
|
||||
|
||||
// Olm sessions
|
||||
@@ -341,9 +357,7 @@ export class IndexedDBCryptoStore {
|
||||
* @param {function(int)} func Called with the count of sessions
|
||||
*/
|
||||
countEndToEndSessions(txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.countEndToEndSessions(txn, func);
|
||||
});
|
||||
this._backend.countEndToEndSessions(txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -359,9 +373,7 @@ export class IndexedDBCryptoStore {
|
||||
* a message.
|
||||
*/
|
||||
getEndToEndSession(deviceKey, sessionId, txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndSession(deviceKey, sessionId, txn, func);
|
||||
});
|
||||
this._backend.getEndToEndSession(deviceKey, sessionId, txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -376,9 +388,7 @@ export class IndexedDBCryptoStore {
|
||||
* a message.
|
||||
*/
|
||||
getEndToEndSessions(deviceKey, txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndSessions(deviceKey, txn, func);
|
||||
});
|
||||
this._backend.getEndToEndSessions(deviceKey, txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -389,9 +399,7 @@ export class IndexedDBCryptoStore {
|
||||
* and session keys.
|
||||
*/
|
||||
getAllEndToEndSessions(txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getAllEndToEndSessions(txn, func);
|
||||
});
|
||||
this._backend.getAllEndToEndSessions(txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -402,29 +410,21 @@ export class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndSession(
|
||||
deviceKey, sessionId, sessionInfo, txn,
|
||||
);
|
||||
});
|
||||
this._backend.storeEndToEndSession(
|
||||
deviceKey, sessionId, sessionInfo, txn,
|
||||
);
|
||||
}
|
||||
|
||||
storeEndToEndSessionProblem(deviceKey, type, fixed) {
|
||||
return this._backendPromise.then(async (backend) => {
|
||||
await backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
|
||||
});
|
||||
return this._backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
|
||||
}
|
||||
|
||||
getEndToEndSessionProblem(deviceKey, timestamp) {
|
||||
return this._backendPromise.then(async (backend) => {
|
||||
return await backend.getEndToEndSessionProblem(deviceKey, timestamp);
|
||||
});
|
||||
return this._backend.getEndToEndSessionProblem(deviceKey, timestamp);
|
||||
}
|
||||
|
||||
filterOutNotifiedErrorDevices(devices) {
|
||||
return this._backendPromise.then(async (backend) => {
|
||||
return await backend.filterOutNotifiedErrorDevices(devices);
|
||||
});
|
||||
return this._backend.filterOutNotifiedErrorDevices(devices);
|
||||
}
|
||||
|
||||
// Inbound group sessions
|
||||
@@ -439,11 +439,9 @@ export class IndexedDBCryptoStore {
|
||||
* to Base64 end-to-end session.
|
||||
*/
|
||||
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, txn, func,
|
||||
);
|
||||
});
|
||||
this._backend.getEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, txn, func,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -454,9 +452,7 @@ export class IndexedDBCryptoStore {
|
||||
* sessionData}, then once with null to indicate the end of the list.
|
||||
*/
|
||||
getAllEndToEndInboundGroupSessions(txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getAllEndToEndInboundGroupSessions(txn, func);
|
||||
});
|
||||
this._backend.getAllEndToEndInboundGroupSessions(txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -469,11 +465,9 @@ export class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
});
|
||||
this._backend.addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -486,21 +480,17 @@ export class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
});
|
||||
this._backend.storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
}
|
||||
|
||||
storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
});
|
||||
this._backend.storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
}
|
||||
|
||||
// End-to-end device tracking
|
||||
@@ -516,9 +506,7 @@ export class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndDeviceData(deviceData, txn) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndDeviceData(deviceData, txn);
|
||||
});
|
||||
this._backend.storeEndToEndDeviceData(deviceData, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -529,9 +517,7 @@ export class IndexedDBCryptoStore {
|
||||
* device data
|
||||
*/
|
||||
getEndToEndDeviceData(txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndDeviceData(txn, func);
|
||||
});
|
||||
this._backend.getEndToEndDeviceData(txn, func);
|
||||
}
|
||||
|
||||
// End to End Rooms
|
||||
@@ -543,9 +529,7 @@ export class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndRoom(roomId, roomInfo, txn) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndRoom(roomId, roomInfo, txn);
|
||||
});
|
||||
this._backend.storeEndToEndRoom(roomId, roomInfo, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -554,9 +538,7 @@ export class IndexedDBCryptoStore {
|
||||
* @param {function(Object)} func Function called with the end to end encrypted rooms
|
||||
*/
|
||||
getEndToEndRooms(txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndRooms(txn, func);
|
||||
});
|
||||
this._backend.getEndToEndRooms(txn, func);
|
||||
}
|
||||
|
||||
// session backups
|
||||
@@ -568,9 +550,7 @@ export class IndexedDBCryptoStore {
|
||||
* @returns {Promise} resolves to an array of inbound group sessions
|
||||
*/
|
||||
getSessionsNeedingBackup(limit) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.getSessionsNeedingBackup(limit);
|
||||
});
|
||||
return this._backend.getSessionsNeedingBackup(limit);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -579,9 +559,7 @@ export class IndexedDBCryptoStore {
|
||||
* @returns {Promise} resolves to the number of sessions
|
||||
*/
|
||||
countSessionsNeedingBackup(txn) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.countSessionsNeedingBackup(txn);
|
||||
});
|
||||
return this._backend.countSessionsNeedingBackup(txn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -591,9 +569,7 @@ export class IndexedDBCryptoStore {
|
||||
* @returns {Promise} resolves when the sessions are unmarked
|
||||
*/
|
||||
unmarkSessionsNeedingBackup(sessions, txn) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.unmarkSessionsNeedingBackup(sessions, txn);
|
||||
});
|
||||
return this._backend.unmarkSessionsNeedingBackup(sessions, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -603,9 +579,30 @@ export class IndexedDBCryptoStore {
|
||||
* @returns {Promise} resolves when the sessions are marked
|
||||
*/
|
||||
markSessionsNeedingBackup(sessions, txn) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.markSessionsNeedingBackup(sessions, txn);
|
||||
});
|
||||
return this._backend.markSessionsNeedingBackup(sessions, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a shared-history group session for a room.
|
||||
* @param {string} roomId The room that the key belongs to
|
||||
* @param {string} senderKey The sender's curve 25519 key
|
||||
* @param {string} sessionId The ID of the session
|
||||
* @param {*} txn An active transaction. See doTxn(). (optional)
|
||||
*/
|
||||
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId, txn) {
|
||||
this._backend.addSharedHistoryInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shared-history group session for a room.
|
||||
* @param {string} roomId The room that the key belongs to
|
||||
* @param {*} txn An active transaction. See doTxn(). (optional)
|
||||
* @returns {Promise} Resolves to an array of [senderKey, sessionId]
|
||||
*/
|
||||
getSharedHistoryInboundGroupSessions(roomId, txn) {
|
||||
return this._backend.getSharedHistoryInboundGroupSessions(roomId, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -622,6 +619,7 @@ export class IndexedDBCryptoStore {
|
||||
* @param {function(*)} func Function called with the
|
||||
* transaction object: an opaque object that should be passed
|
||||
* to store functions.
|
||||
* @param {Logger} [log] A possibly customised log
|
||||
* @return {Promise} Promise that resolves with the result of the `func`
|
||||
* when the transaction is complete. If the backend is
|
||||
* async (ie. the indexeddb backend) any of the callback
|
||||
@@ -629,10 +627,8 @@ export class IndexedDBCryptoStore {
|
||||
* reject with that exception. On synchronous backends, the
|
||||
* exception will propagate to the caller of the getFoo method.
|
||||
*/
|
||||
doTxn(mode, stores, func) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.doTxn(mode, stores, func);
|
||||
});
|
||||
doTxn(mode, stores, func, log) {
|
||||
return this._backend.doTxn(mode, stores, func, log);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,6 +637,8 @@ IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
|
||||
= 'inbound_group_sessions_withheld';
|
||||
IndexedDBCryptoStore.STORE_SHARED_HISTORY_INBOUND_GROUP_SESSIONS
|
||||
= 'shared_history_inbound_group_sessions';
|
||||
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
|
||||
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
|
||||
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
|
||||
|
||||
@@ -367,12 +367,23 @@ export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
func(keys);
|
||||
}
|
||||
|
||||
getSecretStorePrivateKey(txn, func, type) {
|
||||
const key = getJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`);
|
||||
func(key);
|
||||
}
|
||||
|
||||
storeCrossSigningKeys(txn, keys) {
|
||||
setJsonItem(
|
||||
this.store, KEY_CROSS_SIGNING_KEYS, keys,
|
||||
);
|
||||
}
|
||||
|
||||
storeSecretStorePrivateKey(txn, type, key) {
|
||||
setJsonItem(
|
||||
this.store, E2E_PREFIX + `ssss_cache.${type}`, key,
|
||||
);
|
||||
}
|
||||
|
||||
doTxn(mode, stores, func) {
|
||||
return Promise.resolve(func(null));
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ export class MemoryCryptoStore {
|
||||
this._outgoingRoomKeyRequests = [];
|
||||
this._account = null;
|
||||
this._crossSigningKeys = null;
|
||||
this._privateKeys = {};
|
||||
this._backupKeys = {};
|
||||
|
||||
// Map of {devicekey -> {sessionId -> session pickle}}
|
||||
this._sessions = {};
|
||||
@@ -49,6 +51,20 @@ export class MemoryCryptoStore {
|
||||
this._rooms = {};
|
||||
// Set of {senderCurve25519Key+'/'+sessionId}
|
||||
this._sessionsNeedingBackup = {};
|
||||
// roomId -> array of [senderKey, sessionId]
|
||||
this._sharedHistoryInboundGroupSessions = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the database exists and is up-to-date.
|
||||
*
|
||||
* This must be called before the store can be used.
|
||||
*
|
||||
* @return {Promise} resolves to the store.
|
||||
*/
|
||||
async startup() {
|
||||
// No startup work to do for the memory store.
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -152,6 +168,19 @@ export class MemoryCryptoStore {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Number} wantedState
|
||||
* @return {Promise<Array<*>>} All OutgoingRoomKeyRequests in state
|
||||
*/
|
||||
getAllOutgoingRoomKeyRequestsByState(wantedState) {
|
||||
return Promise.resolve(
|
||||
this._outgoingRoomKeyRequests.filter(
|
||||
(r) => r.state == wantedState,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
|
||||
const results = [];
|
||||
|
||||
@@ -243,10 +272,19 @@ export class MemoryCryptoStore {
|
||||
func(this._crossSigningKeys);
|
||||
}
|
||||
|
||||
getSecretStorePrivateKey(txn, func, type) {
|
||||
const result = this._privateKeys[type];
|
||||
return func(result || null);
|
||||
}
|
||||
|
||||
storeCrossSigningKeys(txn, keys) {
|
||||
this._crossSigningKeys = keys;
|
||||
}
|
||||
|
||||
storeSecretStorePrivateKey(txn, type, key) {
|
||||
this._privateKeys[type] = key;
|
||||
}
|
||||
|
||||
// Olm Sessions
|
||||
|
||||
countEndToEndSessions(txn, func) {
|
||||
@@ -431,6 +469,16 @@ export class MemoryCryptoStore {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
addSharedHistoryInboundGroupSession(roomId, senderKey, sessionId) {
|
||||
const sessions = this._sharedHistoryInboundGroupSessions[roomId] || [];
|
||||
sessions.push([senderKey, sessionId]);
|
||||
this._sharedHistoryInboundGroupSessions[roomId] = sessions;
|
||||
}
|
||||
|
||||
getSharedHistoryInboundGroupSessions(roomId) {
|
||||
return Promise.resolve(this._sharedHistoryInboundGroupSessions[roomId] || []);
|
||||
}
|
||||
|
||||
// Session key backups
|
||||
|
||||
doTxn(mode, stores, func) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -24,6 +25,7 @@ import {EventEmitter} from 'events';
|
||||
import {logger} from '../../logger';
|
||||
import {DeviceInfo} from '../deviceinfo';
|
||||
import {newTimeoutError} from "./Error";
|
||||
import {requestKeysDuringVerification} from "../CrossSigning";
|
||||
|
||||
const timeoutException = new Error("Verification timed out");
|
||||
|
||||
@@ -117,6 +119,11 @@ export class VerificationBase extends EventEmitter {
|
||||
if (this._done) {
|
||||
return Promise.reject(new Error("Verification is already done"));
|
||||
}
|
||||
const existingEvent = this.request.getEventFromOtherParty(type);
|
||||
if (existingEvent) {
|
||||
return Promise.resolve(existingEvent);
|
||||
}
|
||||
|
||||
this._expectedEvent = type;
|
||||
return new Promise((resolve, reject) => {
|
||||
this._resolveEvent = resolve;
|
||||
@@ -130,6 +137,8 @@ export class VerificationBase extends EventEmitter {
|
||||
|
||||
switchStartEvent(event) {
|
||||
if (this.canSwitchStartEvent(event)) {
|
||||
logger.log("Verification Base: switching verification start event",
|
||||
{restartingFlow: !!this._rejectEvent});
|
||||
if (this._rejectEvent) {
|
||||
const reject = this._rejectEvent;
|
||||
this._rejectEvent = undefined;
|
||||
@@ -184,11 +193,9 @@ export class VerificationBase extends EventEmitter {
|
||||
done() {
|
||||
this._endTimer(); // always kill the activity timer
|
||||
if (!this._done) {
|
||||
if (this._channel.needsDoneMessage) {
|
||||
// verification in DM requires a done message
|
||||
this._send("m.key.verification.done", {});
|
||||
}
|
||||
this.request.onVerifierFinished();
|
||||
this._resolve();
|
||||
return requestKeysDuringVerification(this._baseApis, this.userId, this.deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,6 +203,7 @@ export class VerificationBase extends EventEmitter {
|
||||
this._endTimer(); // always kill the activity timer
|
||||
if (!this._done) {
|
||||
this.cancelled = true;
|
||||
this.request.onVerifierCancelled();
|
||||
if (this.userId && this.deviceId) {
|
||||
// send a cancellation to the other user (if it wasn't
|
||||
// cancelled by the other user)
|
||||
@@ -278,7 +286,7 @@ export class VerificationBase extends EventEmitter {
|
||||
|
||||
for (const [keyId, keyInfo] of Object.entries(keys)) {
|
||||
const deviceId = keyId.split(':', 2)[1];
|
||||
const device = await this._baseApis.getStoredDevice(userId, deviceId);
|
||||
const device = this._baseApis.getStoredDevice(userId, deviceId);
|
||||
if (device) {
|
||||
await verifier(keyId, device, keyInfo);
|
||||
verifiedDevices.push(deviceId);
|
||||
|
||||
@@ -23,8 +23,10 @@ limitations under the License.
|
||||
import {VerificationBase as Base} from "./Base";
|
||||
import {
|
||||
newKeyMismatchError,
|
||||
newUserMismatchError,
|
||||
newUserCancelledError,
|
||||
} from './Error';
|
||||
import {encodeUnpaddedBase64, decodeBase64} from "../olmlib";
|
||||
import {logger} from '../../logger';
|
||||
|
||||
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
|
||||
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
|
||||
@@ -49,52 +51,51 @@ export class ReciprocateQRCode extends Base {
|
||||
"with this method yet.");
|
||||
}
|
||||
|
||||
const targetUserId = this.startEvent.getSender();
|
||||
if (!this.userId) {
|
||||
console.log("Asking to confirm user ID");
|
||||
this.userId = await new Promise((resolve, reject) => {
|
||||
this.emit("confirm_user_id", {
|
||||
userId: targetUserId,
|
||||
confirm: resolve, // takes a userId
|
||||
cancel: () => reject(newUserMismatchError()),
|
||||
});
|
||||
});
|
||||
} else if (targetUserId !== this.userId) {
|
||||
throw newUserMismatchError({
|
||||
expected: this.userId,
|
||||
actual: targetUserId,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.startEvent.getContent()['secret'] !== this.request.encodedSharedSecret) {
|
||||
const {qrCodeData} = this.request;
|
||||
// 1. check the secret
|
||||
if (this.startEvent.getContent()['secret'] !== qrCodeData.encodedSharedSecret) {
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
|
||||
// If we've gotten this far, verify the user's master cross signing key
|
||||
const xsignInfo = this._baseApis.getStoredCrossSigningForUser(this.userId);
|
||||
if (!xsignInfo) throw new Error("Missing cross signing info");
|
||||
|
||||
const masterKey = xsignInfo.getId("master");
|
||||
const masterKeyId = `ed25519:${masterKey}`;
|
||||
const keys = {[masterKeyId]: masterKey};
|
||||
|
||||
const devices = (await this._baseApis.getStoredDevicesForUser(this.userId)) || [];
|
||||
const targetDevice = devices.find(d => {
|
||||
return d.deviceId === this.request.targetDevice.deviceId;
|
||||
// 2. ask if other user shows shield as well
|
||||
await new Promise((resolve, reject) => {
|
||||
this.reciprocateQREvent = {
|
||||
confirm: resolve,
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
};
|
||||
this.emit("show_reciprocate_qr", this.reciprocateQREvent);
|
||||
});
|
||||
if (!targetDevice) throw new Error("Device not found, somehow");
|
||||
keys[`ed25519:${targetDevice.deviceId}`] = targetDevice.getFingerprint();
|
||||
|
||||
if (this.request.requestingUserId === this.request.receivingUserId) {
|
||||
delete keys[masterKeyId];
|
||||
// 3. determine key to sign / mark as trusted
|
||||
const keys = {};
|
||||
|
||||
switch (qrCodeData.mode) {
|
||||
case MODE_VERIFY_OTHER_USER: {
|
||||
// add master key to keys to be signed, only if we're not doing self-verification
|
||||
const masterKey = qrCodeData.otherUserMasterKey;
|
||||
keys[`ed25519:${masterKey}`] = masterKey;
|
||||
break;
|
||||
}
|
||||
case MODE_VERIFY_SELF_TRUSTED: {
|
||||
const deviceId = this.request.targetDevice.deviceId;
|
||||
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey;
|
||||
break;
|
||||
}
|
||||
case MODE_VERIFY_SELF_UNTRUSTED: {
|
||||
const masterKey = qrCodeData.myMasterKey;
|
||||
keys[`ed25519:${masterKey}`] = masterKey;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. sign the key (or mark own MSK as verified in case of MODE_VERIFY_SELF_TRUSTED)
|
||||
await this._verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
|
||||
// make sure the device has the expected keys
|
||||
const targetKey = keys[keyId];
|
||||
if (!targetKey) throw newKeyMismatchError();
|
||||
|
||||
if (keyInfo !== targetKey) {
|
||||
console.error("key ID from key info does not match");
|
||||
logger.error("key ID from key info does not match");
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
for (const deviceKeyId in device.keys) {
|
||||
@@ -102,12 +103,204 @@ export class ReciprocateQRCode extends Base {
|
||||
const deviceTargetKey = keys[deviceKeyId];
|
||||
if (!deviceTargetKey) throw newKeyMismatchError();
|
||||
if (device.keys[deviceKeyId] !== deviceTargetKey) {
|
||||
console.error("master key does not match");
|
||||
logger.error("master key does not match");
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise it is probably fine
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const CODE_VERSION = 0x02; // the version of binary QR codes we support
|
||||
const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
|
||||
const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us
|
||||
const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key
|
||||
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
|
||||
|
||||
export class QRCodeData {
|
||||
constructor(
|
||||
mode, sharedSecret, otherUserMasterKey,
|
||||
otherDeviceKey, myMasterKey, buffer,
|
||||
) {
|
||||
this._sharedSecret = sharedSecret;
|
||||
this._mode = mode;
|
||||
this._otherUserMasterKey = otherUserMasterKey;
|
||||
this._otherDeviceKey = otherDeviceKey;
|
||||
this._myMasterKey = myMasterKey;
|
||||
this._buffer = buffer;
|
||||
}
|
||||
|
||||
static async create(request, client) {
|
||||
const sharedSecret = QRCodeData._generateSharedSecret();
|
||||
const mode = QRCodeData._determineMode(request, client);
|
||||
let otherUserMasterKey = null;
|
||||
let otherDeviceKey = null;
|
||||
let myMasterKey = null;
|
||||
if (mode === MODE_VERIFY_OTHER_USER) {
|
||||
const otherUserCrossSigningInfo =
|
||||
client.getStoredCrossSigningForUser(request.otherUserId);
|
||||
otherUserMasterKey = otherUserCrossSigningInfo.getId("master");
|
||||
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
|
||||
otherDeviceKey = await QRCodeData._getOtherDeviceKey(request, client);
|
||||
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
|
||||
const myUserId = client.getUserId();
|
||||
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
|
||||
myMasterKey = myCrossSigningInfo.getId("master");
|
||||
}
|
||||
const qrData = QRCodeData._generateQrData(
|
||||
request, client, mode,
|
||||
sharedSecret,
|
||||
otherUserMasterKey,
|
||||
otherDeviceKey,
|
||||
myMasterKey,
|
||||
);
|
||||
const buffer = QRCodeData._generateBuffer(qrData);
|
||||
return new QRCodeData(mode, sharedSecret,
|
||||
otherUserMasterKey, otherDeviceKey, myMasterKey, buffer);
|
||||
}
|
||||
|
||||
get buffer() {
|
||||
return this._buffer;
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* only set when mode is MODE_VERIFY_SELF_TRUSTED
|
||||
* @return {string} device key of other party at time of generating QR code
|
||||
*/
|
||||
get otherDeviceKey() {
|
||||
return this._otherDeviceKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* only set when mode is MODE_VERIFY_OTHER_USER
|
||||
* @return {string} master key of other party at time of generating QR code
|
||||
*/
|
||||
get otherUserMasterKey() {
|
||||
return this._otherUserMasterKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* only set when mode is MODE_VERIFY_SELF_UNTRUSTED
|
||||
* @return {string} own master key at time of generating QR code
|
||||
*/
|
||||
get myMasterKey() {
|
||||
return this._myMasterKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* The unpadded base64 encoded shared secret.
|
||||
*/
|
||||
get encodedSharedSecret() {
|
||||
return this._sharedSecret;
|
||||
}
|
||||
|
||||
static _generateSharedSecret() {
|
||||
const secretBytes = new Uint8Array(11);
|
||||
global.crypto.getRandomValues(secretBytes);
|
||||
return encodeUnpaddedBase64(secretBytes);
|
||||
}
|
||||
|
||||
static async _getOtherDeviceKey(request, client) {
|
||||
const myUserId = client.getUserId();
|
||||
const otherDevice = request.targetDevice;
|
||||
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
|
||||
const device = client.getStoredDevice(myUserId, otherDeviceId);
|
||||
if (!device) {
|
||||
throw new Error("could not find device " + otherDeviceId);
|
||||
}
|
||||
const key = device.getFingerprint();
|
||||
return key;
|
||||
}
|
||||
|
||||
static _determineMode(request, client) {
|
||||
const myUserId = client.getUserId();
|
||||
const otherUserId = request.otherUserId;
|
||||
|
||||
let mode = MODE_VERIFY_OTHER_USER;
|
||||
if (myUserId === otherUserId) {
|
||||
// Mode changes depending on whether or not we trust the master cross signing key
|
||||
const myTrust = client.checkUserTrust(myUserId);
|
||||
if (myTrust.isCrossSigningVerified()) {
|
||||
mode = MODE_VERIFY_SELF_TRUSTED;
|
||||
} else {
|
||||
mode = MODE_VERIFY_SELF_UNTRUSTED;
|
||||
}
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
static _generateQrData(request, client, mode,
|
||||
encodedSharedSecret, otherUserMasterKey,
|
||||
otherDeviceKey, myMasterKey,
|
||||
) {
|
||||
const myUserId = client.getUserId();
|
||||
const transactionId = request.channel.transactionId;
|
||||
const qrData = {
|
||||
prefix: BINARY_PREFIX,
|
||||
version: CODE_VERSION,
|
||||
mode,
|
||||
transactionId,
|
||||
firstKeyB64: '', // worked out shortly
|
||||
secondKeyB64: '', // worked out shortly
|
||||
secretB64: encodedSharedSecret,
|
||||
};
|
||||
|
||||
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
|
||||
|
||||
if (mode === MODE_VERIFY_OTHER_USER) {
|
||||
// First key is our master cross signing key
|
||||
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
|
||||
// Second key is the other user's master cross signing key
|
||||
qrData.secondKeyB64 = otherUserMasterKey;
|
||||
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
|
||||
// First key is our master cross signing key
|
||||
qrData.firstKeyB64 = myCrossSigningInfo.getId("master");
|
||||
qrData.secondKeyB64 = otherDeviceKey;
|
||||
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
|
||||
// First key is our device's key
|
||||
qrData.firstKeyB64 = client.getDeviceEd25519Key();
|
||||
// Second key is what we think our master cross signing key is
|
||||
qrData.secondKeyB64 = myMasterKey;
|
||||
}
|
||||
return qrData;
|
||||
}
|
||||
|
||||
static _generateBuffer(qrData) {
|
||||
let buf = Buffer.alloc(0); // we'll concat our way through life
|
||||
|
||||
const appendByte = (b) => {
|
||||
const tmpBuf = Buffer.from([b]);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendInt = (i) => {
|
||||
const tmpBuf = Buffer.alloc(2);
|
||||
tmpBuf.writeInt16BE(i, 0);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendStr = (s, enc, withLengthPrefix = true) => {
|
||||
const tmpBuf = Buffer.from(s, enc);
|
||||
if (withLengthPrefix) appendInt(tmpBuf.byteLength);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendEncBase64 = (b64) => {
|
||||
const b = decodeBase64(b64);
|
||||
const tmpBuf = Buffer.from(b);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
|
||||
// Actually build the buffer for the QR code
|
||||
appendStr(qrData.prefix, "ascii", false);
|
||||
appendByte(qrData.version);
|
||||
appendByte(qrData.mode);
|
||||
appendStr(qrData.transactionId, "utf-8");
|
||||
appendEncBase64(qrData.firstKeyB64);
|
||||
appendEncBase64(qrData.secondKeyB64);
|
||||
appendEncBase64(qrData.secretB64);
|
||||
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,11 +175,33 @@ function calculateMAC(olmSAS, method) {
|
||||
};
|
||||
}
|
||||
|
||||
const calculateKeyAgreement = {
|
||||
"curve25519-hkdf-sha256": function(sas, olmSAS, bytes) {
|
||||
const ourInfo = `${sas._baseApis.getUserId()}|${sas._baseApis.deviceId}|`
|
||||
+ `${sas.ourSASPubKey}|`;
|
||||
const theirInfo = `${sas.userId}|${sas.deviceId}|${sas.theirSASPubKey}|`;
|
||||
const sasInfo =
|
||||
"MATRIX_KEY_VERIFICATION_SAS|"
|
||||
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
|
||||
+ sas._channel.transactionId;
|
||||
return olmSAS.generate_bytes(sasInfo, bytes);
|
||||
},
|
||||
"curve25519": function(sas, olmSAS, bytes) {
|
||||
const ourInfo = `${sas._baseApis.getUserId()}${sas._baseApis.deviceId}`;
|
||||
const theirInfo = `${sas.userId}${sas.deviceId}`;
|
||||
const sasInfo =
|
||||
"MATRIX_KEY_VERIFICATION_SAS"
|
||||
+ (sas.initiatedByMe ? ourInfo + theirInfo : theirInfo + ourInfo)
|
||||
+ sas._channel.transactionId;
|
||||
return olmSAS.generate_bytes(sasInfo, bytes);
|
||||
},
|
||||
};
|
||||
|
||||
/* lists of algorithms/methods that are supported. The key agreement, hashes,
|
||||
* and MAC lists should be sorted in order of preference (most preferred
|
||||
* first).
|
||||
*/
|
||||
const KEY_AGREEMENT_LIST = ["curve25519"];
|
||||
const KEY_AGREEMENT_LIST = ["curve25519-hkdf-sha256", "curve25519"];
|
||||
const HASHES_LIST = ["sha256"];
|
||||
const MAC_LIST = ["hkdf-hmac-sha256", "hmac-sha256"];
|
||||
const SAS_LIST = Object.keys(sasGenerators);
|
||||
@@ -291,12 +313,14 @@ export class SAS extends Base {
|
||||
if (typeof content.commitment !== "string") {
|
||||
throw newInvalidMessageError();
|
||||
}
|
||||
const keyAgreement = content.key_agreement_protocol;
|
||||
const macMethod = content.message_authentication_code;
|
||||
const hashCommitment = content.commitment;
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
try {
|
||||
this._send("m.key.verification.key", {
|
||||
key: olmSAS.get_pubkey(),
|
||||
this.ourSASPubKey = olmSAS.get_pubkey();
|
||||
await this._send("m.key.verification.key", {
|
||||
key: this.ourSASPubKey,
|
||||
});
|
||||
|
||||
|
||||
@@ -308,19 +332,20 @@ export class SAS extends Base {
|
||||
if (olmutil.sha256(commitmentStr) !== hashCommitment) {
|
||||
throw newMismatchedCommitmentError();
|
||||
}
|
||||
this.theirSASPubKey = content.key;
|
||||
olmSAS.set_their_key(content.key);
|
||||
|
||||
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this.userId + this.deviceId
|
||||
+ this._channel.transactionId;
|
||||
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
|
||||
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
|
||||
const verifySAS = new Promise((resolve, reject) => {
|
||||
this.sasEvent = {
|
||||
sas: generateSas(sasBytes, sasMethods),
|
||||
confirm: () => {
|
||||
this._sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
confirm: async () => {
|
||||
try {
|
||||
await this._sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
mismatch: () => reject(newMismatchedSASError()),
|
||||
@@ -377,7 +402,7 @@ export class SAS extends Base {
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
try {
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
|
||||
this._send("m.key.verification.accept", {
|
||||
await this._send("m.key.verification.accept", {
|
||||
key_agreement_protocol: keyAgreement,
|
||||
hash: hashMethod,
|
||||
message_authentication_code: macMethod,
|
||||
@@ -390,22 +415,24 @@ export class SAS extends Base {
|
||||
let e = await this._waitForEvent("m.key.verification.key");
|
||||
// FIXME: make sure event is properly formed
|
||||
content = e.getContent();
|
||||
this.theirSASPubKey = content.key;
|
||||
olmSAS.set_their_key(content.key);
|
||||
this._send("m.key.verification.key", {
|
||||
key: olmSAS.get_pubkey(),
|
||||
this.ourSASPubKey = olmSAS.get_pubkey();
|
||||
await this._send("m.key.verification.key", {
|
||||
key: this.ourSASPubKey,
|
||||
});
|
||||
|
||||
const sasInfo = "MATRIX_KEY_VERIFICATION_SAS"
|
||||
+ this.userId + this.deviceId
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this._channel.transactionId;
|
||||
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
|
||||
const sasBytes = calculateKeyAgreement[keyAgreement](this, olmSAS, 6);
|
||||
const verifySAS = new Promise((resolve, reject) => {
|
||||
this.sasEvent = {
|
||||
sas: generateSas(sasBytes, sasMethods),
|
||||
confirm: () => {
|
||||
this._sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
confirm: async () => {
|
||||
try {
|
||||
await this._sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
mismatch: () => reject(newMismatchedSASError()),
|
||||
@@ -461,7 +488,7 @@ export class SAS extends Base {
|
||||
keyList.sort().join(","),
|
||||
baseInfo + "KEY_IDS",
|
||||
);
|
||||
this._send("m.key.verification.mac", { mac, keys });
|
||||
return this._send("m.key.verification.mac", { mac, keys });
|
||||
}
|
||||
|
||||
async _checkMAC(olmSAS, content, method) {
|
||||
|
||||
@@ -44,11 +44,6 @@ export class InRoomChannel {
|
||||
this._requestEventId = null;
|
||||
}
|
||||
|
||||
/** Whether this channel needs m.key.verification.done messages to be sent after a successful verification */
|
||||
get needsDoneMessage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get receiveStartFromOtherDevices() {
|
||||
return true;
|
||||
}
|
||||
@@ -183,6 +178,11 @@ export class InRoomChannel {
|
||||
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
||||
*/
|
||||
async handleEvent(event, request, isLiveEvent) {
|
||||
// prevent processing the same event multiple times, as under
|
||||
// some circumstances Room.timeline can get emitted twice for the same event
|
||||
if (request.hasEventId(event.getId())) {
|
||||
return;
|
||||
}
|
||||
const type = InRoomChannel.getEventType(event);
|
||||
// do validations that need state (roomId, userId),
|
||||
// ignore if invalid
|
||||
|
||||
@@ -61,10 +61,6 @@ export class ToDeviceChannel {
|
||||
return this._deviceId;
|
||||
}
|
||||
|
||||
get needsDoneMessage() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static getEventType(event) {
|
||||
return event.getType();
|
||||
}
|
||||
@@ -183,7 +179,9 @@ export class ToDeviceChannel {
|
||||
const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
|
||||
// the request has picked a ready or start event, tell the other devices about it
|
||||
if (isAcceptingEvent && !wasStarted && isStarted && this._deviceId) {
|
||||
const nonChosenDevices = this._devices.filter(d => d !== this._deviceId);
|
||||
const nonChosenDevices = this._devices.filter(
|
||||
d => d !== this._deviceId && d !== this._client.getDeviceId(),
|
||||
);
|
||||
if (nonChosenDevices.length) {
|
||||
const message = this.completeContent({
|
||||
code: "m.accepted",
|
||||
@@ -360,4 +358,12 @@ export class ToDeviceRequests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getRequestsInProgress(userId) {
|
||||
const requestsByTxnId = this._requestsByUserId.get(userId);
|
||||
if (requestsByTxnId) {
|
||||
return Array.from(requestsByTxnId.values()).filter(r => r.pending);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,17 +23,19 @@ import {
|
||||
newUnexpectedMessageError,
|
||||
newUnknownMethodError,
|
||||
} from "../Error";
|
||||
import * as olmlib from "../../olmlib";
|
||||
import {QRCodeData, SCAN_QR_CODE_METHOD} from "../QRCode";
|
||||
|
||||
// How long after the event's timestamp that the request times out
|
||||
const TIMEOUT_FROM_EVENT_TS = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
// How long after we receive the event that the request times out
|
||||
const TIMEOUT_FROM_EVENT_RECEIPT = 2 * 60 * 1000; // 2 minutes
|
||||
|
||||
// the recommended amount of time before a verification request
|
||||
// should be (automatically) cancelled without user interaction
|
||||
// and ignored.
|
||||
const VERIFICATION_REQUEST_TIMEOUT = 10 * 60 * 1000; //10m
|
||||
// to avoid almost expired verification notifications
|
||||
// from showing a notification and almost immediately
|
||||
// disappearing, also ignore verification requests that
|
||||
// are this amount of time away from expiring.
|
||||
const VERIFICATION_REQUEST_MARGIN = 3 * 1000; //3s
|
||||
const VERIFICATION_REQUEST_MARGIN = 3 * 1000; // 3 seconds
|
||||
|
||||
|
||||
export const EVENT_PREFIX = "m.key.verification.";
|
||||
@@ -70,9 +72,19 @@ export class VerificationRequest extends EventEmitter {
|
||||
this._eventsByThem = new Map();
|
||||
this._observeOnly = false;
|
||||
this._timeoutTimer = null;
|
||||
this._sharedSecret = null; // used for QR codes
|
||||
this._accepting = false;
|
||||
this._declining = false;
|
||||
this._verifierHasFinished = false;
|
||||
this._cancelled = false;
|
||||
this._chosenMethod = null;
|
||||
// we keep a copy of the QR Code data (including other user master key) around
|
||||
// for QR reciprocate verification, to protect against
|
||||
// cross-signing identity reset between the .ready and .start event
|
||||
// and signing the wrong key after .start
|
||||
this._qrCodeData = null;
|
||||
|
||||
// The timestamp when we received the request event from the other side
|
||||
this._requestReceivedAt = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,12 +165,31 @@ export class VerificationRequest extends EventEmitter {
|
||||
return this._commonMethods;
|
||||
}
|
||||
|
||||
/** the method picked in the .start event */
|
||||
get chosenMethod() {
|
||||
return this._chosenMethod;
|
||||
}
|
||||
|
||||
calculateEventTimeout(event) {
|
||||
let effectiveExpiresAt = this.channel.getTimestamp(event)
|
||||
+ TIMEOUT_FROM_EVENT_TS;
|
||||
|
||||
if (this._requestReceivedAt && !this.initiatedByMe &&
|
||||
this.phase <= PHASE_REQUESTED
|
||||
) {
|
||||
const expiresAtByReceipt = this._requestReceivedAt
|
||||
+ TIMEOUT_FROM_EVENT_RECEIPT;
|
||||
effectiveExpiresAt = Math.min(effectiveExpiresAt, expiresAtByReceipt);
|
||||
}
|
||||
|
||||
return Math.max(0, effectiveExpiresAt - Date.now());
|
||||
}
|
||||
|
||||
/** The current remaining amount of ms before the request should be automatically cancelled */
|
||||
get timeout() {
|
||||
const requestEvent = this._getEventByEither(REQUEST_TYPE);
|
||||
if (requestEvent) {
|
||||
const elapsed = Date.now() - this.channel.getTimestamp(requestEvent);
|
||||
return Math.max(0, VERIFICATION_REQUEST_TIMEOUT - elapsed);
|
||||
return this.calculateEventTimeout(requestEvent);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -200,19 +231,34 @@ export class VerificationRequest extends EventEmitter {
|
||||
this._phase !== PHASE_CANCELLED;
|
||||
}
|
||||
|
||||
/** Only set after a .ready if the other party can scan a QR code */
|
||||
get qrCodeData() {
|
||||
return this._qrCodeData;
|
||||
}
|
||||
|
||||
/** Checks whether the other party supports a given verification method.
|
||||
* This is useful when setting up the QR code UI, as it is somewhat asymmetrical:
|
||||
* if the other party supports SCAN_QR, we should show a QR code in the UI, and vice versa.
|
||||
* For methods that need to be supported by both ends, use the `methods` property.
|
||||
* @param {string} method the method to check
|
||||
* @param {boolean} force to check even if the phase is not ready or started yet, internal usage
|
||||
* @return {bool} whether or not the other party said the supported the method */
|
||||
otherPartySupportsMethod(method) {
|
||||
if (!this.ready && !this.started) {
|
||||
otherPartySupportsMethod(method, force = false) {
|
||||
if (!force && !this.ready && !this.started) {
|
||||
return false;
|
||||
}
|
||||
const theirMethodEvent = this._eventsByThem.get(REQUEST_TYPE) ||
|
||||
this._eventsByThem.get(READY_TYPE);
|
||||
if (!theirMethodEvent) {
|
||||
// if we started straight away with .start event,
|
||||
// we are assuming that the other side will support the
|
||||
// chosen method, so return true for that.
|
||||
if (this.started && this.initiatedByMe) {
|
||||
const myStartEvent = this._eventsByUs.get(START_TYPE);
|
||||
const content = myStartEvent && myStartEvent.getContent();
|
||||
const myStartMethod = content && content.method;
|
||||
return method == myStartMethod;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const content = theirMethodEvent.getContent();
|
||||
@@ -276,6 +322,10 @@ export class VerificationRequest extends EventEmitter {
|
||||
return this.channel.userId;
|
||||
}
|
||||
|
||||
get isSelfVerification() {
|
||||
return this._client.getUserId() === this.otherUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The id of the user that cancelled the request,
|
||||
* only defined when phase is PHASE_CANCELLED
|
||||
@@ -305,14 +355,6 @@ export class VerificationRequest extends EventEmitter {
|
||||
return this._observeOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* The unpadded base64 encoded shared secret. Primarily used for QR code
|
||||
* verification.
|
||||
*/
|
||||
get encodedSharedSecret() {
|
||||
if (!this._sharedSecret) this._generateSharedSecret();
|
||||
return this._sharedSecret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets which device the verification should be started with
|
||||
@@ -359,6 +401,7 @@ export class VerificationRequest extends EventEmitter {
|
||||
if (!this._verifier) {
|
||||
throw newUnknownMethodError();
|
||||
}
|
||||
this._chosenMethod = method;
|
||||
}
|
||||
}
|
||||
return this._verifier;
|
||||
@@ -372,7 +415,6 @@ export class VerificationRequest extends EventEmitter {
|
||||
if (!this.observeOnly && this._phase === PHASE_UNSENT) {
|
||||
const methods = [...this._verificationMethods.keys()];
|
||||
await this.channel.send(REQUEST_TYPE, {methods});
|
||||
this._generateSharedSecret();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,16 +447,9 @@ export class VerificationRequest extends EventEmitter {
|
||||
this._accepting = true;
|
||||
this.emit("change");
|
||||
await this.channel.send(READY_TYPE, {methods});
|
||||
this._generateSharedSecret();
|
||||
}
|
||||
}
|
||||
|
||||
_generateSharedSecret() {
|
||||
const secretBytes = new Uint8Array(8);
|
||||
global.crypto.getRandomValues(secretBytes);
|
||||
this._sharedSecret = olmlib.encodeBase64(secretBytes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to listen for state changes until the callback returns true.
|
||||
* @param {Function} fn callback to evaluate whether the request is in the desired state.
|
||||
@@ -505,12 +540,12 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
|
||||
const ourDoneEvent = this._eventsByUs.get(DONE_TYPE);
|
||||
if (ourDoneEvent && phase() === PHASE_STARTED) {
|
||||
if (this._verifierHasFinished || (ourDoneEvent && phase() === PHASE_STARTED)) {
|
||||
transitions.push({phase: PHASE_DONE});
|
||||
}
|
||||
|
||||
const cancelEvent = this._getEventByEither(CANCEL_TYPE);
|
||||
if (cancelEvent && phase() !== PHASE_DONE) {
|
||||
if ((this._cancelled || cancelEvent) && phase() !== PHASE_DONE) {
|
||||
transitions.push({phase: PHASE_CANCELLED, event: cancelEvent});
|
||||
return transitions;
|
||||
}
|
||||
@@ -549,10 +584,80 @@ export class VerificationRequest extends EventEmitter {
|
||||
const {method} = event.getContent();
|
||||
if (!this._verifier && !this.observeOnly) {
|
||||
this._verifier = this._createVerifier(method, event);
|
||||
if (!this._verifier) {
|
||||
this.cancel({
|
||||
code: "m.unknown_method",
|
||||
reason: `Unknown method: ${method}`,
|
||||
});
|
||||
} else {
|
||||
this._chosenMethod = method;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_applyPhaseTransitions() {
|
||||
const transitions = this._calculatePhaseTransitions();
|
||||
const existingIdx = transitions.findIndex(t => t.phase === this.phase);
|
||||
// trim off phases we already went through, if any
|
||||
const newTransitions = transitions.slice(existingIdx + 1);
|
||||
// transition to all new phases
|
||||
for (const transition of newTransitions) {
|
||||
this._transitionToPhase(transition);
|
||||
}
|
||||
return newTransitions;
|
||||
}
|
||||
|
||||
_isWinningStartRace(newEvent) {
|
||||
if (newEvent.getType() !== START_TYPE) {
|
||||
return false;
|
||||
}
|
||||
const oldEvent = this._verifier.startEvent;
|
||||
|
||||
let oldRaceIdentifier;
|
||||
if (this.isSelfVerification) {
|
||||
// if the verifier does not have a startEvent,
|
||||
// it is because it's still sending and we are on the initator side
|
||||
// we know we are sending a .start event because we already
|
||||
// have a verifier (checked in calling method)
|
||||
if (oldEvent) {
|
||||
const oldContent = oldEvent.getContent();
|
||||
oldRaceIdentifier = oldContent && oldContent.from_device;
|
||||
} else {
|
||||
oldRaceIdentifier = this._client.getDeviceId();
|
||||
}
|
||||
} else {
|
||||
if (oldEvent) {
|
||||
oldRaceIdentifier = oldEvent.getSender();
|
||||
} else {
|
||||
oldRaceIdentifier = this._client.getUserId();
|
||||
}
|
||||
}
|
||||
|
||||
let newRaceIdentifier;
|
||||
if (this.isSelfVerification) {
|
||||
const newContent = newEvent.getContent();
|
||||
newRaceIdentifier = newContent && newContent.from_device;
|
||||
} else {
|
||||
newRaceIdentifier = newEvent.getSender();
|
||||
}
|
||||
return newRaceIdentifier < oldRaceIdentifier;
|
||||
}
|
||||
|
||||
hasEventId(eventId) {
|
||||
for (const event of this._eventsByUs.values()) {
|
||||
if (event.getId() === eventId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const event of this._eventsByThem.values()) {
|
||||
if (event.getId() === eventId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the state of the request and verifier in response to a key verification event.
|
||||
* @param {string} type the "symbolic" event type, as returned by the `getEventType` function on the channel.
|
||||
@@ -578,31 +683,32 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
// This assumes verification won't need to send an event with
|
||||
// the same type for the same party twice.
|
||||
// This is true for QR and SAS verification, and was
|
||||
// added here to prevent verification getting cancelled
|
||||
// when the server duplicates an event (https://github.com/matrix-org/synapse/issues/3365)
|
||||
const isDuplicateEvent = isSentByUs ?
|
||||
this._eventsByUs.has(type) :
|
||||
this._eventsByThem.has(type);
|
||||
if (isDuplicateEvent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldPhase = this.phase;
|
||||
this._addEvent(type, event, isSentByUs);
|
||||
|
||||
const transitions = this._calculatePhaseTransitions();
|
||||
const existingIdx = transitions.findIndex(t => t.phase === this.phase);
|
||||
// trim off phases we already went through, if any
|
||||
const newTransitions = transitions.slice(existingIdx + 1);
|
||||
// transition to all new phases
|
||||
for (const transition of newTransitions) {
|
||||
this._transitionToPhase(transition);
|
||||
}
|
||||
// this will create if needed the verifier so needs to happen before calling it
|
||||
const newTransitions = this._applyPhaseTransitions();
|
||||
try {
|
||||
// only pass events from the other side to the verifier,
|
||||
// no remote echos of our own events
|
||||
if (this._verifier && !this.observeOnly) {
|
||||
const oldEvent = this._verifier.startEvent;
|
||||
// if the verifier does not have a startEvent, it is because it's still sending and we are on the initator side
|
||||
const oldSender = oldEvent ?
|
||||
oldEvent.getSender() :
|
||||
this._client.getUserId();
|
||||
const newEventWinsRace = event.getSender() < oldSender;
|
||||
const newEventWinsRace = this._isWinningStartRace(event);
|
||||
if (this._verifier.canSwitchStartEvent(event) && newEventWinsRace) {
|
||||
this._verifier.switchStartEvent(event);
|
||||
} else if (!isRemoteEcho) {
|
||||
if (type === CANCEL_TYPE || (this._verifier.events
|
||||
if (type === CANCEL_TYPE || (this._verifier.events
|
||||
&& this._verifier.events.includes(type))) {
|
||||
this._verifier.handleEvent(event);
|
||||
}
|
||||
@@ -610,6 +716,20 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
|
||||
if (newTransitions.length) {
|
||||
// create QRCodeData if the other side can scan
|
||||
// important this happens before emitting a phase change,
|
||||
// so listeners can rely on it being there already
|
||||
// We only do this for live events because it is important that
|
||||
// we sign the keys that were in the QR code, and not the keys
|
||||
// we happen to have at some later point in time.
|
||||
if (isLiveEvent && newTransitions.some(t => t.phase === PHASE_READY)) {
|
||||
const shouldGenerateQrCode =
|
||||
this.otherPartySupportsMethod(SCAN_QR_CODE_METHOD, true);
|
||||
if (shouldGenerateQrCode) {
|
||||
this._qrCodeData = await QRCodeData.create(this, this._client);
|
||||
}
|
||||
}
|
||||
|
||||
const lastTransition = newTransitions[newTransitions.length - 1];
|
||||
const {phase} = lastTransition;
|
||||
|
||||
@@ -622,9 +742,11 @@ export class VerificationRequest extends EventEmitter {
|
||||
} finally {
|
||||
// log events we processed so we can see from rageshakes what events were added to a request
|
||||
logger.log(`Verification request ${this.channel.transactionId}: ` +
|
||||
`${type} event with ${JSON.stringify(event.getContent())} ` +
|
||||
`deviceId:${this.channel.deviceId} ` +
|
||||
`sender:${event.getSender()}, isSentByUs:${isSentByUs} ` +
|
||||
`${type} event with id:${event.getId()}, ` +
|
||||
`content:${JSON.stringify(event.getContent())} ` +
|
||||
`deviceId:${this.channel.deviceId}, ` +
|
||||
`sender:${event.getSender()}, isSentByUs:${isSentByUs}, ` +
|
||||
`isLiveEvent:${isLiveEvent}, isRemoteEcho:${isRemoteEcho}, ` +
|
||||
`phase:${oldPhase}=>${this.phase}, ` +
|
||||
`observeOnly:${wasObserveOnly}=>${this._observeOnly}`);
|
||||
}
|
||||
@@ -632,7 +754,7 @@ export class VerificationRequest extends EventEmitter {
|
||||
|
||||
_setupTimeout(phase) {
|
||||
const shouldTimeout = !this._timeoutTimer && !this.observeOnly &&
|
||||
phase === PHASE_REQUESTED && this.initiatedByMe;
|
||||
phase === PHASE_REQUESTED;
|
||||
|
||||
if (shouldTimeout) {
|
||||
this._timeoutTimer = setTimeout(this._cancelOnTimeout, this.timeout);
|
||||
@@ -651,7 +773,17 @@ export class VerificationRequest extends EventEmitter {
|
||||
|
||||
_cancelOnTimeout = () => {
|
||||
try {
|
||||
this.cancel({reason: "Other party didn't accept in time", code: "m.timeout"});
|
||||
if (this.initiatedByMe) {
|
||||
this.cancel({
|
||||
reason: "Other party didn't accept in time",
|
||||
code: "m.timeout",
|
||||
});
|
||||
} else {
|
||||
this.cancel({
|
||||
reason: "User didn't accept in time",
|
||||
code: "m.timeout",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Error while cancelling verification request", err);
|
||||
}
|
||||
@@ -668,7 +800,12 @@ export class VerificationRequest extends EventEmitter {
|
||||
|
||||
const isUnexpectedRequest = type === REQUEST_TYPE && this.phase !== PHASE_UNSENT;
|
||||
const isUnexpectedReady = type === READY_TYPE && this.phase !== PHASE_REQUESTED;
|
||||
if (isUnexpectedRequest || isUnexpectedReady) {
|
||||
// only if phase has passed from PHASE_UNSENT should we cancel, because events
|
||||
// are allowed to come in in any order (at least with InRoomChannel). So we only know
|
||||
// we're dealing with a valid request we should participate in once we've moved to PHASE_REQUESTED
|
||||
// before that, we could be looking at somebody elses verification request and we just
|
||||
// happen to be in the room
|
||||
if (this.phase !== PHASE_UNSENT && (isUnexpectedRequest || isUnexpectedReady)) {
|
||||
logger.warn(`Cancelling, unexpected ${type} verification ` +
|
||||
`event from ${event.getSender()}`);
|
||||
const reason = `Unexpected ${type} event in phase ${this.phase}`;
|
||||
@@ -683,16 +820,8 @@ export class VerificationRequest extends EventEmitter {
|
||||
if (!isLiveEvent) {
|
||||
this._observeOnly = true;
|
||||
}
|
||||
// a timestamp is not provided on all to_device events
|
||||
const timestamp = this.channel.getTimestamp(event);
|
||||
if (Number.isFinite(timestamp)) {
|
||||
const elapsed = Date.now() - timestamp;
|
||||
// don't allow interaction on old requests
|
||||
if (elapsed > (VERIFICATION_REQUEST_TIMEOUT - VERIFICATION_REQUEST_MARGIN) ||
|
||||
elapsed < -(VERIFICATION_REQUEST_TIMEOUT / 2)
|
||||
) {
|
||||
this._observeOnly = true;
|
||||
}
|
||||
if (this.calculateEventTimeout(event) < VERIFICATION_REQUEST_MARGIN) {
|
||||
this._observeOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -711,6 +840,8 @@ export class VerificationRequest extends EventEmitter {
|
||||
this._eventsByThem.delete(type);
|
||||
}
|
||||
}
|
||||
// also remember when we received the request event
|
||||
this._requestReceivedAt = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -750,4 +881,27 @@ export class VerificationRequest extends EventEmitter {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
onVerifierCancelled() {
|
||||
this._cancelled = true;
|
||||
// move to cancelled phase
|
||||
const newTransitions = this._applyPhaseTransitions();
|
||||
if (newTransitions.length) {
|
||||
this._setPhase(newTransitions[newTransitions.length - 1].phase);
|
||||
}
|
||||
}
|
||||
|
||||
onVerifierFinished() {
|
||||
this.channel.send("m.key.verification.done", {});
|
||||
this._verifierHasFinished = true;
|
||||
// move to .done phase
|
||||
const newTransitions = this._applyPhaseTransitions();
|
||||
if (newTransitions.length) {
|
||||
this._setPhase(newTransitions[newTransitions.length - 1].phase);
|
||||
}
|
||||
}
|
||||
|
||||
getEventFromOtherParty(type) {
|
||||
return this._eventsByThem.get(type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,3 +44,10 @@ InvalidCryptoStoreError.prototype = Object.create(Error.prototype, {
|
||||
},
|
||||
});
|
||||
Reflect.setPrototypeOf(InvalidCryptoStoreError, Error);
|
||||
|
||||
export class KeySignatureUploadError extends Error {
|
||||
constructor(message, value) {
|
||||
super(message);
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,8 +108,9 @@ FilterComponent.prototype._checkFields =
|
||||
}
|
||||
|
||||
const allowed_values = self[name];
|
||||
if (allowed_values) {
|
||||
if (!allowed_values.map(match_func)) {
|
||||
if (allowed_values && allowed_values.length > 0) {
|
||||
const anyMatch = allowed_values.some(match_func);
|
||||
if (!anyMatch) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
+5
-7
@@ -56,13 +56,6 @@ Filter.LAZY_LOADING_MESSAGES_FILTER = {
|
||||
lazy_load_members: true,
|
||||
};
|
||||
|
||||
Filter.LAZY_LOADING_SYNC_FILTER = {
|
||||
room: {
|
||||
state: Filter.LAZY_LOADING_MESSAGES_FILTER,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Get the ID of this filter on your homeserver (if known)
|
||||
* @return {?Number} The filter ID
|
||||
@@ -96,6 +89,7 @@ Filter.prototype.setDefinition = function(definition) {
|
||||
// "state": {
|
||||
// "types": ["m.room.*"],
|
||||
// "not_rooms": ["!726s6s6q:example.com"],
|
||||
// "lazy_load_members": true,
|
||||
// },
|
||||
// "timeline": {
|
||||
// "limit": 10,
|
||||
@@ -177,6 +171,10 @@ Filter.prototype.setTimelineLimit = function(limit) {
|
||||
setProp(this.definition, "room.timeline.limit", limit);
|
||||
};
|
||||
|
||||
Filter.prototype.setLazyLoadMembers = function(enabled) {
|
||||
setProp(this.definition, "room.state.lazy_load_members", !!enabled);
|
||||
};
|
||||
|
||||
/**
|
||||
* Control whether left rooms should be included in responses.
|
||||
* @param {boolean} includeLeave True to make rooms the user has left appear
|
||||
|
||||
+93
-16
@@ -47,6 +47,7 @@ export const PREFIX_UNSTABLE = "/_matrix/client/unstable";
|
||||
|
||||
/**
|
||||
* URI path for v1 of the the identity API
|
||||
* @deprecated Use v2.
|
||||
*/
|
||||
export const PREFIX_IDENTITY_V1 = "/_matrix/identity/api/v1";
|
||||
|
||||
@@ -270,11 +271,14 @@ MatrixHttpApi.prototype = {
|
||||
xhr.timeout_timer = callbacks.setTimeout(timeout_fn, 30000);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
let resp;
|
||||
switch (xhr.readyState) {
|
||||
case global.XMLHttpRequest.DONE:
|
||||
callbacks.clearTimeout(xhr.timeout_timer);
|
||||
var resp;
|
||||
try {
|
||||
if (xhr.status === 0) {
|
||||
throw new AbortError();
|
||||
}
|
||||
if (!xhr.responseText) {
|
||||
throw new Error('No response body.');
|
||||
}
|
||||
@@ -647,12 +651,10 @@ MatrixHttpApi.prototype = {
|
||||
|
||||
const self = this;
|
||||
if (this.opts.extraParams) {
|
||||
for (const key in this.opts.extraParams) {
|
||||
if (!this.opts.extraParams.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
queryParams[key] = this.opts.extraParams[key];
|
||||
}
|
||||
queryParams = {
|
||||
...queryParams,
|
||||
...this.opts.extraParams,
|
||||
};
|
||||
}
|
||||
|
||||
const headers = utils.extend({}, opts.headers || {});
|
||||
@@ -788,6 +790,17 @@ const requestCallback = function(
|
||||
userDefinedCallback = userDefinedCallback || function() {};
|
||||
|
||||
return function(err, response, body) {
|
||||
if (err) {
|
||||
// the unit tests use matrix-mock-request, which throw the string "aborted" when aborting a request.
|
||||
// See https://github.com/matrix-org/matrix-mock-request/blob/3276d0263a561b5b8326b47bae720578a2c7473a/src/index.js#L48
|
||||
const aborted = err.name === "AbortError" || err === "aborted";
|
||||
if (!aborted && !(err instanceof MatrixError)) {
|
||||
// browser-request just throws normal Error objects,
|
||||
// not `TypeError`s like fetch does. So just assume any
|
||||
// error is due to the connection.
|
||||
err = new ConnectionError("request failed", err);
|
||||
}
|
||||
}
|
||||
if (!err) {
|
||||
try {
|
||||
if (response.statusCode >= 400) {
|
||||
@@ -875,7 +888,7 @@ function getResponseContentType(response) {
|
||||
|
||||
try {
|
||||
return parseContentType(contentType);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
throw new Error(`Error parsing Content-Type '${contentType}': ${e}`);
|
||||
}
|
||||
}
|
||||
@@ -891,12 +904,76 @@ function getResponseContentType(response) {
|
||||
* @prop {Object} data The raw Matrix error JSON used to construct this object.
|
||||
* @prop {integer} httpStatus The numeric HTTP status code given
|
||||
*/
|
||||
export function MatrixError(errorJson) {
|
||||
errorJson = errorJson || {};
|
||||
this.errcode = errorJson.errcode;
|
||||
this.name = errorJson.errcode || "Unknown error code";
|
||||
this.message = errorJson.error || "Unknown message";
|
||||
this.data = errorJson;
|
||||
export class MatrixError extends Error {
|
||||
constructor(errorJson) {
|
||||
errorJson = errorJson || {};
|
||||
super(`MatrixError: ${errorJson.errcode}`);
|
||||
this.errcode = errorJson.errcode;
|
||||
this.name = errorJson.errcode || "Unknown error code";
|
||||
this.message = errorJson.error || "Unknown message";
|
||||
this.data = errorJson;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a ConnectionError. This is a JavaScript Error indicating
|
||||
* that a request failed because of some error with the connection, either
|
||||
* CORS was not correctly configured on the server, the server didn't response,
|
||||
* the request timed out, or the internet connection on the client side went down.
|
||||
* @constructor
|
||||
*/
|
||||
export class ConnectionError extends Error {
|
||||
constructor(message, cause = undefined) {
|
||||
super(message + (cause ? `: ${cause.message}` : ""));
|
||||
this._cause = cause;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return "ConnectionError";
|
||||
}
|
||||
|
||||
get cause() {
|
||||
return this._cause;
|
||||
}
|
||||
}
|
||||
|
||||
export class AbortError extends Error {
|
||||
constructor() {
|
||||
super("Operation aborted");
|
||||
}
|
||||
|
||||
get name() {
|
||||
return "AbortError";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries a network operation run in a callback.
|
||||
* @param {number} maxAttempts maximum attempts to try
|
||||
* @param {Function} callback callback that returns a promise of the network operation. If rejected with ConnectionError, it will be retried by calling the callback again.
|
||||
* @return {any} the result of the network operation
|
||||
* @throws {ConnectionError} If after maxAttempts the callback still throws ConnectionError
|
||||
*/
|
||||
export async function retryNetworkOperation(maxAttempts, callback) {
|
||||
let attempts = 0;
|
||||
let lastConnectionError = null;
|
||||
while (attempts < maxAttempts) {
|
||||
try {
|
||||
if (attempts > 0) {
|
||||
const timeout = 1000 * Math.pow(2, attempts);
|
||||
logger.log(`network operation failed ${attempts} times,` +
|
||||
` retrying in ${timeout}ms...`);
|
||||
await new Promise(r => setTimeout(r, timeout));
|
||||
}
|
||||
return await callback();
|
||||
} catch (err) {
|
||||
if (err instanceof ConnectionError) {
|
||||
attempts += 1;
|
||||
lastConnectionError = err;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastConnectionError;
|
||||
}
|
||||
MatrixError.prototype = Object.create(Error.prototype);
|
||||
MatrixError.prototype.constructor = MatrixError;
|
||||
|
||||
@@ -21,5 +21,13 @@ import request from "request";
|
||||
matrixcs.request(request);
|
||||
utils.runPolyfills();
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const crypto = require('crypto');
|
||||
utils.setCrypto(crypto);
|
||||
} catch (err) {
|
||||
console.log('nodejs was compiled without crypto support');
|
||||
}
|
||||
|
||||
export * from "./matrix";
|
||||
export default matrixcs;
|
||||
|
||||
+42
-13
@@ -143,11 +143,19 @@ InteractiveAuth.prototype = {
|
||||
this._resolveFunc = resolve;
|
||||
this._rejectFunc = reject;
|
||||
|
||||
// if we have no flows, try a request (we'll have
|
||||
// just a session ID in _data if resuming)
|
||||
if (!this._data.flows) {
|
||||
const hasFlows = this._data && this._data.flows;
|
||||
|
||||
// if we have no flows, try a request to acquire the flows
|
||||
if (!hasFlows) {
|
||||
if (this._busyChangedCallback) this._busyChangedCallback(true);
|
||||
this._doRequest(this._data).finally(() => {
|
||||
// use the existing sessionid, if one is present.
|
||||
let auth = null;
|
||||
if (this._data.session) {
|
||||
auth = {
|
||||
session: this._data.session,
|
||||
};
|
||||
}
|
||||
this._doRequest(auth).finally(() => {
|
||||
if (this._busyChangedCallback) this._busyChangedCallback(false);
|
||||
});
|
||||
} else {
|
||||
@@ -163,6 +171,8 @@ InteractiveAuth.prototype = {
|
||||
*/
|
||||
poll: async function() {
|
||||
if (!this._data.session) return;
|
||||
// likewise don't poll if there is no auth session in progress
|
||||
if (!this._resolveFunc) return;
|
||||
// if we currently have a request in flight, there's no point making
|
||||
// another just to check what the status is
|
||||
if (this._submitPromise) return;
|
||||
@@ -184,7 +194,11 @@ InteractiveAuth.prototype = {
|
||||
}
|
||||
authDict = {
|
||||
type: EMAIL_STAGE_TYPE,
|
||||
// TODO: Remove `threepid_creds` once servers support proper UIA
|
||||
// See https://github.com/matrix-org/synapse/issues/5665
|
||||
// See https://github.com/matrix-org/matrix-doc/issues/2220
|
||||
threepid_creds: creds,
|
||||
threepidCreds: creds,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -262,11 +276,16 @@ InteractiveAuth.prototype = {
|
||||
}
|
||||
}
|
||||
|
||||
// use the sessionid from the last request.
|
||||
const auth = {
|
||||
session: this._data.session,
|
||||
};
|
||||
utils.extend(auth, authData);
|
||||
// use the sessionid from the last request, if one is present.
|
||||
let auth;
|
||||
if (this._data.session) {
|
||||
auth = {
|
||||
session: this._data.session,
|
||||
};
|
||||
utils.extend(auth, authData);
|
||||
} else {
|
||||
auth = authData;
|
||||
}
|
||||
|
||||
try {
|
||||
// NB. the 'background' flag is deprecated by the busyChanged
|
||||
@@ -318,10 +337,12 @@ InteractiveAuth.prototype = {
|
||||
try {
|
||||
const result = await this._requestCallback(auth, background);
|
||||
this._resolveFunc(result);
|
||||
this._resolveFunc = null;
|
||||
this._rejectFunc = null;
|
||||
} catch (error) {
|
||||
// sometimes UI auth errors don't come with flows
|
||||
const errorFlows = error.data ? error.data.flows : null;
|
||||
const haveFlows = Boolean(this._data.flows) || Boolean(errorFlows);
|
||||
const haveFlows = this._data.flows || Boolean(errorFlows);
|
||||
if (error.httpStatus !== 401 || !error.data || !haveFlows) {
|
||||
// doesn't look like an interactive-auth failure.
|
||||
if (!background) {
|
||||
@@ -347,7 +368,13 @@ InteractiveAuth.prototype = {
|
||||
error.data.session = this._data.session;
|
||||
}
|
||||
this._data = error.data;
|
||||
this._startNextAuthStage();
|
||||
try {
|
||||
this._startNextAuthStage();
|
||||
} catch (e) {
|
||||
this._rejectFunc(e);
|
||||
this._resolveFunc = null;
|
||||
this._rejectFunc = null;
|
||||
}
|
||||
|
||||
if (
|
||||
!this._emailSid &&
|
||||
@@ -379,8 +406,10 @@ InteractiveAuth.prototype = {
|
||||
// (or not being registered, depending on what we're trying
|
||||
// to do) or it could be a network failure. Either way, pass
|
||||
// the failure up as the user can't complete auth if we can't
|
||||
// send the email, foe whatever reason.
|
||||
// send the email, for whatever reason.
|
||||
this._rejectFunc(e);
|
||||
this._resolveFunc = null;
|
||||
this._rejectFunc = null;
|
||||
} finally {
|
||||
this._requestingEmailToken = false;
|
||||
}
|
||||
@@ -408,7 +437,7 @@ InteractiveAuth.prototype = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._data.errcode || this._data.error) {
|
||||
if (this._data && this._data.errcode || this._data.error) {
|
||||
this._stateUpdatedCallback(nextStage, {
|
||||
errcode: this._data.errcode || "",
|
||||
error: this._data.error || "",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
Copyright 2018 André Jaenisch
|
||||
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.
|
||||
@@ -19,7 +19,7 @@ limitations under the License.
|
||||
* @module logger
|
||||
*/
|
||||
|
||||
import log from "loglevel";
|
||||
import log, { Logger } from "loglevel";
|
||||
|
||||
// This is to demonstrate, that you can use any namespace you want.
|
||||
// Namespaces allow you to turn on/off the logging for specific parts of the
|
||||
@@ -36,15 +36,22 @@ const DEFAULT_NAMESPACE = "matrix";
|
||||
// when logging so we always get the current value of console methods.
|
||||
log.methodFactory = function(methodName, logLevel, loggerName) {
|
||||
return function(...args) {
|
||||
/* eslint-disable @babel/no-invalid-this */
|
||||
if (this.prefix) {
|
||||
args.unshift(this.prefix);
|
||||
}
|
||||
/* eslint-enable @babel/no-invalid-this */
|
||||
const supportedByConsole = methodName === "error" ||
|
||||
methodName === "warn" ||
|
||||
methodName === "trace" ||
|
||||
methodName === "info";
|
||||
/* eslint-disable no-console */
|
||||
if (supportedByConsole) {
|
||||
return console[methodName](...args);
|
||||
} else {
|
||||
return console.log(...args);
|
||||
}
|
||||
/* eslint-enable no-console */
|
||||
};
|
||||
};
|
||||
|
||||
@@ -52,6 +59,30 @@ log.methodFactory = function(methodName, logLevel, loggerName) {
|
||||
* Drop-in replacement for <code>console</code> using {@link https://www.npmjs.com/package/loglevel|loglevel}.
|
||||
* Can be tailored down to specific use cases if needed.
|
||||
*/
|
||||
export const logger = log.getLogger(DEFAULT_NAMESPACE);
|
||||
export const logger: PrefixedLogger = log.getLogger(DEFAULT_NAMESPACE);
|
||||
logger.setLevel(log.levels.DEBUG);
|
||||
|
||||
interface PrefixedLogger extends Logger {
|
||||
withPrefix?: (prefix: string) => PrefixedLogger;
|
||||
prefix?: string;
|
||||
}
|
||||
|
||||
function extendLogger(logger: PrefixedLogger) {
|
||||
logger.withPrefix = function(prefix: string): PrefixedLogger {
|
||||
const existingPrefix = this.prefix || "";
|
||||
return getPrefixedLogger(existingPrefix + prefix);
|
||||
};
|
||||
}
|
||||
|
||||
extendLogger(logger);
|
||||
|
||||
function getPrefixedLogger(prefix): PrefixedLogger {
|
||||
const prefixLogger: PrefixedLogger = log.getLogger(`${DEFAULT_NAMESPACE}-${prefix}`);
|
||||
if (prefixLogger.prefix !== prefix) {
|
||||
// Only do this setup work the first time through, as loggers are saved by name.
|
||||
extendLogger(prefixLogger);
|
||||
prefixLogger.prefix = prefix;
|
||||
prefixLogger.setLevel(log.levels.DEBUG);
|
||||
}
|
||||
return prefixLogger;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2019, 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -16,8 +16,15 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type Request from "request";
|
||||
|
||||
import {MemoryCryptoStore} from "./crypto/store/memory-crypto-store";
|
||||
import {LocalStorageCryptoStore} from "./crypto/store/localStorage-crypto-store";
|
||||
import {IndexedDBCryptoStore} from "./crypto/store/indexeddb-crypto-store";
|
||||
import {MemoryStore} from "./store/memory";
|
||||
import {StubStore} from "./store/stub";
|
||||
import {LocalIndexedDBStoreBackend} from "./store/indexeddb-local-backend";
|
||||
import {RemoteIndexedDBStoreBackend} from "./store/indexeddb-remote-backend";
|
||||
import {MatrixScheduler} from "./scheduler";
|
||||
import {MatrixClient} from "./client";
|
||||
|
||||
@@ -45,10 +52,9 @@ export * from "./store/session/webstorage";
|
||||
export * from "./crypto/store/memory-crypto-store";
|
||||
export * from "./crypto/store/indexeddb-crypto-store";
|
||||
export * from "./content-repo";
|
||||
export const ContentHelpers = import("./content-helpers");
|
||||
export * as ContentHelpers from "./content-helpers";
|
||||
export {
|
||||
createNewMatrixCall,
|
||||
setAudioOutput as setMatrixCallAudioOutput,
|
||||
setAudioInput as setMatrixCallAudioInput,
|
||||
setVideoInput as setMatrixCallVideoInput,
|
||||
} from "./webrtc/call";
|
||||
@@ -89,6 +95,10 @@ export function wrapRequest(wrapper) {
|
||||
};
|
||||
}
|
||||
|
||||
type Store =
|
||||
StubStore | MemoryStore | LocalIndexedDBStoreBackend | RemoteIndexedDBStoreBackend;
|
||||
|
||||
type CryptoStore = MemoryCryptoStore | LocalStorageCryptoStore | IndexedDBCryptoStore;
|
||||
|
||||
let cryptoStoreFactory = () => new MemoryCryptoStore;
|
||||
|
||||
@@ -102,6 +112,82 @@ export function setCryptoStoreFactory(fac) {
|
||||
cryptoStoreFactory = fac;
|
||||
}
|
||||
|
||||
export interface ICreateClientOpts {
|
||||
baseUrl: string;
|
||||
idBaseUrl?: string;
|
||||
store?: Store;
|
||||
cryptoStore?: CryptoStore;
|
||||
scheduler?: MatrixScheduler;
|
||||
request?: Request;
|
||||
userId?: string;
|
||||
deviceId?: string;
|
||||
accessToken?: string;
|
||||
identityServer?: any;
|
||||
localTimeoutMs?: number;
|
||||
useAuthorizationHeader?: boolean;
|
||||
timelineSupport?: boolean;
|
||||
queryParams?: Record<string, unknown>;
|
||||
deviceToImport?: {
|
||||
olmDevice: {
|
||||
pickledAccount: string;
|
||||
sessions: Array<Record<string, any>>;
|
||||
pickleKey: string;
|
||||
};
|
||||
userId: string;
|
||||
deviceId: string;
|
||||
};
|
||||
pickleKey?: string;
|
||||
sessionStore?: any;
|
||||
unstableClientRelationAggregation?: boolean;
|
||||
verificationMethods?: Array<any>;
|
||||
forceTURN?: boolean;
|
||||
iceCandidatePoolSize?: number,
|
||||
supportsCallTransfer?: boolean,
|
||||
fallbackICEServerAllowed?: boolean;
|
||||
cryptoCallbacks?: ICryptoCallbacks;
|
||||
}
|
||||
|
||||
export interface ICryptoCallbacks {
|
||||
getCrossSigningKey?: (keyType: string, pubKey: Uint8Array) => Promise<Uint8Array>;
|
||||
saveCrossSigningKeys?: (keys: Record<string, Uint8Array>) => void;
|
||||
shouldUpgradeDeviceVerifications?: (
|
||||
users: Record<string, any>
|
||||
) => Promise<string[]>;
|
||||
getSecretStorageKey?: (
|
||||
keys: {keys: Record<string, ISecretStorageKeyInfo>}, name: string
|
||||
) => Promise<[string, Uint8Array] | null>;
|
||||
cacheSecretStorageKey?: (
|
||||
keyId: string, keyInfo: ISecretStorageKeyInfo, key: Uint8Array
|
||||
) => void;
|
||||
onSecretRequested?: (
|
||||
userId: string, deviceId: string,
|
||||
requestId: string, secretName: string, deviceTrust: IDeviceTrustLevel
|
||||
) => Promise<string>;
|
||||
getDehydrationKey?: (
|
||||
keyInfo: ISecretStorageKeyInfo,
|
||||
checkFunc: (Uint8Array) => void,
|
||||
) => Promise<Uint8Array>;
|
||||
}
|
||||
|
||||
// TODO: Move this to `SecretStorage` once converted
|
||||
export interface ISecretStorageKeyInfo {
|
||||
passphrase?: {
|
||||
algorithm: "m.pbkdf2";
|
||||
iterations: number;
|
||||
salt: string;
|
||||
};
|
||||
iv?: string;
|
||||
mac?: string;
|
||||
}
|
||||
|
||||
// TODO: Move this to `CrossSigning` once converted
|
||||
export interface IDeviceTrustLevel {
|
||||
isVerified(): boolean;
|
||||
isCrossSigningVerified(): boolean;
|
||||
isLocallyVerified(): boolean;
|
||||
isTofu(): boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a Matrix Client. Similar to {@link module:client.MatrixClient}
|
||||
* except that the 'request', 'store' and 'scheduler' dependencies are satisfied.
|
||||
@@ -125,10 +211,10 @@ export function setCryptoStoreFactory(fac) {
|
||||
* @see {@link module:client.MatrixClient} for the full list of options for
|
||||
* <code>opts</code>.
|
||||
*/
|
||||
export function createClient(opts) {
|
||||
export function createClient(opts: ICreateClientOpts | string) {
|
||||
if (typeof opts === "string") {
|
||||
opts = {
|
||||
"baseUrl": opts,
|
||||
"baseUrl": opts as string,
|
||||
};
|
||||
}
|
||||
opts.request = opts.request || requestInstance;
|
||||
@@ -167,7 +253,7 @@ export function createClient(opts) {
|
||||
* @param {requestCallback} callback The request callback.
|
||||
*/
|
||||
|
||||
/**
|
||||
/**
|
||||
* The request callback interface for performing HTTP requests. This matches the
|
||||
* API for the {@link https://github.com/request/request#requestoptions-callback|
|
||||
* request NPM module}. The SDK will implement a callback which meets this
|
||||
@@ -647,7 +647,8 @@ EventTimelineSet.prototype.compareEventOrdering = function(eventId1, eventId2) {
|
||||
if (timeline1 === timeline2) {
|
||||
// both events are in the same timeline - figure out their
|
||||
// relative indices
|
||||
let idx1, idx2;
|
||||
let idx1;
|
||||
let idx2;
|
||||
const events = timeline1.getEvents();
|
||||
for (let idx = 0; idx < events.length &&
|
||||
(idx1 === undefined || idx2 === undefined); idx++) {
|
||||
@@ -805,7 +806,7 @@ EventTimelineSet.prototype.aggregateRelations = function(event) {
|
||||
this.room,
|
||||
);
|
||||
isNewRelations = true;
|
||||
relatesToEvent = this.findEventById(relatesToEventId);
|
||||
relatesToEvent = this.findEventById(relatesToEventId) || this.room.getPendingEvent(relatesToEventId);
|
||||
if (relatesToEvent) {
|
||||
relationsWithEventType.setTargetEvent(relatesToEvent);
|
||||
}
|
||||
|
||||
+72
-10
@@ -87,7 +87,7 @@ export const MatrixEvent = function(
|
||||
// amount of needless string duplication. This can save moderate amounts of
|
||||
// memory (~10% on a 350MB heap).
|
||||
// 'membership' at the event level (rather than the content level) is a legacy
|
||||
// field that Riot never otherwise looks at, but it will still take up a lot
|
||||
// field that Element never otherwise looks at, but it will still take up a lot
|
||||
// of space if we don't intern it.
|
||||
["state_key", "type", "sender", "room_id", "membership"].forEach((prop) => {
|
||||
if (!event[prop]) {
|
||||
@@ -144,6 +144,10 @@ export const MatrixEvent = function(
|
||||
*/
|
||||
this._forwardingCurve25519KeyChain = [];
|
||||
|
||||
/* where the decryption key is untrusted
|
||||
*/
|
||||
this._untrusted = null;
|
||||
|
||||
/* if we have a process decrypting this event, a Promise which resolves
|
||||
* when it is finished. Normally null.
|
||||
*/
|
||||
@@ -160,12 +164,24 @@ export const MatrixEvent = function(
|
||||
* so it can be easily accessed from the timeline.
|
||||
*/
|
||||
this.verificationRequest = null;
|
||||
|
||||
/* The txnId with which this event was sent if it was during this session,
|
||||
allows for a unique ID which does not change when the event comes back down sync.
|
||||
*/
|
||||
this._txnId = event.txn_id || null;
|
||||
|
||||
/* Set an approximate timestamp for the event relative the local clock.
|
||||
* This will inherently be approximate because it doesn't take into account
|
||||
* the time between the server putting the 'age' field on the event as it sent
|
||||
* it to us and the time we're now constructing this event, but that's better
|
||||
* than assuming the local clock is in sync with the origin HS's clock.
|
||||
*/
|
||||
this._localTimestamp = Date.now() - this.getAge();
|
||||
};
|
||||
utils.inherits(MatrixEvent, EventEmitter);
|
||||
|
||||
|
||||
utils.extend(MatrixEvent.prototype, {
|
||||
|
||||
/**
|
||||
* Get the event_id for this event.
|
||||
* @return {string} The event ID, e.g. <code>$143350589368169JsLZx:localhost
|
||||
@@ -303,11 +319,12 @@ utils.extend(MatrixEvent.prototype, {
|
||||
|
||||
/**
|
||||
* Get the age of the event when this function was called.
|
||||
* Relies on the local clock being in sync with the clock of the original homeserver.
|
||||
* This is the 'age' field adjusted according to how long this client has
|
||||
* had the event.
|
||||
* @return {Number} The age of this event in milliseconds.
|
||||
*/
|
||||
getLocalAge: function() {
|
||||
return Date.now() - this.getTs();
|
||||
return Date.now() - this._localTimestamp;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -390,11 +407,12 @@ utils.extend(MatrixEvent.prototype, {
|
||||
* @internal
|
||||
*
|
||||
* @param {module:crypto} crypto crypto module
|
||||
* @param {bool} isRetry True if this is a retry (enables more logging)
|
||||
*
|
||||
* @returns {Promise} promise which resolves (to undefined) when the decryption
|
||||
* attempt is completed.
|
||||
*/
|
||||
attemptDecryption: async function(crypto) {
|
||||
attemptDecryption: async function(crypto, isRetry) {
|
||||
// start with a couple of sanity checks.
|
||||
if (!this.isEncrypted()) {
|
||||
throw new Error("Attempt to decrypt event which isn't encrypted");
|
||||
@@ -406,7 +424,7 @@ utils.extend(MatrixEvent.prototype, {
|
||||
) {
|
||||
// we may want to just ignore this? let's start with rejecting it.
|
||||
throw new Error(
|
||||
"Attempt to decrypt event which has already been encrypted",
|
||||
"Attempt to decrypt event which has already been decrypted",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -424,7 +442,7 @@ utils.extend(MatrixEvent.prototype, {
|
||||
return this._decryptionPromise;
|
||||
}
|
||||
|
||||
this._decryptionPromise = this._decryptionLoop(crypto);
|
||||
this._decryptionPromise = this._decryptionLoop(crypto, isRetry);
|
||||
return this._decryptionPromise;
|
||||
},
|
||||
|
||||
@@ -469,7 +487,7 @@ utils.extend(MatrixEvent.prototype, {
|
||||
return recipients;
|
||||
},
|
||||
|
||||
_decryptionLoop: async function(crypto) {
|
||||
_decryptionLoop: async function(crypto, isRetry) {
|
||||
// make sure that this method never runs completely synchronously.
|
||||
// (doing so would mean that we would clear _decryptionPromise *before*
|
||||
// it is set in attemptDecryption - and hence end up with a stuck
|
||||
@@ -486,13 +504,18 @@ utils.extend(MatrixEvent.prototype, {
|
||||
res = this._badEncryptedMessage("Encryption not enabled");
|
||||
} else {
|
||||
res = await crypto.decryptEvent(this);
|
||||
if (isRetry) {
|
||||
logger.info(`Decrypted event on retry (id=${this.getId()})`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name !== "DecryptionError") {
|
||||
// not a decryption error: log the whole exception as an error
|
||||
// (and don't bother with a retry)
|
||||
const re = isRetry ? 're' : '';
|
||||
logger.error(
|
||||
`Error decrypting event (id=${this.getId()}): ${e.stack || e}`,
|
||||
`Error ${re}decrypting event ` +
|
||||
`(id=${this.getId()}): ${e.stack || e}`,
|
||||
);
|
||||
this._decryptionPromise = null;
|
||||
this._retryDecryption = false;
|
||||
@@ -593,6 +616,7 @@ utils.extend(MatrixEvent.prototype, {
|
||||
decryptionResult.claimedEd25519Key || null;
|
||||
this._forwardingCurve25519KeyChain =
|
||||
decryptionResult.forwardingCurve25519KeyChain || [];
|
||||
this._untrusted = decryptionResult.untrusted || false;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -611,7 +635,7 @@ utils.extend(MatrixEvent.prototype, {
|
||||
* @return {boolean} True if this event is encrypted.
|
||||
*/
|
||||
isEncrypted: function() {
|
||||
return this.event.type === "m.room.encrypted";
|
||||
return !this.isState() && this.event.type === "m.room.encrypted";
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -683,6 +707,16 @@ utils.extend(MatrixEvent.prototype, {
|
||||
return this._forwardingCurve25519KeyChain;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether the decryption key was obtained from an untrusted source. If so,
|
||||
* we cannot verify the authenticity of the message.
|
||||
*
|
||||
* @return {boolean}
|
||||
*/
|
||||
isKeySourceUntrusted: function() {
|
||||
return this._untrusted;
|
||||
},
|
||||
|
||||
getUnsigned: function() {
|
||||
return this.event.unsigned || {};
|
||||
},
|
||||
@@ -777,6 +811,24 @@ utils.extend(MatrixEvent.prototype, {
|
||||
return this.getType() === "m.room.redaction";
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the (decrypted, if necessary) redaction event JSON
|
||||
* if event was redacted
|
||||
*
|
||||
* @returns {object} The redaction event JSON, or an empty object
|
||||
*/
|
||||
getRedactionEvent: function() {
|
||||
if (!this.isRedacted()) return null;
|
||||
|
||||
if (this._clearEvent.unsigned) {
|
||||
return this._clearEvent.unsigned.redacted_because;
|
||||
} else if (this.event.unsigned.redacted_because) {
|
||||
return this.event.unsigned.redacted_because;
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the push actions, if known, for this event
|
||||
*
|
||||
@@ -879,6 +931,8 @@ utils.extend(MatrixEvent.prototype, {
|
||||
/**
|
||||
* Set an event that replaces the content of this event, through an m.replace relation.
|
||||
*
|
||||
* @fires module:models/event.MatrixEvent#"Event.replaced"
|
||||
*
|
||||
* @param {MatrixEvent?} newEvent the event with the replacing content, if any.
|
||||
*/
|
||||
makeReplaced(newEvent) {
|
||||
@@ -1064,6 +1118,14 @@ utils.extend(MatrixEvent.prototype, {
|
||||
setVerificationRequest: function(request) {
|
||||
this.verificationRequest = request;
|
||||
},
|
||||
|
||||
setTxnId(txnId) {
|
||||
this._txnId = txnId;
|
||||
},
|
||||
|
||||
getTxnId() {
|
||||
return this._txnId;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
import {EventStatus} from '../models/event';
|
||||
import {logger} from '../logger';
|
||||
|
||||
/**
|
||||
* A container for relation events that supports easy access to common ways of
|
||||
@@ -60,7 +61,7 @@ export class Relations extends EventEmitter {
|
||||
|
||||
const relation = event.getRelation();
|
||||
if (!relation) {
|
||||
console.error("Event must have relation info");
|
||||
logger.error("Event must have relation info");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,7 +69,7 @@ export class Relations extends EventEmitter {
|
||||
const eventType = event.getType();
|
||||
|
||||
if (this.relationType !== relationType || this.eventType !== eventType) {
|
||||
console.error("Event relation info doesn't match this container");
|
||||
logger.error("Event relation info doesn't match this container");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -104,7 +105,7 @@ export class Relations extends EventEmitter {
|
||||
|
||||
const relation = event.getRelation();
|
||||
if (!relation) {
|
||||
console.error("Event must have relation info");
|
||||
logger.error("Event must have relation info");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -112,7 +113,7 @@ export class Relations extends EventEmitter {
|
||||
const eventType = event.getType();
|
||||
|
||||
if (this.relationType !== relationType || this.eventType !== eventType) {
|
||||
console.error("Event relation info doesn't match this container");
|
||||
logger.error("Event relation info doesn't match this container");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {EventEmitter} from "events";
|
||||
import {getHttpUriForMxc, getIdenticonUri} from "../content-repo";
|
||||
import {getHttpUriForMxc} from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
|
||||
/**
|
||||
@@ -274,10 +274,6 @@ RoomMember.prototype.getAvatarUrl =
|
||||
);
|
||||
if (httpUrl) {
|
||||
return httpUrl;
|
||||
} else if (allowDefault) {
|
||||
return getIdenticonUri(
|
||||
baseUrl, this.userId, width, height,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
@@ -286,14 +282,17 @@ RoomMember.prototype.getAvatarUrl =
|
||||
* @return {string} the mxc avatar url
|
||||
*/
|
||||
RoomMember.prototype.getMxcAvatarUrl = function() {
|
||||
if(this.events.member) {
|
||||
if (this.events.member) {
|
||||
return this.events.member.getDirectionalContent().avatar_url;
|
||||
} else if(this.user) {
|
||||
} else if (this.user) {
|
||||
return this.user.avatarUrl;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const MXID_PATTERN = /@.+:.+/;
|
||||
const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/;
|
||||
|
||||
function calculateDisplayName(selfUserId, displayName, roomState) {
|
||||
if (!displayName || displayName === selfUserId) {
|
||||
return selfUserId;
|
||||
@@ -312,13 +311,13 @@ function calculateDisplayName(selfUserId, displayName, roomState) {
|
||||
// Next check if the name contains something that look like a mxid
|
||||
// If it does, it may be someone trying to impersonate someone else
|
||||
// Show full mxid in this case
|
||||
let disambiguate = /@.+:.+/.test(displayName);
|
||||
let disambiguate = MXID_PATTERN.test(displayName);
|
||||
|
||||
if (!disambiguate) {
|
||||
// Also show mxid if the display name contains any LTR/RTL characters as these
|
||||
// make it very difficult for us to find similar *looking* display names
|
||||
// E.g "Mark" could be cloned by writing "kraM" but in RTL.
|
||||
disambiguate = /[\u200E\u200F\u202A-\u202F]/.test(displayName);
|
||||
disambiguate = LTR_RTL_PATTERN.test(displayName);
|
||||
}
|
||||
|
||||
if (!disambiguate) {
|
||||
|
||||
+42
-18
@@ -23,6 +23,7 @@ import {EventEmitter} from "events";
|
||||
import {RoomMember} from "./room-member";
|
||||
import {logger} from '../logger';
|
||||
import * as utils from "../utils";
|
||||
import {EventType} from "../@types/event";
|
||||
|
||||
// possible statuses for out-of-band member loading
|
||||
const OOB_STATUS_NOTSTARTED = 1;
|
||||
@@ -68,9 +69,7 @@ export function RoomState(roomId, oobMemberFlags = undefined) {
|
||||
this.members = {
|
||||
// userId: RoomMember
|
||||
};
|
||||
this.events = {
|
||||
// eventType: { stateKey: MatrixEvent }
|
||||
};
|
||||
this.events = new Map(); // Map<eventType, Map<stateKey, MatrixEvent>>
|
||||
this.paginationToken = null;
|
||||
|
||||
this._sentinels = {
|
||||
@@ -211,14 +210,14 @@ RoomState.prototype.getSentinelMember = function(userId) {
|
||||
* <code>undefined</code>, else a single event (or null if no match found).
|
||||
*/
|
||||
RoomState.prototype.getStateEvents = function(eventType, stateKey) {
|
||||
if (!this.events[eventType]) {
|
||||
if (!this.events.has(eventType)) {
|
||||
// no match
|
||||
return stateKey === undefined ? [] : null;
|
||||
}
|
||||
if (stateKey === undefined) { // return all values
|
||||
return utils.values(this.events[eventType]);
|
||||
return Array.from(this.events.get(eventType).values());
|
||||
}
|
||||
const event = this.events[eventType][stateKey];
|
||||
const event = this.events.get(eventType).get(stateKey);
|
||||
return event ? event : null;
|
||||
};
|
||||
|
||||
@@ -238,9 +237,8 @@ RoomState.prototype.clone = function() {
|
||||
const status = this._oobMemberFlags.status;
|
||||
this._oobMemberFlags.status = OOB_STATUS_NOTSTARTED;
|
||||
|
||||
Object.values(this.events).forEach((eventsByStateKey) => {
|
||||
const eventsForType = Object.values(eventsByStateKey);
|
||||
copy.setStateEvents(eventsForType);
|
||||
Array.from(this.events.values()).forEach((eventsByStateKey) => {
|
||||
copy.setStateEvents(Array.from(eventsByStateKey.values()));
|
||||
});
|
||||
|
||||
// Ugly hack: see above
|
||||
@@ -276,8 +274,8 @@ RoomState.prototype.clone = function() {
|
||||
*/
|
||||
RoomState.prototype.setUnknownStateEvents = function(events) {
|
||||
const unknownStateEvents = events.filter((event) => {
|
||||
return this.events[event.getType()] === undefined ||
|
||||
this.events[event.getType()][event.getStateKey()] === undefined;
|
||||
return !this.events.has(event.getType()) ||
|
||||
!this.events.get(event.getType()).has(event.getStateKey());
|
||||
});
|
||||
|
||||
this.setStateEvents(unknownStateEvents);
|
||||
@@ -306,6 +304,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastStateEvent = self._getStateEventMatching(event);
|
||||
self._setStateEvent(event);
|
||||
if (event.getType() === "m.room.member") {
|
||||
_updateDisplayNameCache(
|
||||
@@ -313,7 +312,7 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
);
|
||||
_updateThirdPartyTokenCache(self, event);
|
||||
}
|
||||
self.emit("RoomState.events", event, self);
|
||||
self.emit("RoomState.events", event, self, lastStateEvent);
|
||||
});
|
||||
|
||||
// update higher level data structures. This needs to be done AFTER the
|
||||
@@ -352,8 +351,14 @@ RoomState.prototype.setStateEvents = function(stateEvents) {
|
||||
} else if (event.getType() === "m.room.power_levels") {
|
||||
const members = utils.values(self.members);
|
||||
utils.forEach(members, function(member) {
|
||||
// We only propagate `RoomState.members` event if the
|
||||
// power levels has been changed
|
||||
// large room suffer from large re-rendering especially when not needed
|
||||
const oldLastModified = member.getLastModifiedTime();
|
||||
member.setPowerLevelEvent(event);
|
||||
self.emit("RoomState.members", event, self, member);
|
||||
if (oldLastModified !== member.getLastModifiedTime()) {
|
||||
self.emit("RoomState.members", event, self, member);
|
||||
}
|
||||
});
|
||||
|
||||
// assume all our sentinels are now out-of-date
|
||||
@@ -385,10 +390,15 @@ RoomState.prototype._getOrCreateMember = function(userId, event) {
|
||||
};
|
||||
|
||||
RoomState.prototype._setStateEvent = function(event) {
|
||||
if (this.events[event.getType()] === undefined) {
|
||||
this.events[event.getType()] = {};
|
||||
if (!this.events.has(event.getType())) {
|
||||
this.events.set(event.getType(), new Map());
|
||||
}
|
||||
this.events[event.getType()][event.getStateKey()] = event;
|
||||
this.events.get(event.getType()).set(event.getStateKey(), event);
|
||||
};
|
||||
|
||||
RoomState.prototype._getStateEventMatching = function(event) {
|
||||
if (!this.events.has(event.getType())) return null;
|
||||
return this.events.get(event.getType()).get(event.getStateKey());
|
||||
};
|
||||
|
||||
RoomState.prototype._updateMember = function(member) {
|
||||
@@ -670,7 +680,7 @@ RoomState.prototype._maySendEventOfType = function(eventType, userId, state) {
|
||||
const userPowerLevel = power_levels.users && power_levels.users[userId];
|
||||
if (Number.isFinite(userPowerLevel)) {
|
||||
powerLevel = userPowerLevel;
|
||||
} else if(Number.isFinite(power_levels.users_default)) {
|
||||
} else if (Number.isFinite(power_levels.users_default)) {
|
||||
powerLevel = power_levels.users_default;
|
||||
}
|
||||
|
||||
@@ -715,6 +725,16 @@ RoomState.prototype.mayTriggerNotifOfType = function(notifLevelKey, userId) {
|
||||
return member.powerLevel >= notifLevel;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
|
||||
* @returns {string} the join_rule applied to this room
|
||||
*/
|
||||
RoomState.prototype.getJoinRule = function() {
|
||||
const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, "");
|
||||
const joinRuleContent = joinRuleEvent ? joinRuleEvent.getContent() : {};
|
||||
return joinRuleContent["join_rule"] || "invite";
|
||||
};
|
||||
|
||||
|
||||
function _updateThirdPartyTokenCache(roomState, memberEvent) {
|
||||
if (!memberEvent.getContent().third_party_invite) {
|
||||
@@ -769,8 +789,12 @@ function _updateDisplayNameCache(roomState, userId, displayName) {
|
||||
* @param {MatrixEvent} event The matrix event which caused this event to fire.
|
||||
* @param {RoomState} state The room state whose RoomState.events dictionary
|
||||
* was updated.
|
||||
* @param {MatrixEvent} prevEvent The event being replaced by the new state, if
|
||||
* known. Note that this can differ from `getPrevContent()` on the new state event
|
||||
* as this is the store's view of the last state, not the previous state provided
|
||||
* by the server.
|
||||
* @example
|
||||
* matrixClient.on("RoomState.events", function(event, state){
|
||||
* matrixClient.on("RoomState.events", function(event, state, prevEvent){
|
||||
* var newStateEvent = event;
|
||||
* });
|
||||
*/
|
||||
|
||||
+210
-32
@@ -23,13 +23,14 @@ limitations under the License.
|
||||
import {EventEmitter} from "events";
|
||||
import {EventTimelineSet} from "./event-timeline-set";
|
||||
import {EventTimeline} from "./event-timeline";
|
||||
import {getHttpUriForMxc, getIdenticonUri} from "../content-repo";
|
||||
import {getHttpUriForMxc} from "../content-repo";
|
||||
import * as utils from "../utils";
|
||||
import {EventStatus, MatrixEvent} from "./event";
|
||||
import {RoomMember} from "./room-member";
|
||||
import {RoomSummary} from "./room-summary";
|
||||
import {logger} from '../logger';
|
||||
import {ReEmitter} from '../ReEmitter';
|
||||
import {EventType, RoomCreateTypeField, RoomType} from "../@types/event";
|
||||
|
||||
// These constants are used as sane defaults when the homeserver doesn't support
|
||||
// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
|
||||
@@ -37,8 +38,8 @@ import {ReEmitter} from '../ReEmitter';
|
||||
// room versions which are considered okay for people to run without being asked
|
||||
// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
|
||||
// return an m.room_versions capability.
|
||||
const KNOWN_SAFE_ROOM_VERSION = '5';
|
||||
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5'];
|
||||
const KNOWN_SAFE_ROOM_VERSION = '6';
|
||||
const SAFE_ROOM_VERSIONS = ['1', '2', '3', '4', '5', '6'];
|
||||
|
||||
function synthesizeReceipt(userId, event, receiptType) {
|
||||
// console.log("synthesizing receipt for "+event.getId());
|
||||
@@ -122,6 +123,12 @@ export function Room(roomId, client, myUserId, opts) {
|
||||
opts = opts || {};
|
||||
opts.pendingEventOrdering = opts.pendingEventOrdering || "chronological";
|
||||
|
||||
this._client = client;
|
||||
|
||||
// In some cases, we add listeners for every displayed Matrix event, so it's
|
||||
// common to have quite a few more than the default limit.
|
||||
this.setMaxListeners(100);
|
||||
|
||||
this.reEmitter = new ReEmitter(this);
|
||||
|
||||
if (["chronological", "detached"].indexOf(opts.pendingEventOrdering) === -1) {
|
||||
@@ -184,6 +191,18 @@ export function Room(roomId, client, myUserId, opts) {
|
||||
|
||||
if (this._opts.pendingEventOrdering == "detached") {
|
||||
this._pendingEventList = [];
|
||||
const serializedPendingEventList = client._sessionStore.store.getItem(pendingEventsKey(this.roomId));
|
||||
if (serializedPendingEventList) {
|
||||
JSON.parse(serializedPendingEventList)
|
||||
.forEach(async serializedEvent => {
|
||||
const event = new MatrixEvent(serializedEvent);
|
||||
if (event.getType() === "m.room.encrypted") {
|
||||
await event.attemptDecryption(this._client._crypto);
|
||||
}
|
||||
event.setStatus(EventStatus.NOT_SENT);
|
||||
this.addPendingEvent(event, event.getTxnId());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// read by megolm; boolean value - null indicates "use global value"
|
||||
@@ -192,7 +211,6 @@ export function Room(roomId, client, myUserId, opts) {
|
||||
this._summaryHeroes = null;
|
||||
// awaited by getEncryptionTargetMembers while room members are loading
|
||||
|
||||
this._client = client;
|
||||
if (!this._opts.lazyLoadMembers) {
|
||||
this._membersPromise = Promise.resolve();
|
||||
} else {
|
||||
@@ -200,6 +218,14 @@ export function Room(roomId, client, myUserId, opts) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} roomId ID of the current room
|
||||
* @returns {string} Storage key to retrieve pending events
|
||||
*/
|
||||
function pendingEventsKey(roomId) {
|
||||
return `mx_pending_events_${roomId}`;
|
||||
}
|
||||
|
||||
utils.inherits(Room, EventEmitter);
|
||||
|
||||
/**
|
||||
@@ -209,7 +235,7 @@ utils.inherits(Room, EventEmitter);
|
||||
Room.prototype.getVersion = function() {
|
||||
const createEvent = this.currentState.getStateEvents("m.room.create", "");
|
||||
if (!createEvent) {
|
||||
logger.warn("Room " + this.room_id + " does not have an m.room.create event");
|
||||
logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event");
|
||||
return '1';
|
||||
}
|
||||
const ver = createEvent.getContent()['room_version'];
|
||||
@@ -353,22 +379,59 @@ Room.prototype.getPendingEvents = function() {
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether the pending event list contains a given event by ID.
|
||||
* Removes a pending event for this room
|
||||
*
|
||||
* @param {string} eventId The event ID to check for.
|
||||
* @return {boolean}
|
||||
* @throws If <code>opts.pendingEventOrdering</code> was not 'detached'
|
||||
* @param {string} eventId
|
||||
* @return {boolean} True if an element was removed.
|
||||
*/
|
||||
Room.prototype.hasPendingEvent = function(eventId) {
|
||||
Room.prototype.removePendingEvent = function(eventId) {
|
||||
if (this._opts.pendingEventOrdering !== "detached") {
|
||||
throw new Error(
|
||||
"Cannot call hasPendingEvent with pendingEventOrdering == " +
|
||||
"Cannot call removePendingEvent with pendingEventOrdering == " +
|
||||
this._opts.pendingEventOrdering);
|
||||
}
|
||||
|
||||
const removed = utils.removeElement(
|
||||
this._pendingEventList,
|
||||
function(ev) {
|
||||
return ev.getId() == eventId;
|
||||
}, false,
|
||||
);
|
||||
|
||||
this._savePendingEvents();
|
||||
|
||||
return removed;
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether the pending event list contains a given event by ID.
|
||||
* If pending event ordering is not "detached" then this returns false.
|
||||
*
|
||||
* @param {string} eventId The event ID to check for.
|
||||
* @return {boolean}
|
||||
*/
|
||||
Room.prototype.hasPendingEvent = function(eventId) {
|
||||
if (this._opts.pendingEventOrdering !== "detached") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this._pendingEventList.some(event => event.getId() === eventId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a specific event from the pending event list, if configured, null otherwise.
|
||||
*
|
||||
* @param {string} eventId The event ID to check for.
|
||||
* @return {MatrixEvent}
|
||||
*/
|
||||
Room.prototype.getPendingEvent = function(eventId) {
|
||||
if (this._opts.pendingEventOrdering !== "detached") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this._pendingEventList.find(event => event.getId() === eventId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the live unfiltered timeline for this room.
|
||||
*
|
||||
@@ -675,7 +738,7 @@ Room.prototype.hasUnverifiedDevices = async function() {
|
||||
}
|
||||
const e2eMembers = await this.getEncryptionTargetMembers();
|
||||
for (const member of e2eMembers) {
|
||||
const devices = await this._client.getStoredDevicesForUser(member.userId);
|
||||
const devices = this._client.getStoredDevicesForUser(member.userId);
|
||||
if (devices.some((device) => device.isUnverified())) {
|
||||
return true;
|
||||
}
|
||||
@@ -801,7 +864,7 @@ Room.prototype.getBlacklistUnverifiedDevices = function() {
|
||||
*/
|
||||
Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
|
||||
allowDefault) {
|
||||
const roomAvatarEvent = this.currentState.getStateEvents("m.room.avatar", "");
|
||||
const roomAvatarEvent = this.currentState.getStateEvents(EventType.RoomAvatar, "");
|
||||
if (allowDefault === undefined) {
|
||||
allowDefault = true;
|
||||
}
|
||||
@@ -814,15 +877,20 @@ Room.prototype.getAvatarUrl = function(baseUrl, width, height, resizeMethod,
|
||||
return getHttpUriForMxc(
|
||||
baseUrl, mainUrl, width, height, resizeMethod,
|
||||
);
|
||||
} else if (allowDefault) {
|
||||
return getIdenticonUri(
|
||||
baseUrl, this.roomId, width, height,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the mxc avatar url for the room, if one was set.
|
||||
* @return {string} the mxc avatar url or falsy
|
||||
*/
|
||||
Room.prototype.getMxcAvatarUrl = function() {
|
||||
const roomAvatarEvent = this.currentState.getStateEvents(EventType.RoomAvatar, "");
|
||||
return roomAvatarEvent ? roomAvatarEvent.getContent().url : null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the aliases this room has according to the room's state
|
||||
* The aliases returned by this function may not necessarily
|
||||
@@ -914,6 +982,15 @@ Room.prototype.addEventsToTimeline = function(events, toStartOfTimeline,
|
||||
return this.currentState.getMember(userId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all currently loaded members from the current
|
||||
* room state.
|
||||
* @returns {RoomMember[]} Room members
|
||||
*/
|
||||
Room.prototype.getMembers = function() {
|
||||
return this.currentState.getMembers();
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of members whose membership state is "join".
|
||||
* @return {RoomMember[]} A list of currently joined members.
|
||||
@@ -1170,7 +1247,7 @@ Room.prototype._addLiveEvent = function(event, duplicateStrategy, fromCache) {
|
||||
* unique transaction id.
|
||||
*/
|
||||
Room.prototype.addPendingEvent = function(event, txnId) {
|
||||
if (event.status !== EventStatus.SENDING) {
|
||||
if (event.status !== EventStatus.SENDING && event.status !== EventStatus.NOT_SENT) {
|
||||
throw new Error("addPendingEvent called on an event with status " +
|
||||
event.status);
|
||||
}
|
||||
@@ -1197,7 +1274,7 @@ Room.prototype.addPendingEvent = function(event, txnId) {
|
||||
event.setStatus(EventStatus.NOT_SENT);
|
||||
}
|
||||
this._pendingEventList.push(event);
|
||||
|
||||
this._savePendingEvents();
|
||||
if (event.isRelation()) {
|
||||
// For pending events, add them to the relations collection immediately.
|
||||
// (The alternate case below already covers this as part of adding to
|
||||
@@ -1234,6 +1311,46 @@ Room.prototype.addPendingEvent = function(event, txnId) {
|
||||
|
||||
this.emit("Room.localEchoUpdated", event, this, null, null);
|
||||
};
|
||||
|
||||
/**
|
||||
* Persists all pending events to local storage
|
||||
*
|
||||
* If the current room is encrypted only encrypted events will be persisted
|
||||
* all messages that are not yet encrypted will be discarded
|
||||
*
|
||||
* This is because the flow of EVENT_STATUS transition is
|
||||
* queued => sending => encrypting => sending => sent
|
||||
*
|
||||
* Steps 3 and 4 are skipped for unencrypted room.
|
||||
* It is better to discard an unencrypted message rather than persisting
|
||||
* it locally for everyone to read
|
||||
*/
|
||||
Room.prototype._savePendingEvents = function() {
|
||||
if (this._pendingEventList) {
|
||||
const pendingEvents = this._pendingEventList.map(event => {
|
||||
return {
|
||||
...event.event,
|
||||
txn_id: event.getTxnId(),
|
||||
};
|
||||
}).filter(event => {
|
||||
// Filter out the unencrypted messages if the room is encrypted
|
||||
const isEventEncrypted = event.type === "m.room.encrypted";
|
||||
const isRoomEncrypted = this._client.isRoomEncrypted(this.roomId);
|
||||
return isEventEncrypted || !isRoomEncrypted;
|
||||
});
|
||||
|
||||
const { store } = this._client._sessionStore;
|
||||
if (this._pendingEventList.length > 0) {
|
||||
store.setItem(
|
||||
pendingEventsKey(this.roomId),
|
||||
JSON.stringify(pendingEvents),
|
||||
);
|
||||
} else {
|
||||
store.removeItem(pendingEventsKey(this.roomId));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Used to aggregate the local echo for a relation, and also
|
||||
* for re-applying a relation after it's redaction has been cancelled,
|
||||
@@ -1278,17 +1395,17 @@ Room.prototype._handleRemoteEcho = function(remoteEvent, localEvent) {
|
||||
const newEventId = remoteEvent.getId();
|
||||
const oldStatus = localEvent.status;
|
||||
|
||||
logger.debug(
|
||||
`Got remote echo for event ${oldEventId} -> ${newEventId} ` +
|
||||
`old status ${oldStatus}`,
|
||||
);
|
||||
|
||||
// no longer pending
|
||||
delete this._txnToEvent[remoteEvent.getUnsigned().transaction_id];
|
||||
|
||||
// if it's in the pending list, remove it
|
||||
if (this._pendingEventList) {
|
||||
utils.removeElement(
|
||||
this._pendingEventList,
|
||||
function(ev) {
|
||||
return ev.getId() == oldEventId;
|
||||
}, false,
|
||||
);
|
||||
this.removePendingEvent(oldEventId);
|
||||
}
|
||||
|
||||
// replace the event source (this will preserve the plaintext payload if
|
||||
@@ -1347,7 +1464,10 @@ ALLOWED_TRANSITIONS[EventStatus.CANCELLED] =
|
||||
* @fires module:client~MatrixClient#event:"Room.localEchoUpdated"
|
||||
*/
|
||||
Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
|
||||
logger.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()}`);
|
||||
logger.log(
|
||||
`setting pendingEvent status to ${newStatus} in ${event.getRoomId()} ` +
|
||||
`event ID ${event.getId()} -> ${newEventId}`,
|
||||
);
|
||||
|
||||
// if the message was sent, we expect an event id
|
||||
if (newStatus == EventStatus.SENT && !newEventId) {
|
||||
@@ -1404,6 +1524,7 @@ Room.prototype.updatePendingEvent = function(event, newStatus, newEventId) {
|
||||
}
|
||||
this.removeEvent(oldEventId);
|
||||
}
|
||||
this._savePendingEvents();
|
||||
|
||||
this.emit("Room.localEchoUpdated", event, this, oldEventId, oldStatus);
|
||||
};
|
||||
@@ -1785,8 +1906,9 @@ Room.prototype.addAccountData = function(events) {
|
||||
if (event.getType() === "m.tag") {
|
||||
this.addTags(event);
|
||||
}
|
||||
const lastEvent = this.accountData[event.getType()];
|
||||
this.accountData[event.getType()] = event;
|
||||
this.emit("Room.accountData", event, this);
|
||||
this.emit("Room.accountData", event, this, lastEvent);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1801,7 +1923,7 @@ Room.prototype.getAccountData = function(type) {
|
||||
|
||||
|
||||
/**
|
||||
* Returns wheter the syncing user has permission to send a message in the room
|
||||
* Returns whether the syncing user has permission to send a message in the room
|
||||
* @return {boolean} true if the user should be permitted to send
|
||||
* message events into the room.
|
||||
*/
|
||||
@@ -1810,6 +1932,51 @@ Room.prototype.maySendMessage = function() {
|
||||
this.currentState.maySendEvent('m.room.message', this.myUserId);
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the given user has permissions to issue an invite for this room.
|
||||
* @param {string} userId the ID of the Matrix user to check permissions for
|
||||
* @returns {boolean} true if the user should be permitted to issue invites for this room.
|
||||
*/
|
||||
Room.prototype.canInvite = function(userId) {
|
||||
let canInvite = this.getMyMembership() === "join";
|
||||
const powerLevelsEvent = this.currentState.getStateEvents(EventType.RoomPowerLevels, "");
|
||||
const powerLevels = powerLevelsEvent && powerLevelsEvent.getContent();
|
||||
const me = this.getMember(userId);
|
||||
if (powerLevels && me && powerLevels.invite > me.powerLevel) {
|
||||
canInvite = false;
|
||||
}
|
||||
return canInvite;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
|
||||
* @returns {string} the join_rule applied to this room
|
||||
*/
|
||||
Room.prototype.getJoinRule = function() {
|
||||
return this.currentState.getJoinRule();
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the type of the room from the `m.room.create` event content or undefined if none is set
|
||||
* @returns {?string} the type of the room. Currently only RoomType.Space is known.
|
||||
*/
|
||||
Room.prototype.getType = function() {
|
||||
const createEvent = this.currentState.getStateEvents("m.room.create", "");
|
||||
if (!createEvent) {
|
||||
logger.warn("[getType] Room " + this.roomId + " does not have an m.room.create event");
|
||||
return undefined;
|
||||
}
|
||||
return createEvent.getContent()[RoomCreateTypeField];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns whether the room is a space-room as defined by MSC1772.
|
||||
* @returns {boolean} true if the room's type is RoomType.Space
|
||||
*/
|
||||
Room.prototype.isSpaceRoom = function() {
|
||||
return this.getType() === RoomType.Space;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is an internal method. Calculates the name of the room from the current
|
||||
* room state.
|
||||
@@ -1833,7 +2000,7 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
|
||||
let alias = room.getCanonicalAlias();
|
||||
|
||||
if (!alias) {
|
||||
const aliases = room.getAliases();
|
||||
const aliases = room.getAltAliases();
|
||||
|
||||
if (aliases.length) {
|
||||
alias = aliases[0];
|
||||
@@ -1891,14 +2058,14 @@ function calculateRoomName(room, userId, ignoreRoomNameEvent) {
|
||||
// let's try to figure out who was here before
|
||||
let leftNames = otherNames;
|
||||
// if we didn't have heroes, try finding them in the room state
|
||||
if(!leftNames.length) {
|
||||
if (!leftNames.length) {
|
||||
leftNames = room.currentState.getMembers().filter((m) => {
|
||||
return m.userId !== userId &&
|
||||
m.membership !== "invite" &&
|
||||
m.membership !== "join";
|
||||
}).map((m) => m.name);
|
||||
}
|
||||
if(leftNames.length) {
|
||||
if (leftNames.length) {
|
||||
return `Empty room (was ${memberNamesToRoomName(leftNames)})`;
|
||||
} else {
|
||||
return "Empty room";
|
||||
@@ -1983,8 +2150,10 @@ function memberNamesToRoomName(names, count = (names.length + 1)) {
|
||||
* @event module:client~MatrixClient#"Room.accountData"
|
||||
* @param {event} event The account_data event
|
||||
* @param {Room} room The room whose account_data was updated.
|
||||
* @param {MatrixEvent} prevEvent The event being replaced by
|
||||
* the new account data, if known.
|
||||
* @example
|
||||
* matrixClient.on("Room.accountData", function(event, room){
|
||||
* matrixClient.on("Room.accountData", function(event, room, oldEvent){
|
||||
* if (event.getType() === "m.room.colorscheme") {
|
||||
* applyColorScheme(event.getContents());
|
||||
* }
|
||||
@@ -2025,3 +2194,12 @@ function memberNamesToRoomName(names, count = (names.length + 1)) {
|
||||
*
|
||||
* @param {EventStatus} oldStatus The previous event status.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when the logged in user's membership in the room is updated.
|
||||
*
|
||||
* @event module:models/room~Room#"Room.myMembership"
|
||||
* @param {Room} room The room in which the membership has been updated
|
||||
* @param {string} membership The new membership value
|
||||
* @param {string} prevMembership The previous membership value
|
||||
*/
|
||||
|
||||
+10
-2
@@ -129,7 +129,11 @@ User.prototype.setPresenceEvent = function(event) {
|
||||
*/
|
||||
User.prototype.setDisplayName = function(name) {
|
||||
const oldName = this.displayName;
|
||||
this.displayName = name;
|
||||
if (typeof name === "string") {
|
||||
this.displayName = name;
|
||||
} else {
|
||||
this.displayName = undefined;
|
||||
}
|
||||
if (name !== oldName) {
|
||||
this._updateModifiedTime();
|
||||
}
|
||||
@@ -142,7 +146,11 @@ User.prototype.setDisplayName = function(name) {
|
||||
* @param {string} name The new display name.
|
||||
*/
|
||||
User.prototype.setRawDisplayName = function(name) {
|
||||
this.rawDisplayName = name;
|
||||
if (typeof name === "string") {
|
||||
this.rawDisplayName = name;
|
||||
} else {
|
||||
this.rawDisplayName = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
+35
-70
@@ -16,6 +16,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {escapeRegExp, globToRegexp, isNullOrUndefined} from "./utils";
|
||||
import {logger} from './logger';
|
||||
|
||||
/**
|
||||
* @module pushprocessor
|
||||
@@ -23,9 +24,8 @@ import {escapeRegExp, globToRegexp, isNullOrUndefined} from "./utils";
|
||||
|
||||
const RULEKINDS_IN_ORDER = ['override', 'content', 'room', 'sender', 'underride'];
|
||||
|
||||
// The default override rules to apply when calculating actions for an event. These
|
||||
// defaults apply under no other circumstances to avoid confusing the client with server
|
||||
// state. We do this for two reasons:
|
||||
// The default override rules to apply to the push rules that arrive from the server.
|
||||
// We do this for two reasons:
|
||||
// 1. Synapse is unlikely to send us the push rule in an incremental sync - see
|
||||
// https://github.com/matrix-org/synapse/pull/4867#issuecomment-481446072 for
|
||||
// more details.
|
||||
@@ -85,12 +85,15 @@ export function PushProcessor(client) {
|
||||
// $glob: RegExp,
|
||||
};
|
||||
|
||||
const matchingRuleFromKindSet = (ev, kindset, device) => {
|
||||
const matchingRuleFromKindSet = (ev, kindset) => {
|
||||
for (let ruleKindIndex = 0;
|
||||
ruleKindIndex < RULEKINDS_IN_ORDER.length;
|
||||
++ruleKindIndex) {
|
||||
const kind = RULEKINDS_IN_ORDER[ruleKindIndex];
|
||||
const ruleset = kindset[kind];
|
||||
if (!ruleset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (let ruleIndex = 0; ruleIndex < ruleset.length; ++ruleIndex) {
|
||||
const rule = ruleset[ruleIndex];
|
||||
@@ -98,7 +101,7 @@ export function PushProcessor(client) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const rawrule = templateRuleToRaw(kind, rule, device);
|
||||
const rawrule = templateRuleToRaw(kind, rule);
|
||||
if (!rawrule) {
|
||||
continue;
|
||||
}
|
||||
@@ -112,7 +115,7 @@ export function PushProcessor(client) {
|
||||
return null;
|
||||
};
|
||||
|
||||
const templateRuleToRaw = function(kind, tprule, device) {
|
||||
const templateRuleToRaw = function(kind, tprule) {
|
||||
const rawrule = {
|
||||
'rule_id': tprule.rule_id,
|
||||
'actions': tprule.actions,
|
||||
@@ -154,19 +157,12 @@ export function PushProcessor(client) {
|
||||
});
|
||||
break;
|
||||
}
|
||||
if (device) {
|
||||
rawrule.conditions.push({
|
||||
'kind': 'device',
|
||||
'profile_tag': device,
|
||||
});
|
||||
}
|
||||
return rawrule;
|
||||
};
|
||||
|
||||
const eventFulfillsCondition = function(cond, ev) {
|
||||
const condition_functions = {
|
||||
"event_match": eventFulfillsEventMatchCondition,
|
||||
"device": eventFulfillsDeviceCondition,
|
||||
"contains_display_name": eventFulfillsDisplayNameCondition,
|
||||
"room_member_count": eventFulfillsRoomMemberCountCondition,
|
||||
"sender_notification_permission": eventFulfillsSenderNotifPermCondition,
|
||||
@@ -258,10 +254,6 @@ export function PushProcessor(client) {
|
||||
return content.body.search(pat) > -1;
|
||||
};
|
||||
|
||||
const eventFulfillsDeviceCondition = function(cond, ev) {
|
||||
return false; // XXX: Allow a profile tag to be set for the web client instance
|
||||
};
|
||||
|
||||
const eventFulfillsEventMatchCondition = function(cond, ev) {
|
||||
if (!cond.key) {
|
||||
return false;
|
||||
@@ -276,6 +268,10 @@ export function PushProcessor(client) {
|
||||
return cond.value === val;
|
||||
}
|
||||
|
||||
if (typeof cond.pattern !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let regex;
|
||||
|
||||
if (cond.key == 'content.body') {
|
||||
@@ -326,23 +322,13 @@ export function PushProcessor(client) {
|
||||
};
|
||||
|
||||
const matchingRuleForEventWithRulesets = function(ev, rulesets) {
|
||||
if (!rulesets || !rulesets.device) {
|
||||
if (!rulesets) {
|
||||
return null;
|
||||
}
|
||||
if (ev.getSender() == client.credentials.userId) {
|
||||
if (ev.getSender() === client.credentials.userId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const allDevNames = Object.keys(rulesets.device);
|
||||
for (let i = 0; i < allDevNames.length; ++i) {
|
||||
const devname = allDevNames[i];
|
||||
const devrules = rulesets.device[devname];
|
||||
|
||||
const matchingRule = matchingRuleFromKindSet(devrules, devname);
|
||||
if (matchingRule) {
|
||||
return matchingRule;
|
||||
}
|
||||
}
|
||||
return matchingRuleFromKindSet(ev, rulesets.global);
|
||||
};
|
||||
|
||||
@@ -364,33 +350,6 @@ export function PushProcessor(client) {
|
||||
return actionObj;
|
||||
};
|
||||
|
||||
const applyRuleDefaults = function(clientRuleset) {
|
||||
// Deep clone the object before we mutate it
|
||||
const ruleset = JSON.parse(JSON.stringify(clientRuleset));
|
||||
|
||||
if (!clientRuleset['global']) {
|
||||
clientRuleset['global'] = {};
|
||||
}
|
||||
if (!clientRuleset['global']['override']) {
|
||||
clientRuleset['global']['override'] = [];
|
||||
}
|
||||
|
||||
// Apply default overrides
|
||||
const globalOverrides = clientRuleset['global']['override'];
|
||||
for (const override of DEFAULT_OVERRIDE_RULES) {
|
||||
const existingRule = globalOverrides
|
||||
.find((r) => r.rule_id === override.rule_id);
|
||||
|
||||
if (!existingRule) {
|
||||
const ruleId = override.rule_id;
|
||||
console.warn(`Adding default global override for ${ruleId}`);
|
||||
globalOverrides.push(override);
|
||||
}
|
||||
}
|
||||
|
||||
return ruleset;
|
||||
};
|
||||
|
||||
this.ruleMatchesEvent = function(rule, ev) {
|
||||
let ret = true;
|
||||
for (let i = 0; i < rule.conditions.length; ++i) {
|
||||
@@ -410,8 +369,7 @@ export function PushProcessor(client) {
|
||||
* @return {PushAction}
|
||||
*/
|
||||
this.actionsForEvent = function(ev) {
|
||||
const rules = applyRuleDefaults(client.pushRules);
|
||||
return pushActionsForEventAndRulesets(ev, rules);
|
||||
return pushActionsForEventAndRulesets(ev, client.pushRules);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -421,7 +379,7 @@ export function PushProcessor(client) {
|
||||
* @return {object} The push rule, or null if no such rule was found
|
||||
*/
|
||||
this.getPushRuleById = function(ruleId) {
|
||||
for (const scope of ['device', 'global']) {
|
||||
for (const scope of ['global']) {
|
||||
if (client.pushRules[scope] === undefined) continue;
|
||||
|
||||
for (const kind of RULEKINDS_IN_ORDER) {
|
||||
@@ -476,18 +434,25 @@ PushProcessor.rewriteDefaultRules = function(incomingRules) {
|
||||
if (!newRules.global) newRules.global = {};
|
||||
if (!newRules.global.override) newRules.global.override = [];
|
||||
|
||||
// Fix default override rules
|
||||
newRules.global.override = newRules.global.override.map(r => {
|
||||
const defaultRule = DEFAULT_OVERRIDE_RULES.find(d => d.rule_id === r.rule_id);
|
||||
if (!defaultRule) return r;
|
||||
// Merge the client-level defaults with the ones from the server
|
||||
const globalOverrides = newRules.global.override;
|
||||
for (const override of DEFAULT_OVERRIDE_RULES) {
|
||||
const existingRule = globalOverrides
|
||||
.find((r) => r.rule_id === override.rule_id);
|
||||
|
||||
// Copy over the actions, default, and conditions. Don't touch the user's
|
||||
// preference.
|
||||
r.default = defaultRule.default;
|
||||
r.conditions = defaultRule.conditions;
|
||||
r.actions = defaultRule.actions;
|
||||
return r;
|
||||
});
|
||||
if (existingRule) {
|
||||
// Copy over the actions, default, and conditions. Don't touch the user's
|
||||
// preference.
|
||||
existingRule.default = override.default;
|
||||
existingRule.conditions = override.conditions;
|
||||
existingRule.actions = override.actions;
|
||||
} else {
|
||||
// Add the rule
|
||||
const ruleId = override.rule_id;
|
||||
logger.warn(`Adding default global override for ${ruleId}`);
|
||||
globalOverrides.push(override);
|
||||
}
|
||||
}
|
||||
|
||||
return newRules;
|
||||
};
|
||||
|
||||
+16
-1
@@ -15,9 +15,24 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
|
||||
const UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const DIGITS = "0123456789";
|
||||
|
||||
export function randomString(len: number): string {
|
||||
return randomStringFrom(len, UPPERCASE + LOWERCASE + DIGITS);
|
||||
}
|
||||
|
||||
export function randomLowercaseString(len: number): string {
|
||||
return randomStringFrom(len, LOWERCASE);
|
||||
}
|
||||
|
||||
export function randomUppercaseString(len: number): string {
|
||||
return randomStringFrom(len, UPPERCASE);
|
||||
}
|
||||
|
||||
function randomStringFrom(len: number, chars: string): string {
|
||||
let ret = "";
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (let i = 0; i < len; ++i) {
|
||||
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
|
||||
@@ -185,8 +185,8 @@ function _runCallbacks() {
|
||||
*/
|
||||
function binarySearch(array, func) {
|
||||
// min is inclusive, max exclusive.
|
||||
let min = 0,
|
||||
max = array.length;
|
||||
let min = 0;
|
||||
let max = array.length;
|
||||
|
||||
while (min < max) {
|
||||
const mid = (min + max) >> 1;
|
||||
|
||||
+1
-1
@@ -164,7 +164,7 @@ MatrixScheduler.RETRY_BACKOFF_RATELIMIT = function(event, attempts, err) {
|
||||
|
||||
if (err.name === "M_LIMIT_EXCEEDED") {
|
||||
const waitTime = err.data.retry_after_ms;
|
||||
if (waitTime) {
|
||||
if (waitTime > 0) {
|
||||
return waitTime;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user