Compare commits
1288 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5414b3b39d | |||
| c54db30dc8 | |||
| f61604a51e | |||
| ae77f900ef | |||
| 39d3640973 | |||
| 0a7a80d5a8 | |||
| 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 | |||
| 8b6b16067b | |||
| a2da0de17d | |||
| 93ff3edb6b | |||
| 7c67fd69dd | |||
| 5ef5412a55 | |||
| e88a384aa7 | |||
| 9067feeafb | |||
| ed978f69fb | |||
| 743f2465ea | |||
| 41fffa233a | |||
| e45377166b | |||
| 24939bf0b0 | |||
| 3221be4855 | |||
| 3135f1ed24 | |||
| 1b0834ffb0 | |||
| ad85740ae2 | |||
| d79d613cb7 | |||
| d8cc1f7b7a | |||
| d7c8856fdd | |||
| 9d80a332aa | |||
| e14f7b63c7 | |||
| 3bd2880923 | |||
| 2401ad7159 | |||
| 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 | |||
| f654c8a892 | |||
| 336fce55df | |||
| 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 | |||
| 6b35302442 | |||
| 2937e58215 | |||
| d42589b6cc | |||
| 26e9dfb4fb | |||
| f27d03a6bc | |||
| b1e3150a81 | |||
| 5d52053caa | |||
| ce668d051c | |||
| e06579ecf5 | |||
| 6c30af245c | |||
| c9c40a6dde | |||
| e748ac3d00 | |||
| aec79f3a79 | |||
| bf92cb1522 | |||
| 14e1920ff5 | |||
| c95cdf5a11 | |||
| c14d0616ea | |||
| 0112701145 | |||
| cb69515be9 | |||
| 3cd791e08f | |||
| 6e233e860e | |||
| b4f0ea441b | |||
| 39974d3a61 | |||
| a998006842 | |||
| c4e449fc45 | |||
| 765fbe2182 | |||
| 08dfa73b57 | |||
| a58e7a34e7 | |||
| 7a481beec6 | |||
| d51fad2de4 | |||
| c66755a756 | |||
| 886ad03505 | |||
| ba33ef0a68 | |||
| fe97dc3ece | |||
| 76c4875088 | |||
| 04a3aaee35 | |||
| fef03cda9b | |||
| 3292fde41b | |||
| 38cf25ac5a | |||
| 13d5d2f958 | |||
| 7f6b66c824 | |||
| 62c344b633 | |||
| 75ce2729f9 | |||
| 6669554867 | |||
| d3294da37c | |||
| 9b56bf25cf | |||
| e1a33d8a7b | |||
| 47a1224c13 | |||
| 5c57d81e94 | |||
| edefd3ec88 | |||
| f15098efde | |||
| 8ee99a0616 | |||
| 3ace1d04cd | |||
| 365bb772bc | |||
| 5ee6ada973 | |||
| ee0fa0e687 | |||
| 0d41f6aafc | |||
| 91b6499815 | |||
| 7cd1166a47 | |||
| f76cb677ff | |||
| 05e7f4e6f7 | |||
| 6684574bdf | |||
| 36a945f8e2 | |||
| 6a3d322033 | |||
| 00c003ec65 | |||
| f4d335c161 | |||
| 659f42139b | |||
| 0e791ed022 | |||
| 48655aa1a3 | |||
| 83fa80cfda | |||
| cf5b5ee085 | |||
| 429a4e3526 | |||
| d66d4c1cd9 | |||
| 7a1bbdf2dd | |||
| 29c1459568 | |||
| efad46a8a4 | |||
| a69c621305 | |||
| ad6dde6f26 | |||
| 2627e46723 | |||
| 408d70b55e | |||
| 3f369e528b | |||
| 312976294b | |||
| 77f42c479b | |||
| d60bd22674 | |||
| 2e67f77d3e | |||
| 6d8e8e6bd7 | |||
| 9c01945a05 | |||
| 7ce5ddd380 | |||
| 2b5de914f5 | |||
| 18a2426707 | |||
| 367fac6d54 | |||
| 157cc9e5eb | |||
| 81daf12598 | |||
| 9249b0652f | |||
| ee4c6b6265 | |||
| 68deab4a68 | |||
| c9c765b5b8 | |||
| 616f73d8c6 | |||
| 208c371afb | |||
| 3a59cfa9c0 | |||
| cf94527bd5 | |||
| fa93479863 | |||
| 8bc0ef8c27 | |||
| bd403b6d87 | |||
| 57a7328065 | |||
| 4945463beb | |||
| dfafa791f2 | |||
| 5f2cb6b3a4 | |||
| 5398fac348 | |||
| b217f6aa81 | |||
| ec597bea93 | |||
| 7a5c54fef7 | |||
| 4064f18de2 | |||
| 6d13457172 | |||
| f39518ef93 | |||
| 4b1cecd246 | |||
| 352509fd3a | |||
| d0f08f8839 | |||
| efd38a3471 | |||
| a4e74fea94 | |||
| fdb33b6189 | |||
| dcbb67838b | |||
| 1727d636a3 | |||
| 9eadc7f868 | |||
| 620118af5f | |||
| 3645764f9a | |||
| 769bfeb10f | |||
| 5fbaa9cfa7 | |||
| 007508ba12 | |||
| 0f1f18b232 | |||
| d6b754b133 | |||
| 1b80c83676 | |||
| ec4dc582b6 | |||
| 65646ff9e2 | |||
| 92f6ec918b | |||
| 62bd41d2e6 | |||
| 9d864ffd60 | |||
| c45b38cece | |||
| 0d7aee2c36 | |||
| be345a523f | |||
| 470bdf8741 | |||
| 59319fb55b | |||
| fb7695fdbc | |||
| 25b7552683 | |||
| 21d520378f | |||
| 9cd6607520 | |||
| efd3550f53 | |||
| 76402ec8d7 | |||
| f689142806 | |||
| fd563bda6a | |||
| 09a8f7122c | |||
| 608fb00844 | |||
| 5c45e9c306 | |||
| 950221dc13 | |||
| f816679596 | |||
| 80ccf18b16 | |||
| c7abd9062a | |||
| 4287f2229b | |||
| 8408055137 | |||
| cc0965d703 | |||
| 94b3d9d3e1 | |||
| 772bf7d6ff | |||
| 15c2e4bb07 | |||
| 419693023f | |||
| 2d081f2c19 | |||
| c76ce1fd85 | |||
| f38b4d37e6 | |||
| 73c92dfc57 | |||
| 61c5430deb | |||
| 21e4c597d9 | |||
| 4dbeee8cb3 | |||
| adc76c636e | |||
| 0dbf89b2b4 | |||
| 83241ac17d | |||
| 6aa5d39357 | |||
| 1304ecbe03 | |||
| aafc027812 | |||
| d84e0b166b | |||
| d1d46009cd | |||
| 3a4b6f0ea0 | |||
| b3d10ace21 | |||
| c17df7a6f7 | |||
| 1c13f5026e | |||
| b9cfede888 | |||
| 49fd9e90a0 | |||
| e09038232e | |||
| 2cfe310e89 | |||
| 973c7467e8 | |||
| 583df7ed7d | |||
| 6d05376f04 | |||
| e1f832bfa7 | |||
| b8092cd00b | |||
| 3c1dca6cef | |||
| c0f7dd6fe9 | |||
| 6af6e99480 | |||
| c5cbe48668 | |||
| 15707956ef | |||
| 4668fc87a1 | |||
| 468fb2cc41 | |||
| 7c79e7e836 | |||
| 925c6ffc3e | |||
| 0bf1f48623 | |||
| ffcb1c2513 | |||
| f286eb4d11 | |||
| 9346c83dc1 | |||
| a76267f5b0 | |||
| 1d3a7b3d52 | |||
| f78f04d553 | |||
| 7b6dabbe9c | |||
| ed01b3b8cf | |||
| 7880a30e57 | |||
| 3a3ff93450 | |||
| 3a1cdd37a3 | |||
| 8db38f8e75 | |||
| ff24ef4ee5 | |||
| 3faeec4add | |||
| 7d56ee5084 | |||
| b2afaabb8c | |||
| 3efaf90bc8 | |||
| 0c52887688 | |||
| 8aa1c1545e | |||
| 7c84f421c5 | |||
| 42a1dea7ad | |||
| d5e9155a33 | |||
| 5def5ab074 | |||
| 1b242e636b | |||
| 05f05c889a | |||
| 1367e285c8 | |||
| 45ec3e0bb9 | |||
| dc38f78da2 | |||
| 1b6a74fd93 | |||
| 9d8a1494aa | |||
| 08465cf236 | |||
| 7016848401 | |||
| bdd2a9e7e8 | |||
| 80256e6782 | |||
| 7907ef44f8 | |||
| 3a97a24686 | |||
| 7f208ed44e | |||
| 22e6cfaebb | |||
| 9d6f873048 | |||
| d526229a0f | |||
| aac68290ac | |||
| bd9a2c13eb | |||
| e5c65d53f8 | |||
| 121e9d0225 | |||
| c12a3b6610 | |||
| 43fee73924 | |||
| b72e9cb36c | |||
| 77d0a76186 | |||
| e89528315d | |||
| c34ccc9d53 | |||
| e51ba795f3 | |||
| 737dcc1d29 | |||
| dba08d230e | |||
| 15fb363874 | |||
| cbe2965849 | |||
| 59bfc45856 | |||
| ceb4581f91 | |||
| 07cc93cca2 | |||
| 1205178e26 | |||
| 8217c0f05f | |||
| c5c27b3cb0 | |||
| 04bbfae08e | |||
| b3efa73eda | |||
| f3efac059c | |||
| 9fb4ed2ec0 | |||
| f19013143a | |||
| ea3ee9bea5 | |||
| ccca6f4b6d | |||
| 6a583d2ba6 | |||
| 4049a32871 | |||
| 331c9ce1ff | |||
| 81ab2aca37 | |||
| 564b8276bf | |||
| b4a93d2dc3 | |||
| 260040b919 | |||
| 8dbef8b68e | |||
| 458b2d422d | |||
| ee51357dbc | |||
| fa679e873d | |||
| ed3fded8e8 | |||
| 92df82bfa9 | |||
| 0a9959bffb | |||
| b3a16cb852 | |||
| 9beb259333 | |||
| 63c57e8e02 | |||
| 0448a7ea68 | |||
| 5bd005b28a | |||
| 3aec6367d1 | |||
| cea3831c20 | |||
| 18ccceca2d | |||
| fffcdcb514 | |||
| efadf374d6 | |||
| 55ecb40190 | |||
| 01f6b3dfc6 | |||
| f4d1c5c006 | |||
| 72fd1e4e7c | |||
| f44e0a8e12 | |||
| 9338d9c2a6 | |||
| 75fc25feb5 | |||
| 5919874f6f | |||
| 213bb9dba2 | |||
| 3a9dc37d02 | |||
| 423c8a886d | |||
| 3ec8233a2d | |||
| 8ed51c806e | |||
| 57135a898f | |||
| 0d3d27a519 | |||
| cf42ad83da | |||
| e7bcb61a3b | |||
| 883b83f1da | |||
| 48977e6eaa | |||
| efe2488155 | |||
| 29c04b6f9c | |||
| 984b6234d2 | |||
| dac4a5452d | |||
| 5f9e82204a | |||
| c4142d93c3 | |||
| b34a2c7ee2 | |||
| cd7cc1b71f | |||
| 4c6dd564a4 | |||
| 28e46a82ea | |||
| 10e294784e | |||
| 2da725340c | |||
| 882d3a765d | |||
| e52e2f10bf | |||
| dfc19e79f1 | |||
| f59bd3da7a | |||
| 50791e3aa7 | |||
| 8211b2358f | |||
| f2e1f3393d | |||
| 0ffec0a32d | |||
| 1e5e705458 | |||
| f2af6ea60d | |||
| de9187fee2 | |||
| 5eed091185 | |||
| 06644b5748 | |||
| bb853f65e0 | |||
| eb830dd014 | |||
| de82d1e90c | |||
| 53e838083c | |||
| 975368de8f | |||
| 89173be055 | |||
| b376a7c399 | |||
| 2df262d877 | |||
| ffb0e27efa | |||
| e71c4b3bc4 | |||
| 85a0adb004 | |||
| f1475cd3d7 | |||
| 8c14812537 | |||
| 27aedf0563 | |||
| 95c2c1643e | |||
| f952f6742f | |||
| f3a10a8166 | |||
| 0790201cca | |||
| 5938c49453 | |||
| 14fb080f80 | |||
| 034b8db070 | |||
| d3ce0cb82f | |||
| 4dbda8dffd | |||
| 01f32e0f45 | |||
| 86c530e967 | |||
| dcd6626fe6 | |||
| 601cefe975 | |||
| 1984cf02cf | |||
| 4bdabbfbe9 |
@@ -1,11 +1,9 @@
|
||||
{
|
||||
"sourceMaps": true,
|
||||
"presets": [
|
||||
["@babel/preset-env", {
|
||||
"targets": {
|
||||
"browsers": [
|
||||
"last 2 versions"
|
||||
],
|
||||
"node": 12
|
||||
"node": 10
|
||||
},
|
||||
"modules": "commonjs"
|
||||
}],
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
steps:
|
||||
- label: ":eslint: Lint"
|
||||
command:
|
||||
- "yarn install"
|
||||
- "yarn lint"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
|
||||
- label: ":jest: Tests"
|
||||
command:
|
||||
- "yarn install"
|
||||
- "yarn test"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
|
||||
- label: "📃 Docs"
|
||||
command:
|
||||
- "yarn install"
|
||||
- "yarn gendoc"
|
||||
plugins:
|
||||
- docker#v3.0.1:
|
||||
image: "node:10"
|
||||
|
||||
- wait
|
||||
|
||||
- label: "🐴 Trigger matrix-react-sdk"
|
||||
trigger: "matrix-react-sdk"
|
||||
branches: "develop"
|
||||
build:
|
||||
branch: "develop"
|
||||
message: "[js-sdk] ${BUILDKITE_MESSAGE}"
|
||||
async: true
|
||||
+23
-68
@@ -1,71 +1,15 @@
|
||||
module.exports = {
|
||||
parser: "babel-eslint", // now needed for class properties
|
||||
parserOptions: {
|
||||
sourceType: "module",
|
||||
ecmaFeatures: {
|
||||
}
|
||||
},
|
||||
extends: ["matrix-org"],
|
||||
plugins: [
|
||||
"babel",
|
||||
],
|
||||
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 +23,21 @@ 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": ["matrix-org/ts"],
|
||||
"rules": {
|
||||
// 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
|
||||
|
||||
+955
@@ -1,3 +1,958 @@
|
||||
Changes in [9.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.1.0-rc.1) (2020-11-04)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.0.1...v9.1.0-rc.1)
|
||||
|
||||
* Fix spelling error in the server ACL event type
|
||||
[\#1535](https://github.com/matrix-org/matrix-js-sdk/pull/1535)
|
||||
* await idb operations from crypto store for dehydration
|
||||
[\#1533](https://github.com/matrix-org/matrix-js-sdk/pull/1533)
|
||||
* Fix stuck never-sending messages
|
||||
[\#1531](https://github.com/matrix-org/matrix-js-sdk/pull/1531)
|
||||
* Await key cache check to avoid prompts
|
||||
[\#1529](https://github.com/matrix-org/matrix-js-sdk/pull/1529)
|
||||
* Improve ICE candidate batching
|
||||
[\#1524](https://github.com/matrix-org/matrix-js-sdk/pull/1524)
|
||||
* Convert logger to typescript
|
||||
[\#1527](https://github.com/matrix-org/matrix-js-sdk/pull/1527)
|
||||
* Fix logger typo
|
||||
[\#1525](https://github.com/matrix-org/matrix-js-sdk/pull/1525)
|
||||
* bind online listener to window instead of document
|
||||
[\#1523](https://github.com/matrix-org/matrix-js-sdk/pull/1523)
|
||||
* Support m.call.select_answer
|
||||
[\#1522](https://github.com/matrix-org/matrix-js-sdk/pull/1522)
|
||||
|
||||
Changes in [9.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.0.1) (2020-10-28)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.0.0...v9.0.1)
|
||||
|
||||
* [Release] Await key cache check to avoid prompts
|
||||
[\#1530](https://github.com/matrix-org/matrix-js-sdk/pull/1530)
|
||||
|
||||
Changes in [9.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.0.0) (2020-10-26)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v9.0.0-rc.1...v9.0.0)
|
||||
|
||||
* Fix logger typo
|
||||
[\#1528](https://github.com/matrix-org/matrix-js-sdk/pull/1528)
|
||||
|
||||
Changes in [9.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v9.0.0-rc.1) (2020-10-21)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.5.0...v9.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* `hasPendingEvent` now returns false instead of throwing when pending ordering mode is not `detached`
|
||||
|
||||
All changes
|
||||
---
|
||||
|
||||
* Don't cache failures when fetching /versions
|
||||
[\#1521](https://github.com/matrix-org/matrix-js-sdk/pull/1521)
|
||||
* Install deps first as part of release
|
||||
[\#1518](https://github.com/matrix-org/matrix-js-sdk/pull/1518)
|
||||
* [Breaking] Change hasPendingEvent to return false if pending ordering
|
||||
!detached
|
||||
[\#1517](https://github.com/matrix-org/matrix-js-sdk/pull/1517)
|
||||
* Skip editor prompts for merges
|
||||
[\#1519](https://github.com/matrix-org/matrix-js-sdk/pull/1519)
|
||||
* Convert call test to TypeScript
|
||||
[\#1516](https://github.com/matrix-org/matrix-js-sdk/pull/1516)
|
||||
* Support party_id
|
||||
[\#1512](https://github.com/matrix-org/matrix-js-sdk/pull/1512)
|
||||
* Support m.call.reject
|
||||
[\#1510](https://github.com/matrix-org/matrix-js-sdk/pull/1510)
|
||||
* Remove specbuild from .gitignore
|
||||
[\#1515](https://github.com/matrix-org/matrix-js-sdk/pull/1515)
|
||||
* Log the error when we failed to send candidates
|
||||
[\#1514](https://github.com/matrix-org/matrix-js-sdk/pull/1514)
|
||||
* Fixes for call state machine
|
||||
[\#1503](https://github.com/matrix-org/matrix-js-sdk/pull/1503)
|
||||
* Fix call event handler listener removing
|
||||
[\#1506](https://github.com/matrix-org/matrix-js-sdk/pull/1506)
|
||||
* Set the type of the call based on the tracks
|
||||
[\#1501](https://github.com/matrix-org/matrix-js-sdk/pull/1501)
|
||||
* Use new local timestamp for calls
|
||||
[\#1499](https://github.com/matrix-org/matrix-js-sdk/pull/1499)
|
||||
* Adjust types and APIs to match React SDK
|
||||
[\#1502](https://github.com/matrix-org/matrix-js-sdk/pull/1502)
|
||||
* Make an accurate version of 'age' for events
|
||||
[\#1495](https://github.com/matrix-org/matrix-js-sdk/pull/1495)
|
||||
* Make 'options' parameter optional
|
||||
[\#1498](https://github.com/matrix-org/matrix-js-sdk/pull/1498)
|
||||
* Create a giant event type enum
|
||||
[\#1497](https://github.com/matrix-org/matrix-js-sdk/pull/1497)
|
||||
* Convert call.js to Typescript & update WebRTC APIs (re-apply)
|
||||
[\#1494](https://github.com/matrix-org/matrix-js-sdk/pull/1494)
|
||||
|
||||
Changes in [8.5.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.5.0) (2020-10-12)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.5.0-rc.1...v8.5.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [8.5.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.5.0-rc.1) (2020-10-07)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.4.1...v8.5.0-rc.1)
|
||||
|
||||
* Add support for olm fallback keys
|
||||
[\#1467](https://github.com/matrix-org/matrix-js-sdk/pull/1467)
|
||||
* Fix editing local echoes not updating them in real time
|
||||
[\#1492](https://github.com/matrix-org/matrix-js-sdk/pull/1492)
|
||||
* Fix re-emit of Event.replaced to be on client and not room
|
||||
[\#1491](https://github.com/matrix-org/matrix-js-sdk/pull/1491)
|
||||
* Add space to log line
|
||||
[\#1496](https://github.com/matrix-org/matrix-js-sdk/pull/1496)
|
||||
* Revert "Convert call.js to Typescript & update WebRTC APIs"
|
||||
[\#1493](https://github.com/matrix-org/matrix-js-sdk/pull/1493)
|
||||
* Convert call.js to Typescript & update WebRTC APIs
|
||||
[\#1487](https://github.com/matrix-org/matrix-js-sdk/pull/1487)
|
||||
* Dehydrate and rehydrate devices
|
||||
[\#1436](https://github.com/matrix-org/matrix-js-sdk/pull/1436)
|
||||
* Keep local device after processing device list sync
|
||||
[\#1490](https://github.com/matrix-org/matrix-js-sdk/pull/1490)
|
||||
* Enforce logger module via lint rules
|
||||
[\#1489](https://github.com/matrix-org/matrix-js-sdk/pull/1489)
|
||||
* Extend method redactEvent with reason
|
||||
[\#1462](https://github.com/matrix-org/matrix-js-sdk/pull/1462)
|
||||
* Catch exception from call event handler
|
||||
[\#1484](https://github.com/matrix-org/matrix-js-sdk/pull/1484)
|
||||
* Ignore invalid candidates
|
||||
[\#1483](https://github.com/matrix-org/matrix-js-sdk/pull/1483)
|
||||
* Always push docs if they are generated
|
||||
[\#1478](https://github.com/matrix-org/matrix-js-sdk/pull/1478)
|
||||
* Only sign key backup with cross-signing keys when available
|
||||
[\#1481](https://github.com/matrix-org/matrix-js-sdk/pull/1481)
|
||||
* Upgrade dependencies
|
||||
[\#1479](https://github.com/matrix-org/matrix-js-sdk/pull/1479)
|
||||
|
||||
Changes in [8.4.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.4.1) (2020-09-28)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.4.0...v8.4.1)
|
||||
|
||||
* Catch exception from call event handler
|
||||
[\#1486](https://github.com/matrix-org/matrix-js-sdk/pull/1486)
|
||||
* Ignore invalid candidates
|
||||
[\#1485](https://github.com/matrix-org/matrix-js-sdk/pull/1485)
|
||||
|
||||
Changes in [8.4.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.4.0) (2020-09-28)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.4.0-rc.1...v8.4.0)
|
||||
|
||||
* Only sign key backup with cross-signing keys when available
|
||||
[\#1482](https://github.com/matrix-org/matrix-js-sdk/pull/1482)
|
||||
|
||||
Changes in [8.4.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.4.0-rc.1) (2020-09-23)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.3.0...v8.4.0-rc.1)
|
||||
|
||||
* If there are extraParams set, ensure that queryParams is defined
|
||||
[\#1477](https://github.com/matrix-org/matrix-js-sdk/pull/1477)
|
||||
* Add diagnostics to security bootstrap paths
|
||||
[\#1475](https://github.com/matrix-org/matrix-js-sdk/pull/1475)
|
||||
* Switch to a combination of better-docs and docdash
|
||||
[\#1459](https://github.com/matrix-org/matrix-js-sdk/pull/1459)
|
||||
* Undo attempts to cache private keys aggressively
|
||||
[\#1474](https://github.com/matrix-org/matrix-js-sdk/pull/1474)
|
||||
* Repair secret storage reset, cache keys when missing
|
||||
[\#1472](https://github.com/matrix-org/matrix-js-sdk/pull/1472)
|
||||
* Prevent parallel getVersions calls
|
||||
[\#1471](https://github.com/matrix-org/matrix-js-sdk/pull/1471)
|
||||
* Send end-of-candidates
|
||||
[\#1473](https://github.com/matrix-org/matrix-js-sdk/pull/1473)
|
||||
* Add a function for checking the /versions flag for forced e2ee
|
||||
[\#1470](https://github.com/matrix-org/matrix-js-sdk/pull/1470)
|
||||
* Add option to allow users of pantialaimon to use the SDK
|
||||
[\#1469](https://github.com/matrix-org/matrix-js-sdk/pull/1469)
|
||||
* Fixed Yarn broken link
|
||||
[\#1468](https://github.com/matrix-org/matrix-js-sdk/pull/1468)
|
||||
* some TypeScript and doc fixes
|
||||
[\#1466](https://github.com/matrix-org/matrix-js-sdk/pull/1466)
|
||||
* Remove Travis CI reference
|
||||
[\#1464](https://github.com/matrix-org/matrix-js-sdk/pull/1464)
|
||||
* Inject identity server token for 3pid invites on createRoom
|
||||
[\#1463](https://github.com/matrix-org/matrix-js-sdk/pull/1463)
|
||||
|
||||
Changes in [8.3.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.3.0) (2020-09-14)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.3.0-rc.1...v8.3.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [8.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.3.0-rc.1) (2020-09-09)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.2.0...v8.3.0-rc.1)
|
||||
|
||||
* Add missing options in ICreateClientOpts
|
||||
[\#1452](https://github.com/matrix-org/matrix-js-sdk/pull/1452)
|
||||
* Ensure ready functions return boolean values
|
||||
[\#1457](https://github.com/matrix-org/matrix-js-sdk/pull/1457)
|
||||
* Handle missing cross-signing keys gracefully
|
||||
[\#1456](https://github.com/matrix-org/matrix-js-sdk/pull/1456)
|
||||
* Fix eslint ts override tsx matching
|
||||
[\#1451](https://github.com/matrix-org/matrix-js-sdk/pull/1451)
|
||||
* Untangle cross-signing and secret storage
|
||||
[\#1450](https://github.com/matrix-org/matrix-js-sdk/pull/1450)
|
||||
|
||||
Changes in [8.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.2.0) (2020-09-01)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.2.0-rc.1...v8.2.0)
|
||||
|
||||
## Security notice
|
||||
|
||||
JS SDK 8.2.0 fixes an issue where encrypted state events could break incoming call handling.
|
||||
Thanks to @awesome-michael from Awesome Technologies for responsibly disclosing this via Matrix's
|
||||
Security Disclosure Policy.
|
||||
|
||||
## All changes
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [8.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.2.0-rc.1) (2020-08-26)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.1.0...v8.2.0-rc.1)
|
||||
|
||||
* Add state event check
|
||||
[\#1449](https://github.com/matrix-org/matrix-js-sdk/pull/1449)
|
||||
* Add method to check whether client .well-known has been fetched
|
||||
[\#1444](https://github.com/matrix-org/matrix-js-sdk/pull/1444)
|
||||
* Handle auth errors during cross-signing key upload
|
||||
[\#1443](https://github.com/matrix-org/matrix-js-sdk/pull/1443)
|
||||
* Don't fail if the requested audio output isn't available
|
||||
[\#1448](https://github.com/matrix-org/matrix-js-sdk/pull/1448)
|
||||
* Fix logging failures
|
||||
[\#1447](https://github.com/matrix-org/matrix-js-sdk/pull/1447)
|
||||
* Log the constraints we pass to getUserMedia
|
||||
[\#1446](https://github.com/matrix-org/matrix-js-sdk/pull/1446)
|
||||
* Use SAS emoji data from matrix-doc
|
||||
[\#1440](https://github.com/matrix-org/matrix-js-sdk/pull/1440)
|
||||
|
||||
Changes in [8.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.1.0) (2020-08-17)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.1.0-rc.1...v8.1.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [8.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.1.0-rc.1) (2020-08-13)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.0.1...v8.1.0-rc.1)
|
||||
|
||||
* Update on Promises
|
||||
[\#1438](https://github.com/matrix-org/matrix-js-sdk/pull/1438)
|
||||
* Store and request master cross-signing key
|
||||
[\#1437](https://github.com/matrix-org/matrix-js-sdk/pull/1437)
|
||||
* Filter out non-string display names
|
||||
[\#1433](https://github.com/matrix-org/matrix-js-sdk/pull/1433)
|
||||
* Bump elliptic from 6.5.2 to 6.5.3
|
||||
[\#1427](https://github.com/matrix-org/matrix-js-sdk/pull/1427)
|
||||
* Replace Riot with Element in docs and comments
|
||||
[\#1431](https://github.com/matrix-org/matrix-js-sdk/pull/1431)
|
||||
* Remove leftover bits of TSLint
|
||||
[\#1430](https://github.com/matrix-org/matrix-js-sdk/pull/1430)
|
||||
|
||||
Changes in [8.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.0.1) (2020-08-05)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.0.1-rc.1...v8.0.1)
|
||||
|
||||
* Filter out non-string display names
|
||||
[\#1434](https://github.com/matrix-org/matrix-js-sdk/pull/1434)
|
||||
|
||||
Changes in [8.0.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.0.1-rc.1) (2020-07-31)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v8.0.0...v8.0.1-rc.1)
|
||||
|
||||
* Remove redundant lint dependencies
|
||||
[\#1426](https://github.com/matrix-org/matrix-js-sdk/pull/1426)
|
||||
* Upload all keys when we start using a new key backup version
|
||||
[\#1428](https://github.com/matrix-org/matrix-js-sdk/pull/1428)
|
||||
* Expose countSessionsNeedingBackup
|
||||
[\#1429](https://github.com/matrix-org/matrix-js-sdk/pull/1429)
|
||||
* Configure and use new eslint package
|
||||
[\#1422](https://github.com/matrix-org/matrix-js-sdk/pull/1422)
|
||||
|
||||
Changes in [8.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v8.0.0) (2020-07-27)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.1.0...v8.0.0)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* `RoomState` events changed to use a Map instead of an object, which changes the collection APIs available to access them.
|
||||
|
||||
All Changes
|
||||
---
|
||||
|
||||
* Properly support txnId
|
||||
[\#1424](https://github.com/matrix-org/matrix-js-sdk/pull/1424)
|
||||
* [BREAKING] Remove deprecated getIdenticonUri
|
||||
[\#1423](https://github.com/matrix-org/matrix-js-sdk/pull/1423)
|
||||
* Bump lodash from 4.17.15 to 4.17.19
|
||||
[\#1421](https://github.com/matrix-org/matrix-js-sdk/pull/1421)
|
||||
* [BREAKING] Convert RoomState's stored state map to a real map
|
||||
[\#1419](https://github.com/matrix-org/matrix-js-sdk/pull/1419)
|
||||
|
||||
Changes in [7.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.1.0) (2020-07-03)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.1.0-rc.1...v7.1.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [7.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.1.0-rc.1) (2020-07-01)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.0.0...v7.1.0-rc.1)
|
||||
|
||||
* Ask general crypto callbacks for 4S privkey if operation adapter doesn't
|
||||
have it yet
|
||||
[\#1414](https://github.com/matrix-org/matrix-js-sdk/pull/1414)
|
||||
* Fix ICreateClientOpts missing idBaseUrl
|
||||
[\#1413](https://github.com/matrix-org/matrix-js-sdk/pull/1413)
|
||||
* Increase max event listeners for rooms
|
||||
[\#1411](https://github.com/matrix-org/matrix-js-sdk/pull/1411)
|
||||
* Don't trust keys megolm received from backup for verifying the sender
|
||||
[\#1406](https://github.com/matrix-org/matrix-js-sdk/pull/1406)
|
||||
* Raise the last known account data / state event for an update
|
||||
[\#1410](https://github.com/matrix-org/matrix-js-sdk/pull/1410)
|
||||
* Isolate encryption bootstrap side-effects
|
||||
[\#1380](https://github.com/matrix-org/matrix-js-sdk/pull/1380)
|
||||
* Add method to get current in-flight to-device requests
|
||||
[\#1405](https://github.com/matrix-org/matrix-js-sdk/pull/1405)
|
||||
|
||||
Changes in [7.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.0.0) (2020-06-23)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v7.0.0-rc.1...v7.0.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [7.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v7.0.0-rc.1) (2020-06-17)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.2.2...v7.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* Presence lists were removed from the spec in r0.5.0, and the corresponding methods have now been removed here as well:
|
||||
* `getPresenceList`
|
||||
* `inviteToPresenceList`
|
||||
* `dropFromPresenceList`
|
||||
|
||||
All changes
|
||||
---
|
||||
|
||||
* Remove support for unspecced device-specific push rules
|
||||
[\#1404](https://github.com/matrix-org/matrix-js-sdk/pull/1404)
|
||||
* Use existing session id for fetching flows as to not get a new session
|
||||
[\#1403](https://github.com/matrix-org/matrix-js-sdk/pull/1403)
|
||||
* Upgrade deps
|
||||
[\#1400](https://github.com/matrix-org/matrix-js-sdk/pull/1400)
|
||||
* Bring back backup key format migration
|
||||
[\#1398](https://github.com/matrix-org/matrix-js-sdk/pull/1398)
|
||||
* Fix: more informative error message when we cant find a key to decrypt with
|
||||
[\#1313](https://github.com/matrix-org/matrix-js-sdk/pull/1313)
|
||||
* Add js-sdk mechanism for polling client well-known for config
|
||||
[\#1394](https://github.com/matrix-org/matrix-js-sdk/pull/1394)
|
||||
* Fix verification request timeouts to match spec
|
||||
[\#1388](https://github.com/matrix-org/matrix-js-sdk/pull/1388)
|
||||
* Drop presence list methods
|
||||
[\#1391](https://github.com/matrix-org/matrix-js-sdk/pull/1391)
|
||||
* Batch up URL previews to prevent excessive requests
|
||||
[\#1395](https://github.com/matrix-org/matrix-js-sdk/pull/1395)
|
||||
|
||||
Changes in [6.2.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.2.2) (2020-06-16)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.2.1...v6.2.2)
|
||||
|
||||
* Use existing session id for fetching flows as to not get a new session
|
||||
[\#1407](https://github.com/matrix-org/matrix-js-sdk/pull/1407)
|
||||
|
||||
Changes in [6.2.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.2.1) (2020-06-05)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.2.0...v6.2.1)
|
||||
|
||||
* Bring back backup key format migration
|
||||
[\#1399](https://github.com/matrix-org/matrix-js-sdk/pull/1399)
|
||||
|
||||
Changes in [6.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.2.0) (2020-06-04)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.2.0-rc.1...v6.2.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [6.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.2.0-rc.1) (2020-06-02)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.1.0...v6.2.0-rc.1)
|
||||
|
||||
* Make auth argument in the register request compliant with r0.6.0
|
||||
[\#1304](https://github.com/matrix-org/matrix-js-sdk/pull/1304)
|
||||
* Send the wrong auth params with the right auth params
|
||||
[\#1393](https://github.com/matrix-org/matrix-js-sdk/pull/1393)
|
||||
* encrypt cached keys with pickle key
|
||||
[\#1387](https://github.com/matrix-org/matrix-js-sdk/pull/1387)
|
||||
* Fix replying to key share requests
|
||||
[\#1385](https://github.com/matrix-org/matrix-js-sdk/pull/1385)
|
||||
* Add dist to package.json files so CDNs can serve it
|
||||
[\#1384](https://github.com/matrix-org/matrix-js-sdk/pull/1384)
|
||||
* Fix getVersion warning saying undefined room
|
||||
[\#1382](https://github.com/matrix-org/matrix-js-sdk/pull/1382)
|
||||
* Combine the two places we processed client-level default push rules
|
||||
[\#1379](https://github.com/matrix-org/matrix-js-sdk/pull/1379)
|
||||
* make MAC check robust against unpadded vs padded base64 differences
|
||||
[\#1378](https://github.com/matrix-org/matrix-js-sdk/pull/1378)
|
||||
* Remove key backup format migration
|
||||
[\#1375](https://github.com/matrix-org/matrix-js-sdk/pull/1375)
|
||||
* Add simple browserify browser-matrix.js tests
|
||||
[\#1241](https://github.com/matrix-org/matrix-js-sdk/pull/1241)
|
||||
* support new key agreement method for SAS
|
||||
[\#1376](https://github.com/matrix-org/matrix-js-sdk/pull/1376)
|
||||
|
||||
Changes in [6.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.1.0) (2020-05-19)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.1.0-rc.1...v6.1.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [6.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.1.0-rc.1) (2020-05-14)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.0.0...v6.1.0-rc.1)
|
||||
|
||||
* Remove support for asymmetric 4S encryption
|
||||
[\#1373](https://github.com/matrix-org/matrix-js-sdk/pull/1373)
|
||||
* Increase timeout for 2nd phase of Olm session creation
|
||||
[\#1367](https://github.com/matrix-org/matrix-js-sdk/pull/1367)
|
||||
* Add logging on decryption retries
|
||||
[\#1366](https://github.com/matrix-org/matrix-js-sdk/pull/1366)
|
||||
* Emit event when a trusted self-key is stored
|
||||
[\#1364](https://github.com/matrix-org/matrix-js-sdk/pull/1364)
|
||||
* Customize error payload for oversized messages
|
||||
[\#1352](https://github.com/matrix-org/matrix-js-sdk/pull/1352)
|
||||
* Return null for key backup state when we haven't checked yet
|
||||
[\#1363](https://github.com/matrix-org/matrix-js-sdk/pull/1363)
|
||||
* Added a progressCallback for backup key loading
|
||||
[\#1351](https://github.com/matrix-org/matrix-js-sdk/pull/1351)
|
||||
* Add initialFetch param to willUpdateDevices / devicesUpdated
|
||||
[\#1360](https://github.com/matrix-org/matrix-js-sdk/pull/1360)
|
||||
* Fix race between sending .request and receiving .ready over to_device
|
||||
[\#1359](https://github.com/matrix-org/matrix-js-sdk/pull/1359)
|
||||
* Handle race between sending and await next event from other party
|
||||
[\#1357](https://github.com/matrix-org/matrix-js-sdk/pull/1357)
|
||||
* Add crypto.willUpdateDevices event and make
|
||||
getStoredDevices/getStoredDevicesForUser synchronous
|
||||
[\#1354](https://github.com/matrix-org/matrix-js-sdk/pull/1354)
|
||||
* Fix sender of local echo events in unsigned redactions
|
||||
[\#1350](https://github.com/matrix-org/matrix-js-sdk/pull/1350)
|
||||
* Remove redundant key backup setup path
|
||||
[\#1353](https://github.com/matrix-org/matrix-js-sdk/pull/1353)
|
||||
* Remove some dead code from _retryDecryption
|
||||
[\#1349](https://github.com/matrix-org/matrix-js-sdk/pull/1349)
|
||||
* Don't send key requests until after sync processing is finished
|
||||
[\#1348](https://github.com/matrix-org/matrix-js-sdk/pull/1348)
|
||||
* Prevent attempts to send olm messages to ourselves
|
||||
[\#1346](https://github.com/matrix-org/matrix-js-sdk/pull/1346)
|
||||
* Retry account data upload requests
|
||||
[\#1345](https://github.com/matrix-org/matrix-js-sdk/pull/1345)
|
||||
* Log first known index with megolm session updates
|
||||
[\#1344](https://github.com/matrix-org/matrix-js-sdk/pull/1344)
|
||||
* Prune to_device messages to avoid sending empty messages
|
||||
[\#1343](https://github.com/matrix-org/matrix-js-sdk/pull/1343)
|
||||
* Convert bunch of things to TypeScript
|
||||
[\#1335](https://github.com/matrix-org/matrix-js-sdk/pull/1335)
|
||||
* Add logging when making new Olm sessions
|
||||
[\#1342](https://github.com/matrix-org/matrix-js-sdk/pull/1342)
|
||||
* Fix: handle filter not found
|
||||
[\#1340](https://github.com/matrix-org/matrix-js-sdk/pull/1340)
|
||||
* Make getAccountDataFromServer return null if not found
|
||||
[\#1338](https://github.com/matrix-org/matrix-js-sdk/pull/1338)
|
||||
* Fix setDefaultKeyId to fail if the request fails
|
||||
[\#1336](https://github.com/matrix-org/matrix-js-sdk/pull/1336)
|
||||
* Document setRoomEncryption not modifying room state
|
||||
[\#1328](https://github.com/matrix-org/matrix-js-sdk/pull/1328)
|
||||
* Fix: don't do extra /filter request when enabling lazy loading of members
|
||||
[\#1332](https://github.com/matrix-org/matrix-js-sdk/pull/1332)
|
||||
* Reject attemptAuth promise if no auth flow found
|
||||
[\#1329](https://github.com/matrix-org/matrix-js-sdk/pull/1329)
|
||||
* Fix FilterComponent allowed_values check
|
||||
[\#1327](https://github.com/matrix-org/matrix-js-sdk/pull/1327)
|
||||
* Serialise Olm prekey decryptions
|
||||
[\#1326](https://github.com/matrix-org/matrix-js-sdk/pull/1326)
|
||||
* Fix: crash when backup key needs fixing from corruption issue
|
||||
[\#1324](https://github.com/matrix-org/matrix-js-sdk/pull/1324)
|
||||
* Fix cross-signing/SSSS reset
|
||||
[\#1322](https://github.com/matrix-org/matrix-js-sdk/pull/1322)
|
||||
* Implement QR code reciprocate for self-verification with untrusted MSK
|
||||
[\#1320](https://github.com/matrix-org/matrix-js-sdk/pull/1320)
|
||||
|
||||
Changes in [6.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.0.0) (2020-05-05)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.0.0-rc.2...v6.0.0)
|
||||
|
||||
* Add progress callback for key backups
|
||||
[\#1368](https://github.com/matrix-org/matrix-js-sdk/pull/1368)
|
||||
|
||||
Changes in [6.0.0-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.0.0-rc.2) (2020-05-01)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v6.0.0-rc.1...v6.0.0-rc.2)
|
||||
|
||||
* Emit event when a trusted self-key is stored
|
||||
[\#1365](https://github.com/matrix-org/matrix-js-sdk/pull/1365)
|
||||
|
||||
Changes in [6.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v6.0.0-rc.1) (2020-04-30)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.1-rc.4...v6.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* client.getStoredDevicesForUser and client.getStoredDevices are no longer async
|
||||
|
||||
All Changes
|
||||
---
|
||||
|
||||
* Add initialFetch param to willUpdateDevices / devicesUpdated
|
||||
[\#1362](https://github.com/matrix-org/matrix-js-sdk/pull/1362)
|
||||
* Fix race between sending .request and receiving .ready over to_device
|
||||
[\#1361](https://github.com/matrix-org/matrix-js-sdk/pull/1361)
|
||||
* Handle race between sending and await next event from other party
|
||||
[\#1358](https://github.com/matrix-org/matrix-js-sdk/pull/1358)
|
||||
* Add crypto.willUpdateDevices event and make
|
||||
getStoredDevices/getStoredDevicesForUser synchronous
|
||||
[\#1356](https://github.com/matrix-org/matrix-js-sdk/pull/1356)
|
||||
* Remove redundant key backup setup path
|
||||
[\#1355](https://github.com/matrix-org/matrix-js-sdk/pull/1355)
|
||||
|
||||
Changes in [5.3.1-rc.4](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.1-rc.4) (2020-04-23)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.1-rc.3...v5.3.1-rc.4)
|
||||
|
||||
* Retry account data upload requests
|
||||
[\#1347](https://github.com/matrix-org/matrix-js-sdk/pull/1347)
|
||||
* Fix: handle filter not found
|
||||
[\#1341](https://github.com/matrix-org/matrix-js-sdk/pull/1341)
|
||||
* Make getAccountDataFromServer return null if not found
|
||||
[\#1339](https://github.com/matrix-org/matrix-js-sdk/pull/1339)
|
||||
* Fix setDefaultKeyId to fail if the request fails
|
||||
[\#1337](https://github.com/matrix-org/matrix-js-sdk/pull/1337)
|
||||
* Fix: don't do extra /filter request when enabling lazy loading of members
|
||||
[\#1333](https://github.com/matrix-org/matrix-js-sdk/pull/1333)
|
||||
* Reject attemptAuth promise if no auth flow found
|
||||
[\#1331](https://github.com/matrix-org/matrix-js-sdk/pull/1331)
|
||||
* Serialise Olm prekey decryptions
|
||||
[\#1330](https://github.com/matrix-org/matrix-js-sdk/pull/1330)
|
||||
|
||||
Changes in [5.3.1-rc.3](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.1-rc.3) (2020-04-17)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.1-rc.2...v5.3.1-rc.3)
|
||||
|
||||
* Fix cross-signing/SSSS reset
|
||||
[\#1323](https://github.com/matrix-org/matrix-js-sdk/pull/1323)
|
||||
* Fix: crash when backup key needs fixing from corruption issue
|
||||
[\#1325](https://github.com/matrix-org/matrix-js-sdk/pull/1325)
|
||||
|
||||
Changes in [5.3.1-rc.2](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.1-rc.2) (2020-04-16)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.1-rc.1...v5.3.1-rc.2)
|
||||
|
||||
* Implement QR code reciprocate for self-verification with untrusted MSK
|
||||
[\#1321](https://github.com/matrix-org/matrix-js-sdk/pull/1321)
|
||||
|
||||
Changes in [5.3.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.1-rc.1) (2020-04-15)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.3.0-rc.1...v5.3.1-rc.1)
|
||||
|
||||
* Adapt release script for riot-desktop
|
||||
[\#1319](https://github.com/matrix-org/matrix-js-sdk/pull/1319)
|
||||
* Fix: prevent spurious notifications from indexer
|
||||
[\#1318](https://github.com/matrix-org/matrix-js-sdk/pull/1318)
|
||||
* Always create our own user object
|
||||
[\#1317](https://github.com/matrix-org/matrix-js-sdk/pull/1317)
|
||||
* Fix incorrect backup key format in SSSS
|
||||
[\#1311](https://github.com/matrix-org/matrix-js-sdk/pull/1311)
|
||||
* Fix e2ee crash after refreshing after having received a cross-singing key
|
||||
reset
|
||||
[\#1315](https://github.com/matrix-org/matrix-js-sdk/pull/1315)
|
||||
* Fix: catch send errors in SAS verifier
|
||||
[\#1314](https://github.com/matrix-org/matrix-js-sdk/pull/1314)
|
||||
* Clear cross-signing keys when detecting the keys have changed
|
||||
[\#1312](https://github.com/matrix-org/matrix-js-sdk/pull/1312)
|
||||
* Upgrade deps
|
||||
[\#1310](https://github.com/matrix-org/matrix-js-sdk/pull/1310)
|
||||
|
||||
Changes in [5.3.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.3.0-rc.1) (2020-04-08)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.2.0...v5.3.0-rc.1)
|
||||
|
||||
* Store key backup key in cache as Uint8Array
|
||||
[\#1308](https://github.com/matrix-org/matrix-js-sdk/pull/1308)
|
||||
* Use the correct request body for the /keys/query endpoint.
|
||||
[\#1307](https://github.com/matrix-org/matrix-js-sdk/pull/1307)
|
||||
* Avoid creating two devices on registration
|
||||
[\#1305](https://github.com/matrix-org/matrix-js-sdk/pull/1305)
|
||||
* Lower max-warnings to 81
|
||||
[\#1306](https://github.com/matrix-org/matrix-js-sdk/pull/1306)
|
||||
* Move key backup key creation before caching
|
||||
[\#1303](https://github.com/matrix-org/matrix-js-sdk/pull/1303)
|
||||
* Expose function to force-reset outgoing room key requests
|
||||
[\#1298](https://github.com/matrix-org/matrix-js-sdk/pull/1298)
|
||||
* Add isSelfVerification property to VerificationRequest
|
||||
[\#1302](https://github.com/matrix-org/matrix-js-sdk/pull/1302)
|
||||
* QR code reciprocation
|
||||
[\#1297](https://github.com/matrix-org/matrix-js-sdk/pull/1297)
|
||||
* Add ability to check symmetric SSSS key before we try to use it
|
||||
[\#1294](https://github.com/matrix-org/matrix-js-sdk/pull/1294)
|
||||
* Add some debug logging for events stuck to bottom of timeline
|
||||
[\#1296](https://github.com/matrix-org/matrix-js-sdk/pull/1296)
|
||||
* Fix: spontanous verification request cancellation under some circumstances
|
||||
[\#1295](https://github.com/matrix-org/matrix-js-sdk/pull/1295)
|
||||
* Receive private key for caching from the app layer
|
||||
[\#1293](https://github.com/matrix-org/matrix-js-sdk/pull/1293)
|
||||
* Track whether we have verified a user before
|
||||
[\#1292](https://github.com/matrix-org/matrix-js-sdk/pull/1292)
|
||||
* Fix: error during tests
|
||||
[\#1222](https://github.com/matrix-org/matrix-js-sdk/pull/1222)
|
||||
* Send .done event for to_device verification
|
||||
[\#1288](https://github.com/matrix-org/matrix-js-sdk/pull/1288)
|
||||
* Request the key backup key & restore backup
|
||||
[\#1291](https://github.com/matrix-org/matrix-js-sdk/pull/1291)
|
||||
* Make screen sharing works on Chrome using getDisplayMedia()
|
||||
[\#1276](https://github.com/matrix-org/matrix-js-sdk/pull/1276)
|
||||
* Fix isVerified returning false
|
||||
[\#1289](https://github.com/matrix-org/matrix-js-sdk/pull/1289)
|
||||
* Fix: verification gets cancelled when event gets duplicated
|
||||
[\#1286](https://github.com/matrix-org/matrix-js-sdk/pull/1286)
|
||||
* Use requestSecret on the client to request secrets
|
||||
[\#1287](https://github.com/matrix-org/matrix-js-sdk/pull/1287)
|
||||
* Allow guests to fetch TURN servers
|
||||
[\#1277](https://github.com/matrix-org/matrix-js-sdk/pull/1277)
|
||||
|
||||
Changes in [5.2.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.2.0) (2020-03-30)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.2.0-rc.1...v5.2.0)
|
||||
|
||||
* Fix isVerified returning false
|
||||
[\#1290](https://github.com/matrix-org/matrix-js-sdk/pull/1290)
|
||||
|
||||
Changes in [5.2.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.2.0-rc.1) (2020-03-26)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.1...v5.2.0-rc.1)
|
||||
|
||||
* Add a flag for whether cross signing signatures are trusted
|
||||
[\#1285](https://github.com/matrix-org/matrix-js-sdk/pull/1285)
|
||||
* Cache user and self signing keys during bootstrap
|
||||
[\#1282](https://github.com/matrix-org/matrix-js-sdk/pull/1282)
|
||||
* remove unnecessary promise
|
||||
[\#1283](https://github.com/matrix-org/matrix-js-sdk/pull/1283)
|
||||
* Functions to cache session backups key automatically
|
||||
[\#1281](https://github.com/matrix-org/matrix-js-sdk/pull/1281)
|
||||
* Add function for checking cross-signing is ready
|
||||
[\#1279](https://github.com/matrix-org/matrix-js-sdk/pull/1279)
|
||||
* Use symmetric encryption for SSSS
|
||||
[\#1228](https://github.com/matrix-org/matrix-js-sdk/pull/1228)
|
||||
* Migrate SSSS to use symmetric algorithm
|
||||
[\#1238](https://github.com/matrix-org/matrix-js-sdk/pull/1238)
|
||||
* Migration to symmetric SSSS
|
||||
[\#1272](https://github.com/matrix-org/matrix-js-sdk/pull/1272)
|
||||
* Reduce number of one-time-key requests
|
||||
[\#1280](https://github.com/matrix-org/matrix-js-sdk/pull/1280)
|
||||
* Fix: assume the requested method is supported by other party with to_device
|
||||
[\#1275](https://github.com/matrix-org/matrix-js-sdk/pull/1275)
|
||||
* Use checkDeviceTrust when computing untrusted devices
|
||||
[\#1278](https://github.com/matrix-org/matrix-js-sdk/pull/1278)
|
||||
* Add a store for backup keys
|
||||
[\#1271](https://github.com/matrix-org/matrix-js-sdk/pull/1271)
|
||||
* Upload only new device signature of master key
|
||||
[\#1268](https://github.com/matrix-org/matrix-js-sdk/pull/1268)
|
||||
* Expose prepareToEncrypt in the client API
|
||||
[\#1270](https://github.com/matrix-org/matrix-js-sdk/pull/1270)
|
||||
* Don't kill the whole device download if one device gives an error
|
||||
[\#1269](https://github.com/matrix-org/matrix-js-sdk/pull/1269)
|
||||
* Handle racing .start event during self verification
|
||||
[\#1267](https://github.com/matrix-org/matrix-js-sdk/pull/1267)
|
||||
* A crypto.keySignatureUploadFailure event reported the wrong source
|
||||
[\#1266](https://github.com/matrix-org/matrix-js-sdk/pull/1266)
|
||||
* Fix editing of unsent messages by waiting for actual event id
|
||||
[\#1263](https://github.com/matrix-org/matrix-js-sdk/pull/1263)
|
||||
* Fix: ensureOlmSessionsForDevices parameter format
|
||||
[\#1264](https://github.com/matrix-org/matrix-js-sdk/pull/1264)
|
||||
* Remove stuff that yarn install doesn't think we need
|
||||
[\#1261](https://github.com/matrix-org/matrix-js-sdk/pull/1261)
|
||||
* Fix: prevent error being thrown during sync in some cases
|
||||
[\#1258](https://github.com/matrix-org/matrix-js-sdk/pull/1258)
|
||||
* Force `is_verified` for key backups to bool and fix computation
|
||||
[\#1259](https://github.com/matrix-org/matrix-js-sdk/pull/1259)
|
||||
* Add a method for legacy single device verification, returning a verification
|
||||
request
|
||||
[\#1257](https://github.com/matrix-org/matrix-js-sdk/pull/1257)
|
||||
* yarn upgrade
|
||||
[\#1256](https://github.com/matrix-org/matrix-js-sdk/pull/1256)
|
||||
|
||||
Changes in [5.1.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.1) (2020-03-17)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.1-rc.1...v5.1.1)
|
||||
|
||||
* Fix: ensureOlmSessionsForDevices parameter format
|
||||
[\#1265](https://github.com/matrix-org/matrix-js-sdk/pull/1265)
|
||||
* Fix: prevent error being thrown during sync in some cases
|
||||
[\#1262](https://github.com/matrix-org/matrix-js-sdk/pull/1262)
|
||||
* Force `is_verified` for key backups to bool and fix computation
|
||||
[\#1260](https://github.com/matrix-org/matrix-js-sdk/pull/1260)
|
||||
|
||||
Changes in [5.1.1-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.1-rc.1) (2020-03-11)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.0...v5.1.1-rc.1)
|
||||
|
||||
* refactor megolm encryption to improve perceived speed
|
||||
[\#1252](https://github.com/matrix-org/matrix-js-sdk/pull/1252)
|
||||
* Remove v1 identity server fallbacks
|
||||
[\#1253](https://github.com/matrix-org/matrix-js-sdk/pull/1253)
|
||||
* Use alt_aliases instead of local ones for room names
|
||||
[\#1251](https://github.com/matrix-org/matrix-js-sdk/pull/1251)
|
||||
* Upload cross-signing key signatures in the background
|
||||
[\#1250](https://github.com/matrix-org/matrix-js-sdk/pull/1250)
|
||||
* Fix secret sharing names to match spec
|
||||
[\#1249](https://github.com/matrix-org/matrix-js-sdk/pull/1249)
|
||||
* Cleanup: remove crypto.verification.start event
|
||||
[\#1248](https://github.com/matrix-org/matrix-js-sdk/pull/1248)
|
||||
* Fix regression in key backup request params
|
||||
[\#1246](https://github.com/matrix-org/matrix-js-sdk/pull/1246)
|
||||
* Use cross-signing trust to mark backups verified
|
||||
[\#1244](https://github.com/matrix-org/matrix-js-sdk/pull/1244)
|
||||
* Check both cross-signing and local trust for key sharing
|
||||
[\#1243](https://github.com/matrix-org/matrix-js-sdk/pull/1243)
|
||||
* Fixed up tests to match new way that crypto stores are created
|
||||
[\#1242](https://github.com/matrix-org/matrix-js-sdk/pull/1242)
|
||||
* Store USK and SSK locally
|
||||
[\#1235](https://github.com/matrix-org/matrix-js-sdk/pull/1235)
|
||||
* Use unpadded base64 for QR code secrets
|
||||
[\#1236](https://github.com/matrix-org/matrix-js-sdk/pull/1236)
|
||||
* Don't require .done event for finishing self-verification
|
||||
[\#1239](https://github.com/matrix-org/matrix-js-sdk/pull/1239)
|
||||
* Don't cancel as 3rd party in verification request
|
||||
[\#1237](https://github.com/matrix-org/matrix-js-sdk/pull/1237)
|
||||
* Verification: log when switching start event
|
||||
[\#1234](https://github.com/matrix-org/matrix-js-sdk/pull/1234)
|
||||
* Perform crypto store operations directly after transaction
|
||||
[\#1233](https://github.com/matrix-org/matrix-js-sdk/pull/1233)
|
||||
* More verification request logging
|
||||
[\#1232](https://github.com/matrix-org/matrix-js-sdk/pull/1232)
|
||||
* Upgrade deps
|
||||
[\#1231](https://github.com/matrix-org/matrix-js-sdk/pull/1231)
|
||||
|
||||
Changes in [5.1.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.0) (2020-03-02)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.1.0-rc.1...v5.1.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [5.1.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.1.0-rc.1) (2020-02-26)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.0.1...v5.1.0-rc.1)
|
||||
|
||||
* Add latest dist-tag for releases
|
||||
[\#1230](https://github.com/matrix-org/matrix-js-sdk/pull/1230)
|
||||
* Add room method for alt_aliases
|
||||
[\#1225](https://github.com/matrix-org/matrix-js-sdk/pull/1225)
|
||||
* Remove buildkite pipeline
|
||||
[\#1227](https://github.com/matrix-org/matrix-js-sdk/pull/1227)
|
||||
* don't assume verify has been called when receiving a cancellation in
|
||||
verifier
|
||||
[\#1226](https://github.com/matrix-org/matrix-js-sdk/pull/1226)
|
||||
* Reduce secret size for new binary packing
|
||||
[\#1221](https://github.com/matrix-org/matrix-js-sdk/pull/1221)
|
||||
* misc rageshake fixes
|
||||
[\#1223](https://github.com/matrix-org/matrix-js-sdk/pull/1223)
|
||||
* Fix cancelled historical requests not appearing as cancelled
|
||||
[\#1220](https://github.com/matrix-org/matrix-js-sdk/pull/1220)
|
||||
* Fix renaming error that broke QR code verification
|
||||
[\#1217](https://github.com/matrix-org/matrix-js-sdk/pull/1217)
|
||||
|
||||
Changes in [5.0.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.0.1) (2020-02-19)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.0.0...v5.0.1)
|
||||
|
||||
* add method for new /aliases endpoint
|
||||
[\#1219](https://github.com/matrix-org/matrix-js-sdk/pull/1219)
|
||||
* method for checking if other party supports verification method
|
||||
[\#1213](https://github.com/matrix-org/matrix-js-sdk/pull/1213)
|
||||
* add local echo state for accepting or declining a verif req
|
||||
[\#1210](https://github.com/matrix-org/matrix-js-sdk/pull/1210)
|
||||
* make logging compatible with rageshakes
|
||||
[\#1214](https://github.com/matrix-org/matrix-js-sdk/pull/1214)
|
||||
* Find existing requests when starting a new verification request
|
||||
[\#1209](https://github.com/matrix-org/matrix-js-sdk/pull/1209)
|
||||
* log MAC calculation during SAS
|
||||
[\#1211](https://github.com/matrix-org/matrix-js-sdk/pull/1211)
|
||||
|
||||
Changes in [5.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.0.0) (2020-02-17)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v5.0.0-rc.1...v5.0.0)
|
||||
|
||||
* No changes since rc.1
|
||||
|
||||
Changes in [5.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v5.0.0-rc.1) (2020-02-13)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v4.0.0...v5.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
---
|
||||
|
||||
* The verification methods API has removed an argument ([\#1206](https://github.com/matrix-org/matrix-js-sdk/pull/1206))
|
||||
|
||||
All Changes
|
||||
---
|
||||
|
||||
* Remove methods argument to verification
|
||||
[\#1206](https://github.com/matrix-org/matrix-js-sdk/pull/1206)
|
||||
* don't do a dynamic import of request
|
||||
[\#1207](https://github.com/matrix-org/matrix-js-sdk/pull/1207)
|
||||
* QR self-verification fixes
|
||||
[\#1201](https://github.com/matrix-org/matrix-js-sdk/pull/1201)
|
||||
* Log every verification event
|
||||
[\#1204](https://github.com/matrix-org/matrix-js-sdk/pull/1204)
|
||||
* dont require .done event from other party
|
||||
[\#1203](https://github.com/matrix-org/matrix-js-sdk/pull/1203)
|
||||
* New option to fully reset Secret Storage keys in boostrapSecretStorage
|
||||
[\#1202](https://github.com/matrix-org/matrix-js-sdk/pull/1202)
|
||||
* Add function to estimate target device for a VerificationRequest
|
||||
[\#1190](https://github.com/matrix-org/matrix-js-sdk/pull/1190)
|
||||
* pass ssss item name to callback so we can differentiate UI on it
|
||||
[\#1200](https://github.com/matrix-org/matrix-js-sdk/pull/1200)
|
||||
* add export/import of Olm devices
|
||||
[\#1167](https://github.com/matrix-org/matrix-js-sdk/pull/1167)
|
||||
* Convert utils.js -> utils.ts
|
||||
[\#1199](https://github.com/matrix-org/matrix-js-sdk/pull/1199)
|
||||
* Don't sign ourselves as a user
|
||||
[\#1197](https://github.com/matrix-org/matrix-js-sdk/pull/1197)
|
||||
* Add a bunch of logging to verification
|
||||
[\#1196](https://github.com/matrix-org/matrix-js-sdk/pull/1196)
|
||||
* Fix: always return a valid string from InRoomChannel.getEventType
|
||||
[\#1198](https://github.com/matrix-org/matrix-js-sdk/pull/1198)
|
||||
* add logging when a request is being cancelled
|
||||
[\#1195](https://github.com/matrix-org/matrix-js-sdk/pull/1195)
|
||||
* Don't explode verification validation if we don't have an event type
|
||||
[\#1194](https://github.com/matrix-org/matrix-js-sdk/pull/1194)
|
||||
* Fix: verification request appearing for users that are not the receiver or
|
||||
sender if they are in room
|
||||
[\#1193](https://github.com/matrix-org/matrix-js-sdk/pull/1193)
|
||||
* Fix getting secrets encoded with passthrough keys
|
||||
[\#1192](https://github.com/matrix-org/matrix-js-sdk/pull/1192)
|
||||
* Update QR code handling for new spec
|
||||
[\#1175](https://github.com/matrix-org/matrix-js-sdk/pull/1175)
|
||||
* Don't add ephemeral events to timeline when peeking
|
||||
[\#1188](https://github.com/matrix-org/matrix-js-sdk/pull/1188)
|
||||
* Fix typo
|
||||
[\#1189](https://github.com/matrix-org/matrix-js-sdk/pull/1189)
|
||||
* Verification: resolve race between .start events from both parties
|
||||
[\#1187](https://github.com/matrix-org/matrix-js-sdk/pull/1187)
|
||||
* Add option to bootstrap to start new key backup
|
||||
[\#1184](https://github.com/matrix-org/matrix-js-sdk/pull/1184)
|
||||
* Add a bunch of null guards to feature checks
|
||||
[\#1182](https://github.com/matrix-org/matrix-js-sdk/pull/1182)
|
||||
* docs: fix MatrixClient reference
|
||||
[\#1183](https://github.com/matrix-org/matrix-js-sdk/pull/1183)
|
||||
* Add helper to obtain the cancellation code for a verification request
|
||||
[\#1180](https://github.com/matrix-org/matrix-js-sdk/pull/1180)
|
||||
* Publish pre-releases as a separate tag on npm
|
||||
[\#1178](https://github.com/matrix-org/matrix-js-sdk/pull/1178)
|
||||
* Fix support for passthrough keys
|
||||
[\#1177](https://github.com/matrix-org/matrix-js-sdk/pull/1177)
|
||||
* Trust our own cross-signing keys if we verify them with another device
|
||||
[\#1174](https://github.com/matrix-org/matrix-js-sdk/pull/1174)
|
||||
* Ensure cross-signing keys are downloaded when checking trust
|
||||
[\#1176](https://github.com/matrix-org/matrix-js-sdk/pull/1176)
|
||||
* Don't log verification validation errors for normal messages
|
||||
[\#1172](https://github.com/matrix-org/matrix-js-sdk/pull/1172)
|
||||
* Fix bootstrap cleanup
|
||||
[\#1173](https://github.com/matrix-org/matrix-js-sdk/pull/1173)
|
||||
* QR code verification
|
||||
[\#1155](https://github.com/matrix-org/matrix-js-sdk/pull/1155)
|
||||
* expose deviceId prop on device channel
|
||||
[\#1171](https://github.com/matrix-org/matrix-js-sdk/pull/1171)
|
||||
* Move & upgrade babel runtime into dependencies (like it wants)
|
||||
[\#1169](https://github.com/matrix-org/matrix-js-sdk/pull/1169)
|
||||
* Add unit tests for verifying your own device, remove .event property on
|
||||
verification request
|
||||
[\#1166](https://github.com/matrix-org/matrix-js-sdk/pull/1166)
|
||||
* For dm-verification, also consider events sent by other devices of same user
|
||||
as "our" events
|
||||
[\#1163](https://github.com/matrix-org/matrix-js-sdk/pull/1163)
|
||||
* Add a prepare script
|
||||
[\#1161](https://github.com/matrix-org/matrix-js-sdk/pull/1161)
|
||||
* Remove :deviceId from /keys/upload/:deviceId as not spec-compliant
|
||||
[\#1162](https://github.com/matrix-org/matrix-js-sdk/pull/1162)
|
||||
* Refactor and expose some logic publicly for the TimelineWindow class.
|
||||
[\#1159](https://github.com/matrix-org/matrix-js-sdk/pull/1159)
|
||||
* Allow a device key upload request without auth
|
||||
[\#1158](https://github.com/matrix-org/matrix-js-sdk/pull/1158)
|
||||
* Support for .ready verification event (MSC2366) & other things
|
||||
[\#1140](https://github.com/matrix-org/matrix-js-sdk/pull/1140)
|
||||
|
||||
Changes in [4.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v4.0.0) (2020-01-27)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v4.0.0-rc.1...v4.0.0)
|
||||
|
||||
* Move & upgrade babel runtime into dependencies (like it wants)
|
||||
[\#1170](https://github.com/matrix-org/matrix-js-sdk/pull/1170)
|
||||
* Add a prepare script
|
||||
[\#1164](https://github.com/matrix-org/matrix-js-sdk/pull/1164)
|
||||
|
||||
Changes in [4.0.0-rc.1](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v4.0.0-rc.1) (2020-01-20)
|
||||
==========================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v3.0.0...v4.0.0-rc.1)
|
||||
|
||||
BREAKING CHANGES
|
||||
================
|
||||
* The js-sdk node module now exports ES6 rather than ES5. If you
|
||||
wish to supports target that aren't compatible with ES6, you
|
||||
will need to transpile the js-sdk to a suitable dialect.
|
||||
|
||||
All Changes
|
||||
===========
|
||||
* Convert secret storage to new account data API
|
||||
[\#1154](https://github.com/matrix-org/matrix-js-sdk/pull/1154)
|
||||
* Add v5 as a safe room version
|
||||
[\#1157](https://github.com/matrix-org/matrix-js-sdk/pull/1157)
|
||||
* Add API to get account data from server
|
||||
[\#1153](https://github.com/matrix-org/matrix-js-sdk/pull/1153)
|
||||
* Fix sourcemaps by refactoring the build system
|
||||
[\#1151](https://github.com/matrix-org/matrix-js-sdk/pull/1151)
|
||||
* record, report, and notify about olm errors
|
||||
[\#1146](https://github.com/matrix-org/matrix-js-sdk/pull/1146)
|
||||
* Send device messages for the same user in same API call.
|
||||
[\#1148](https://github.com/matrix-org/matrix-js-sdk/pull/1148)
|
||||
* Add an option to ignore unverified devices
|
||||
[\#1150](https://github.com/matrix-org/matrix-js-sdk/pull/1150)
|
||||
* Sign key backup with cross-signing key on upgrade
|
||||
[\#1144](https://github.com/matrix-org/matrix-js-sdk/pull/1144)
|
||||
* Emoji verification: Change name of 🔒 to lock
|
||||
[\#1145](https://github.com/matrix-org/matrix-js-sdk/pull/1145)
|
||||
* use a separate object for each encrypted content
|
||||
[\#1147](https://github.com/matrix-org/matrix-js-sdk/pull/1147)
|
||||
* Sourcemaps: develop -> feature branch
|
||||
[\#1143](https://github.com/matrix-org/matrix-js-sdk/pull/1143)
|
||||
* Use a safer import/export scheme for the ContentRepo utilities
|
||||
[\#1134](https://github.com/matrix-org/matrix-js-sdk/pull/1134)
|
||||
* Fix error handling in decryptGroupMessage
|
||||
[\#1142](https://github.com/matrix-org/matrix-js-sdk/pull/1142)
|
||||
* Add additional properties to package.json for riot-web's webpack
|
||||
[\#1131](https://github.com/matrix-org/matrix-js-sdk/pull/1131)
|
||||
* Fix import for indexeddb crypto store
|
||||
[\#1133](https://github.com/matrix-org/matrix-js-sdk/pull/1133)
|
||||
* Use the right request when creating clients
|
||||
[\#1132](https://github.com/matrix-org/matrix-js-sdk/pull/1132)
|
||||
* Target NodeJS 10, minified browser bundle, and other publishing/package
|
||||
things
|
||||
[\#1127](https://github.com/matrix-org/matrix-js-sdk/pull/1127)
|
||||
* Re-focus sourcemap generation
|
||||
[\#1126](https://github.com/matrix-org/matrix-js-sdk/pull/1126)
|
||||
* Remove ancient polyfill for prototype inheritance
|
||||
[\#1125](https://github.com/matrix-org/matrix-js-sdk/pull/1125)
|
||||
* Remove "source-map-support" from tests because it makes sourcemaps worse
|
||||
[\#1124](https://github.com/matrix-org/matrix-js-sdk/pull/1124)
|
||||
* Remove ancient "use strict" annotations
|
||||
[\#1123](https://github.com/matrix-org/matrix-js-sdk/pull/1123)
|
||||
* Use ES6 imports/exports instead of older CommonJS ones
|
||||
[\#1122](https://github.com/matrix-org/matrix-js-sdk/pull/1122)
|
||||
* [BREAKING] Refactor the entire build process
|
||||
[\#1113](https://github.com/matrix-org/matrix-js-sdk/pull/1113)
|
||||
|
||||
Changes in [3.0.0](https://github.com/matrix-org/matrix-js-sdk/releases/tag/v3.0.0) (2020-01-13)
|
||||
================================================================================================
|
||||
[Full Changelog](https://github.com/matrix-org/matrix-js-sdk/compare/v3.0.0-rc.1...v3.0.0)
|
||||
|
||||
+13
-6
@@ -28,16 +28,23 @@ 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
|
||||
~~~~~~~~~~
|
||||
|
||||
The code-style for matrix-js-sdk is not formally documented, but contributors
|
||||
are encouraged to read the code style document for matrix-react-sdk
|
||||
The js-sdk aims to target TypeScript/ES6. All new files should be written in
|
||||
TypeScript and existing files should use ES6 principles where possible.
|
||||
|
||||
Members should not be exported as a default export in general - it causes problems
|
||||
with the architecture of the SDK (index file becomes less clear) and could
|
||||
introduce naming problems (as default exports get aliased upon import). In
|
||||
general, avoid using `export default`.
|
||||
|
||||
The remaining code-style for matrix-js-sdk is not formally documented, but
|
||||
contributors are encouraged to read the code style document for matrix-react-sdk
|
||||
(`<https://github.com/matrix-org/matrix-react-sdk/blob/master/code_style.md>`_)
|
||||
and follow the principles set out there.
|
||||
|
||||
|
||||
@@ -9,12 +9,16 @@ Quickstart
|
||||
|
||||
In a browser
|
||||
------------
|
||||
Download either the full or minified version from
|
||||
Download the browser version from
|
||||
https://github.com/matrix-org/matrix-js-sdk/releases/latest and add that as a
|
||||
``<script>`` to your page. There will be a global variable ``matrixcs``
|
||||
attached to ``window`` through which you can access the SDK. See below for how to
|
||||
include libolm to enable end-to-end-encryption.
|
||||
|
||||
The browser bundle supports recent versions of browsers. Typically this is ES2015
|
||||
or `> 0.5%, last 2 versions, Firefox ESR, not dead` if using
|
||||
[browserlists](https://github.com/browserslist/browserslist).
|
||||
|
||||
Please check [the working browser example](examples/browser) for more information.
|
||||
|
||||
In Node.js
|
||||
@@ -22,13 +26,18 @@ In Node.js
|
||||
|
||||
Ensure you have the latest LTS version of Node.js installed.
|
||||
|
||||
Using `yarn` instead of `npm` is recommended. Please see the Yarn [install guide](https://yarnpkg.com/docs/install/) if you do not have it already.
|
||||
This SDK targets Node 10 for compatibility, which translates to ES6. If you're using
|
||||
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://classic.yarnpkg.com/en/docs/install)
|
||||
if you do not have it already.
|
||||
|
||||
``yarn add matrix-js-sdk``
|
||||
|
||||
```javascript
|
||||
var sdk = require("matrix-js-sdk");
|
||||
var client = sdk.createClient("https://matrix.org");
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
const client = sdk.createClient("https://matrix.org");
|
||||
client.publicRooms(function(err, data) {
|
||||
console.log("Public Rooms: %s", JSON.stringify(data));
|
||||
});
|
||||
@@ -59,7 +68,7 @@ client.once('sync', function(state, prevState, res) {
|
||||
To send a message:
|
||||
|
||||
```javascript
|
||||
var content = {
|
||||
const content = {
|
||||
"body": "message text",
|
||||
"msgtype": "m.text"
|
||||
};
|
||||
@@ -173,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.
|
||||
|
||||
@@ -191,10 +198,10 @@ This section provides some useful code snippets which demonstrate the
|
||||
core functionality of the SDK. These examples assume the SDK is setup like this:
|
||||
|
||||
```javascript
|
||||
var sdk = require("matrix-js-sdk");
|
||||
var myUserId = "@example:localhost";
|
||||
var myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
|
||||
var matrixClient = sdk.createClient({
|
||||
import * as sdk from "matrix-js-sdk";
|
||||
const myUserId = "@example:localhost";
|
||||
const myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
|
||||
const matrixClient = sdk.createClient({
|
||||
baseUrl: "http://localhost:8008",
|
||||
accessToken: myAccessToken,
|
||||
userId: myUserId
|
||||
@@ -247,11 +254,11 @@ Output:
|
||||
|
||||
```javascript
|
||||
matrixClient.on("RoomState.members", function(event, state, member) {
|
||||
var room = matrixClient.getRoom(state.roomId);
|
||||
const room = matrixClient.getRoom(state.roomId);
|
||||
if (!room) {
|
||||
return;
|
||||
}
|
||||
var memberList = state.getMembers();
|
||||
const memberList = state.getMembers();
|
||||
console.log(room.name);
|
||||
console.log(Array(room.name.length + 1).join("=")); // underline
|
||||
for (var i = 0; i < memberList.length; i++) {
|
||||
@@ -351,11 +358,6 @@ To build a browser version from scratch when developing::
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
To constantly do builds when files are modified (using ``watchify``)::
|
||||
```
|
||||
$ yarn watch
|
||||
```
|
||||
|
||||
To run tests (Jasmine)::
|
||||
```
|
||||
$ yarn test
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
var matrixcs = require("./lib/matrix");
|
||||
const request = require('browser-request');
|
||||
const queryString = require('qs');
|
||||
|
||||
matrixcs.request(function(opts, fn) {
|
||||
// We manually fix the query string for browser-request because
|
||||
// it doesn't correctly handle cases like ?via=one&via=two. Instead
|
||||
// we mimic `request`'s query string interface to make it all work
|
||||
// as expected.
|
||||
// browser-request will happily take the constructed string as the
|
||||
// query string without trying to modify it further.
|
||||
opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions);
|
||||
return request(opts, fn);
|
||||
});
|
||||
|
||||
// just *accessing* indexedDB throws an exception in firefox with
|
||||
// indexeddb disabled.
|
||||
var indexedDB;
|
||||
try {
|
||||
indexedDB = global.indexedDB;
|
||||
} catch(e) {}
|
||||
|
||||
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
|
||||
if (indexedDB) {
|
||||
matrixcs.setCryptoStoreFactory(
|
||||
function() {
|
||||
return new matrixcs.IndexedDBCryptoStore(
|
||||
indexedDB, "matrix-js-sdk:crypto"
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = matrixcs; // keep export for browserify package deps
|
||||
global.matrixcs = matrixcs;
|
||||
@@ -1,4 +1,3 @@
|
||||
"use strict";
|
||||
console.log("Loading browser sdk");
|
||||
|
||||
var client = matrixcs.createClient("http://matrix.org");
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
olm.js
|
||||
olm.wasm
|
||||
+1
@@ -0,0 +1 @@
|
||||
../../../dist/browser-matrix.js
|
||||
@@ -0,0 +1,59 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Test Crypto in Browser</title>
|
||||
<script src="lib/olm.js"></script>
|
||||
<script src="lib/matrix.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Testing export/import of Olm devices in the browser</h1>
|
||||
<ul>
|
||||
<li>
|
||||
Make sure you built the current version of the Matrix JS SDK
|
||||
(<code>yarn build</code>)
|
||||
</li>
|
||||
<li>
|
||||
copy <code>olm.js</code> and <code>olm.wasm</code>
|
||||
from a recent release of Olm (was tested with version 3.1.4)
|
||||
in directory <code>lib/</code>
|
||||
</li>
|
||||
<li>start a local Matrix homeserver (on port 8008, or change the port in the code)</li>
|
||||
<li>Serve this HTML file (e.g. <code>python3 -m http.server</code>) and go to it through your browser</li>
|
||||
<li>
|
||||
in the JS console, do:
|
||||
<pre>
|
||||
aliceMatrixClient = await newMatrixClient("alice-"+randomHex());
|
||||
await aliceMatrixClient.exportDevice();
|
||||
await aliceMatrixClient.getAccessToken();
|
||||
</pre>
|
||||
</li>
|
||||
<li>
|
||||
copy the result of <code>exportDevice</code> and <code>getAccessToken</code> somewhere
|
||||
(<strong>not</strong> in a JS variable as it will be destroyed when you refresh the page)
|
||||
</li>
|
||||
<li><strong>refresh the page (F5)</strong> to make sure the client is destroyed</li>
|
||||
<li>
|
||||
Do the following, replacing <code>ALICE_ID</code>
|
||||
with the user ID of Alice (you can find it in the exported data)
|
||||
<pre>
|
||||
bobMatrixClient = await newMatrixClient("bob-"+randomHex());
|
||||
roomId = await bobMatrixClient.createEncryptedRoom([ALICE_ID]);
|
||||
await bobMatrixClient.sendTextMessage('Hi Alice!', roomId);
|
||||
</pre>
|
||||
</li>
|
||||
<li>Again, <strong>refresh the page (F5)</strong>. You may want to clear your console as well.</li>
|
||||
<li>
|
||||
Now do the following, using the exported data and the access token you saved previously:
|
||||
<pre>
|
||||
aliceMatrixClient = await importMatrixClient(EXPORTED_DATA, ACCESS_TOKEN);
|
||||
</pre>
|
||||
</li>
|
||||
<li>You should see the message sent by Bob printed in the console.</li>
|
||||
</ul>
|
||||
|
||||
<script src="olm-device-export-import.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,122 @@
|
||||
if (!Olm) {
|
||||
console.error(
|
||||
"global.Olm does not seem to be present."
|
||||
+ " Did you forget to add olm in the lib/ directory?"
|
||||
);
|
||||
}
|
||||
|
||||
const BASE_URL = 'http://localhost:8008';
|
||||
const ROOM_CRYPTO_CONFIG = { algorithm: 'm.megolm.v1.aes-sha2' };
|
||||
const PASSWORD = 'password';
|
||||
|
||||
// useful to create new usernames
|
||||
window.randomHex = () => Math.floor(Math.random() * (10**6)).toString(16);
|
||||
|
||||
window.newMatrixClient = async function (username) {
|
||||
const registrationClient = matrixcs.createClient(BASE_URL);
|
||||
|
||||
const userRegisterResult = await registrationClient.register(
|
||||
username,
|
||||
PASSWORD,
|
||||
null,
|
||||
{ type: 'm.login.dummy' }
|
||||
);
|
||||
|
||||
const matrixClient = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
userId: userRegisterResult.user_id,
|
||||
accessToken: userRegisterResult.access_token,
|
||||
deviceId: userRegisterResult.device_id,
|
||||
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
|
||||
cryptoStore: new matrixcs.MemoryCryptoStore(),
|
||||
});
|
||||
|
||||
extendMatrixClient(matrixClient);
|
||||
|
||||
await matrixClient.initCrypto();
|
||||
await matrixClient.startClient();
|
||||
return matrixClient;
|
||||
}
|
||||
|
||||
window.importMatrixClient = async function (exportedDevice, accessToken) {
|
||||
const matrixClient = matrixcs.createClient({
|
||||
baseUrl: BASE_URL,
|
||||
deviceToImport: exportedDevice,
|
||||
accessToken,
|
||||
sessionStore: new matrixcs.WebStorageSessionStore(window.localStorage),
|
||||
cryptoStore: new matrixcs.MemoryCryptoStore(),
|
||||
});
|
||||
|
||||
extendMatrixClient(matrixClient);
|
||||
|
||||
await matrixClient.initCrypto();
|
||||
await matrixClient.startClient();
|
||||
return matrixClient;
|
||||
}
|
||||
|
||||
function extendMatrixClient(matrixClient) {
|
||||
// automatic join
|
||||
matrixClient.on('RoomMember.membership', async (event, member) => {
|
||||
if (member.membership === 'invite' && member.userId === matrixClient.getUserId()) {
|
||||
await matrixClient.joinRoom(member.roomId);
|
||||
// setting up of room encryption seems to be triggered automatically
|
||||
// but if we don't wait for it the first messages we send are unencrypted
|
||||
await matrixClient.setRoomEncryption(member.roomId, { algorithm: 'm.megolm.v1.aes-sha2' })
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.onDecryptedMessage = message => {
|
||||
console.log('Got encrypted message: ', message);
|
||||
}
|
||||
|
||||
matrixClient.on('Event.decrypted', (event) => {
|
||||
if (event.getType() === 'm.room.message'){
|
||||
matrixClient.onDecryptedMessage(event.getContent().body);
|
||||
} else {
|
||||
console.log('decrypted an event of type', event.getType());
|
||||
console.log(event);
|
||||
}
|
||||
});
|
||||
|
||||
matrixClient.createEncryptedRoom = async function(usersToInvite) {
|
||||
const {
|
||||
room_id: roomId,
|
||||
} = await this.createRoom({
|
||||
visibility: 'private',
|
||||
invite: usersToInvite,
|
||||
});
|
||||
|
||||
// matrixClient.setRoomEncryption() only updates local state
|
||||
// but does not send anything to the server
|
||||
// (see https://github.com/matrix-org/matrix-js-sdk/issues/905)
|
||||
// so we do it ourselves with 'sendStateEvent'
|
||||
await this.sendStateEvent(
|
||||
roomId, 'm.room.encryption', ROOM_CRYPTO_CONFIG,
|
||||
);
|
||||
await this.setRoomEncryption(
|
||||
roomId, ROOM_CRYPTO_CONFIG,
|
||||
);
|
||||
|
||||
// Marking all devices as verified
|
||||
let room = this.getRoom(roomId);
|
||||
let members = (await room.getEncryptionTargetMembers()).map(x => x["userId"])
|
||||
let memberkeys = await this.downloadKeys(members);
|
||||
for (const userId in memberkeys) {
|
||||
for (const deviceId in memberkeys[userId]) {
|
||||
await this.setDeviceVerified(userId, deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
return roomId;
|
||||
}
|
||||
|
||||
matrixClient.sendTextMessage = async function(message, roomId) {
|
||||
return matrixClient.sendMessage(
|
||||
roomId,
|
||||
{
|
||||
body: message,
|
||||
msgtype: 'm.text',
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
"use strict";
|
||||
|
||||
var myUserId = "@example:localhost";
|
||||
var myAccessToken = "QGV4YW1wbGU6bG9jYWxob3N0.qPEvLuYfNBjxikiCjP";
|
||||
var sdk = require("matrix-js-sdk");
|
||||
@@ -290,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
|
||||
@@ -302,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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
"use strict";
|
||||
console.log("Loading browser sdk");
|
||||
var BASE_URL = "https://matrix.org";
|
||||
var TOKEN = "accesstokengoeshere";
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
var matrixcs = require("./lib/matrix");
|
||||
matrixcs.request(require("request"));
|
||||
module.exports = matrixcs;
|
||||
|
||||
var utils = require("./lib/utils");
|
||||
utils.runPolyfills();
|
||||
+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
-63
@@ -1,21 +1,23 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "3.0.0",
|
||||
"version": "9.1.0-rc.1",
|
||||
"description": "Matrix Client-Server SDK for Javascript",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test:watch": "jest spec/ --coverage --testEnvironment node --watch",
|
||||
"test": "jest spec/ --coverage --testEnvironment node",
|
||||
"gendoc": "jsdoc -c jsdoc.json -P package.json",
|
||||
"start": "yarn start:init && yarn start:watch",
|
||||
"start:watch": "babel src -s -w --skip-initial-build -d lib --verbose --extensions \".ts,.js\"",
|
||||
"start:init": "babel src -s -d lib --verbose --extensions \".ts,.js\"",
|
||||
"prepare": "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": "babel src -s -d lib --verbose --extensions \".ts,.js\" && rimraf dist && mkdir dist && browserify -d browser-index.js | exorcist dist/browser-matrix.js.map > dist/browser-matrix.js && terser -c -m -o dist/browser-matrix.min.js --source-map \"content='dist/browser-matrix.js.map'\" dist/browser-matrix.js",
|
||||
"dist": "yarn build",
|
||||
"watch": "watchify -d browser-index.js -o 'exorcist dist/browser-matrix.js.map > dist/browser-matrix.js' -v",
|
||||
"lint": "eslint --max-warnings 93 src spec",
|
||||
"prepare": "yarn clean && yarn build && git rev-parse HEAD > git-revision.txt"
|
||||
"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: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:js",
|
||||
"lint:js": "eslint --max-warnings 73 src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"test": "jest spec/ --coverage --testEnvironment node",
|
||||
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -24,75 +26,72 @@
|
||||
"keywords": [
|
||||
"matrix-org"
|
||||
],
|
||||
"browser": "browser-index.js",
|
||||
"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",
|
||||
"author": "matrix.org",
|
||||
"license": "Apache-2.0",
|
||||
"files": [
|
||||
".babelrc",
|
||||
".eslintrc.js",
|
||||
"spec/.eslintrc.js",
|
||||
"dist",
|
||||
"lib",
|
||||
"src",
|
||||
"git-revision.txt",
|
||||
"CHANGELOG.md",
|
||||
"CONTRIBUTING.rst",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
"RELEASING.md",
|
||||
"examples",
|
||||
"git-hooks",
|
||||
"git-revision.txt",
|
||||
"index.js",
|
||||
"browser-index.js",
|
||||
"lib",
|
||||
"package.json",
|
||||
"release.sh",
|
||||
"spec",
|
||||
"src"
|
||||
"release.sh"
|
||||
],
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.11.2",
|
||||
"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.0",
|
||||
"qs": "^6.9.4",
|
||||
"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.7.6",
|
||||
"@babel/preset-env": "^7.7.6",
|
||||
"@babel/preset-typescript": "^7.7.4",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@babel/runtime": "^7.7.6",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"@babel/cli": "^7.11.6",
|
||||
"@babel/core": "^7.11.6",
|
||||
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||
"@babel/plugin-proposal-numeric-separator": "^7.10.4",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.11.0",
|
||||
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
|
||||
"@babel/plugin-transform-runtime": "^7.11.5",
|
||||
"@babel/preset-env": "^7.11.5",
|
||||
"@babel/preset-typescript": "^7.10.4",
|
||||
"@babel/register": "^7.11.5",
|
||||
"@types/jest": "^26.0.14",
|
||||
"@types/node": "12",
|
||||
"@types/request": "^2.48.5",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"babel-jest": "^24.9.0",
|
||||
"better-docs": "^1.4.7",
|
||||
"browserify": "^16.2.3",
|
||||
"browserify-shim": "^3.8.13",
|
||||
"eslint": "^5.12.0",
|
||||
"eslint-config-google": "^0.7.1",
|
||||
"eslint-plugin-babel": "^5.3.0",
|
||||
"eslint-plugin-jest": "^23.0.4",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^2.3.2",
|
||||
"browserify": "^16.5.2",
|
||||
"docdash": "^1.2.0",
|
||||
"eslint": "7.9.0",
|
||||
"eslint-config-matrix-org": "^0.1.2",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"exorcist": "^1.0.1",
|
||||
"fake-indexeddb": "^3.1.2",
|
||||
"jest": "^24.9.0",
|
||||
"jsdoc": "^3.5.5",
|
||||
"jest-localstorage-mock": "^2.4.3",
|
||||
"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",
|
||||
"source-map-support": "^0.5.13",
|
||||
"sourceify": "^1.0.0",
|
||||
"terser": "^4.3.8",
|
||||
"typescript": "^3.7.3",
|
||||
"watchify": "^3.11.1"
|
||||
"olm": "https://packages.matrix.org/npm/olm/olm-3.2.1.tgz",
|
||||
"rimraf": "^3.0.2",
|
||||
"terser": "^4.8.0",
|
||||
"tsify": "^4.0.2",
|
||||
"typescript": "^3.9.7"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"sourceify"
|
||||
]
|
||||
"jest": {
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
|
||||
+43
-22
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Script to perform a release of matrix-js-sdk.
|
||||
# Script to perform a release of matrix-js-sdk and downstream projects.
|
||||
#
|
||||
# Requires:
|
||||
# github-changelog-generator; install via:
|
||||
@@ -9,6 +9,8 @@
|
||||
# hub; install via brew (macOS) or source/pre-compiled binaries (debian) (https://github.com/github/hub) - Tested on v2.2.9
|
||||
# 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 element-web.
|
||||
|
||||
set -e
|
||||
|
||||
@@ -36,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
|
||||
}
|
||||
|
||||
@@ -58,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
|
||||
@@ -75,6 +79,9 @@ while getopts hc:u:xz f; do
|
||||
z)
|
||||
skip_jsdoc=1
|
||||
;;
|
||||
n)
|
||||
skip_npm=1
|
||||
;;
|
||||
u)
|
||||
expected_npm_user="$OPTARG"
|
||||
;;
|
||||
@@ -87,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)
|
||||
@@ -94,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
|
||||
@@ -195,11 +212,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
|
||||
@@ -294,7 +306,16 @@ rm "${latest_changes}"
|
||||
|
||||
# Login and publish continues to use `npm`, as it seems to have more clearly
|
||||
# defined options and semantics than `yarn` for writing to the registry.
|
||||
npm publish
|
||||
# Tag both releases and prereleases as `next` so the last stable release remains
|
||||
# the default.
|
||||
if [ -z "$skip_npm" ]; then
|
||||
npm publish --tag next
|
||||
if [ $prerelease -eq 0 ]; then
|
||||
# For a release, also add the default `latest` tag.
|
||||
package=$(cat package.json | jq -er .name)
|
||||
npm dist-tag add "$package@$release" latest
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$skip_jsdoc" ]; then
|
||||
echo "generating jsdocs"
|
||||
@@ -309,6 +330,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.
|
||||
@@ -321,16 +343,15 @@ 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
|
||||
git push origin develop
|
||||
fi
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -18,7 +19,7 @@ limitations under the License.
|
||||
* A mock implementation of the webstorage api
|
||||
* @constructor
|
||||
*/
|
||||
function MockStorageApi() {
|
||||
export function MockStorageApi() {
|
||||
this.data = {};
|
||||
this.keys = [];
|
||||
this.length = 0;
|
||||
@@ -52,5 +53,3 @@ MockStorageApi.prototype = {
|
||||
},
|
||||
};
|
||||
|
||||
/** */
|
||||
module.exports = MockStorageApi;
|
||||
|
||||
+20
-13
@@ -16,16 +16,16 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import './olm-loader';
|
||||
|
||||
import sdk from '..';
|
||||
import testUtils from './test-utils';
|
||||
import MockHttpBackend from 'matrix-mock-request';
|
||||
import LocalStorageCryptoStore from '../lib/crypto/store/localStorage-crypto-store';
|
||||
import logger from '../lib/logger';
|
||||
import {LocalStorageCryptoStore} from '../src/crypto/store/localStorage-crypto-store';
|
||||
import {logger} from '../src/logger';
|
||||
import {WebStorageSessionStore} from "../src/store/session/webstorage";
|
||||
import {syncPromise} from "./test-utils";
|
||||
import {createClient} from "../src/matrix";
|
||||
import {MockStorageApi} from "./MockStorageApi";
|
||||
|
||||
/**
|
||||
* Wrapper for a MockStorageApi, MockHttpBackend and MatrixClient
|
||||
@@ -39,16 +39,16 @@ import logger from '../lib/logger';
|
||||
* session store. If undefined, we will create a MockStorageApi.
|
||||
* @param {object} options additional options to pass to the client
|
||||
*/
|
||||
export default function TestClient(
|
||||
export function TestClient(
|
||||
userId, deviceId, accessToken, sessionStoreBackend, options,
|
||||
) {
|
||||
this.userId = userId;
|
||||
this.deviceId = deviceId;
|
||||
|
||||
if (sessionStoreBackend === undefined) {
|
||||
sessionStoreBackend = new testUtils.MockStorageApi();
|
||||
sessionStoreBackend = new MockStorageApi();
|
||||
}
|
||||
const sessionStore = new sdk.WebStorageSessionStore(sessionStoreBackend);
|
||||
const sessionStore = new WebStorageSessionStore(sessionStoreBackend);
|
||||
|
||||
this.httpBackend = new MockHttpBackend();
|
||||
|
||||
@@ -65,10 +65,13 @@ export default function TestClient(
|
||||
this.cryptoStore = new LocalStorageCryptoStore(sessionStoreBackend);
|
||||
options.cryptoStore = this.cryptoStore;
|
||||
}
|
||||
this.client = sdk.createClient(options);
|
||||
this.client = createClient(options);
|
||||
|
||||
this.deviceKeys = null;
|
||||
this.oneTimeKeys = {};
|
||||
this._callEventHandler = {
|
||||
calls: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
TestClient.prototype.toString = function() {
|
||||
@@ -97,7 +100,7 @@ TestClient.prototype.start = function() {
|
||||
|
||||
return Promise.all([
|
||||
this.httpBackend.flushAllExpected(),
|
||||
testUtils.syncPromise(this.client),
|
||||
syncPromise(this.client),
|
||||
]).then(() => {
|
||||
logger.log(this + ': started');
|
||||
});
|
||||
@@ -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),
|
||||
);
|
||||
@@ -225,8 +228,12 @@ TestClient.prototype.flushSync = function() {
|
||||
logger.log(`${this}: flushSync`);
|
||||
return Promise.all([
|
||||
this.httpBackend.flush('/sync', 1),
|
||||
testUtils.syncPromise(this.client),
|
||||
syncPromise(this.client),
|
||||
]).then(() => {
|
||||
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
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -15,9 +16,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import TestClient from '../TestClient';
|
||||
import testUtils from '../test-utils';
|
||||
import logger from '../../lib/logger';
|
||||
import {TestClient} from '../TestClient';
|
||||
import * as testUtils from '../test-utils';
|
||||
import {logger} from '../../src/logger';
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -139,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(
|
||||
@@ -270,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();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -24,17 +25,14 @@ limitations under the License.
|
||||
* See also `megolm.spec.js`.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
|
||||
// load olm before the sdk if possible
|
||||
import '../olm-loader';
|
||||
|
||||
const sdk = require("../..");
|
||||
const utils = require("../../lib/utils");
|
||||
const testUtils = require("../test-utils");
|
||||
const TestClient = require('../TestClient').default;
|
||||
import logger from '../../lib/logger';
|
||||
import {logger} from '../../src/logger';
|
||||
import * as testUtils from "../test-utils";
|
||||
import * as utils from "../../src/utils";
|
||||
import {TestClient} from "../TestClient";
|
||||
import {CRYPTO_ENABLED} from "../../src/client";
|
||||
|
||||
let aliTestClient;
|
||||
const roomId = "!room:localhost";
|
||||
@@ -72,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),
|
||||
);
|
||||
@@ -100,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),
|
||||
);
|
||||
@@ -400,7 +398,7 @@ function firstSync(testClient) {
|
||||
|
||||
|
||||
describe("MatrixClient crypto", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
import * as utils from "../test-utils";
|
||||
import {TestClient} from "../TestClient";
|
||||
|
||||
describe("MatrixClient events", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client;
|
||||
let httpBackend;
|
||||
const selfUserId = "@alice:localhost";
|
||||
const selfAccessToken = "aseukfgwef";
|
||||
|
||||
beforeEach(function() {
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: selfUserId,
|
||||
accessToken: selfAccessToken,
|
||||
});
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
});
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
const EventTimeline = sdk.EventTimeline;
|
||||
import logger from '../../lib/logger';
|
||||
import * as utils from "../test-utils";
|
||||
import {EventTimeline} from "../../src/matrix";
|
||||
import {logger} from "../../src/logger";
|
||||
import {TestClient} from "../TestClient";
|
||||
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
const userId = "@alice:localhost";
|
||||
const userName = "Alice";
|
||||
const accessToken = "aseukfgwef";
|
||||
@@ -103,8 +99,9 @@ describe("getEventTimeline support", function() {
|
||||
let client;
|
||||
|
||||
beforeEach(function() {
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
const testClient = new TestClient(userId, "DEVICE", accessToken);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -115,12 +112,6 @@ describe("getEventTimeline support", function() {
|
||||
});
|
||||
|
||||
it("timeline support must be enabled to work", function() {
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
});
|
||||
|
||||
return startClient(httpBackend, client).then(function() {
|
||||
const room = client.getRoom(roomId);
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
@@ -131,12 +122,15 @@ describe("getEventTimeline support", function() {
|
||||
});
|
||||
|
||||
it("timeline support works when enabled", function() {
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
timelineSupport: true,
|
||||
});
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{timelineSupport: true},
|
||||
);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
|
||||
return startClient(httpBackend, client).then(() => {
|
||||
const room = client.getRoom(roomId);
|
||||
@@ -151,11 +145,7 @@ describe("getEventTimeline support", function() {
|
||||
it("scrollback should be able to scroll back to before a gappy /sync",
|
||||
function() {
|
||||
// need a client with timelineSupport disabled to make this work
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
});
|
||||
|
||||
let room;
|
||||
|
||||
return startClient(httpBackend, client).then(function() {
|
||||
@@ -223,15 +213,15 @@ describe("MatrixClient event timelines", function() {
|
||||
let httpBackend = null;
|
||||
|
||||
beforeEach(function() {
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
timelineSupport: true,
|
||||
});
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{timelineSupport: true},
|
||||
);
|
||||
client = testClient.client;
|
||||
httpBackend = testClient.httpBackend;
|
||||
|
||||
return startClient(httpBackend, client);
|
||||
});
|
||||
|
||||
@@ -1,39 +1,23 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const publicGlobals = require("../../lib/matrix");
|
||||
const Room = publicGlobals.Room;
|
||||
const MemoryStore = publicGlobals.MemoryStore;
|
||||
const Filter = publicGlobals.Filter;
|
||||
const utils = require("../test-utils");
|
||||
const MockStorageApi = require("../MockStorageApi");
|
||||
import * as utils from "../test-utils";
|
||||
import {CRYPTO_ENABLED} from "../../src/client";
|
||||
import {Filter, MemoryStore, Room} from "../../src/matrix";
|
||||
import {TestClient} from "../TestClient";
|
||||
|
||||
describe("MatrixClient", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
let store = null;
|
||||
let sessionStore = null;
|
||||
const userId = "@alice:localhost";
|
||||
const accessToken = "aseukfgwef";
|
||||
|
||||
beforeEach(function() {
|
||||
httpBackend = new HttpBackend();
|
||||
store = new MemoryStore();
|
||||
|
||||
const mockStorage = new MockStorageApi();
|
||||
sessionStore = new sdk.WebStorageSessionStore(mockStorage);
|
||||
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
deviceId: "aliceDevice",
|
||||
accessToken: accessToken,
|
||||
const testClient = new TestClient(userId, "aliceDevice", accessToken, undefined, {
|
||||
store: store,
|
||||
sessionStore: sessionStore,
|
||||
});
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -303,7 +287,7 @@ describe("MatrixClient", function() {
|
||||
|
||||
|
||||
describe("downloadKeys", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -363,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: {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const MatrixClient = sdk.MatrixClient;
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
import * as utils from "../test-utils";
|
||||
import HttpBackend from "matrix-mock-request";
|
||||
import {MatrixClient} from "../../src/matrix";
|
||||
import {MatrixScheduler} from "../../src/scheduler";
|
||||
import {MemoryStore} from "../../src/store/memory";
|
||||
import {MatrixError} from "../../src/http-api";
|
||||
|
||||
describe("MatrixClient opts", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
@@ -71,7 +71,7 @@ describe("MatrixClient opts", function() {
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
scheduler: new sdk.MatrixScheduler(),
|
||||
scheduler: new MatrixScheduler(),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,7 +124,7 @@ describe("MatrixClient opts", function() {
|
||||
beforeEach(function() {
|
||||
client = new MatrixClient({
|
||||
request: httpBackend.requestFn,
|
||||
store: new sdk.MemoryStore(),
|
||||
store: new MemoryStore(),
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
@@ -133,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) {
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
|
||||
const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const EventStatus = sdk.EventStatus;
|
||||
import {EventStatus} from "../../src/matrix";
|
||||
import {MatrixScheduler} from "../../src/scheduler";
|
||||
import {Room} from "../../src/models/room";
|
||||
import {TestClient} from "../TestClient";
|
||||
|
||||
describe("MatrixClient retrying", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
let scheduler;
|
||||
@@ -16,16 +13,17 @@ describe("MatrixClient retrying", function() {
|
||||
let room;
|
||||
|
||||
beforeEach(function() {
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
scheduler = new sdk.MatrixScheduler();
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
scheduler: scheduler,
|
||||
});
|
||||
room = new sdk.Room(roomId);
|
||||
scheduler = new MatrixScheduler();
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{scheduler},
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
room = new Room(roomId);
|
||||
client.store.storeRoom(room);
|
||||
});
|
||||
|
||||
@@ -92,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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const EventStatus = sdk.EventStatus;
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
import * as utils from "../test-utils";
|
||||
import {EventStatus} from "../../src/models/event";
|
||||
import {TestClient} from "../TestClient";
|
||||
|
||||
|
||||
describe("MatrixClient room timelines", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
const userId = "@alice:localhost";
|
||||
@@ -101,15 +98,17 @@ describe("MatrixClient room timelines", function() {
|
||||
}
|
||||
|
||||
beforeEach(function() {
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
// these tests should work with or without timelineSupport
|
||||
timelineSupport: true,
|
||||
});
|
||||
// these tests should work with or without timelineSupport
|
||||
const testClient = new TestClient(
|
||||
userId,
|
||||
"DEVICE",
|
||||
accessToken,
|
||||
undefined,
|
||||
{timelineSupport: true},
|
||||
);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
|
||||
setNextSyncData();
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const HttpBackend = require("matrix-mock-request");
|
||||
const utils = require("../test-utils");
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
const EventTimeline = sdk.EventTimeline;
|
||||
import {MatrixEvent} from "../../src/models/event";
|
||||
import {EventTimeline} from "../../src/models/event-timeline";
|
||||
import * as utils from "../test-utils";
|
||||
import {TestClient} from "../TestClient";
|
||||
|
||||
describe("MatrixClient syncing", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
let client = null;
|
||||
let httpBackend = null;
|
||||
const selfUserId = "@alice:localhost";
|
||||
@@ -20,13 +16,9 @@ describe("MatrixClient syncing", function() {
|
||||
const roomTwo = "!bar:localhost";
|
||||
|
||||
beforeEach(function() {
|
||||
httpBackend = new HttpBackend();
|
||||
sdk.request(httpBackend.requestFn);
|
||||
client = sdk.createClient({
|
||||
baseUrl: baseUrl,
|
||||
userId: selfUserId,
|
||||
accessToken: selfAccessToken,
|
||||
});
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
httpBackend.when("POST", "/filter").respond(200, { filter_id: "a filter id" });
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,14 +15,11 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const anotherjson = require('another-json');
|
||||
|
||||
const utils = require('../../lib/utils');
|
||||
const testUtils = require('../test-utils');
|
||||
const TestClient = require('../TestClient').default;
|
||||
import logger from '../../lib/logger';
|
||||
import anotherjson from "another-json";
|
||||
import * as utils from "../../src/utils";
|
||||
import * as testUtils from "../test-utils";
|
||||
import {TestClient} from "../TestClient";
|
||||
import {logger} from "../../src/logger";
|
||||
|
||||
const ROOM_ID = "!room:id";
|
||||
|
||||
@@ -348,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(() => {
|
||||
@@ -617,6 +615,9 @@ describe("megolm", function() {
|
||||
).respond(200, {
|
||||
event_id: '$event_id',
|
||||
});
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
|
||||
).respond(200, {});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test'),
|
||||
@@ -714,6 +715,9 @@ describe("megolm", function() {
|
||||
event_id: '$event_id',
|
||||
};
|
||||
});
|
||||
aliceTestClient.httpBackend.when(
|
||||
'PUT', '/sendToDevice/org.matrix.room_key.withheld/',
|
||||
).respond(200, {});
|
||||
|
||||
return Promise.all([
|
||||
aliceTestClient.client.sendTextMessage(ROOM_ID, 'test2'),
|
||||
@@ -722,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.
|
||||
|
||||
+11
-1
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Vector creations Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,7 +15,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import logger from '../lib/logger';
|
||||
import {logger} from '../src/logger';
|
||||
import * as utils from "../src/utils";
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
@@ -23,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');
|
||||
}
|
||||
|
||||
+28
-30
@@ -1,10 +1,8 @@
|
||||
"use strict";
|
||||
// load olm before the sdk if possible
|
||||
import './olm-loader';
|
||||
|
||||
import logger from '../lib/logger';
|
||||
import sdk from '..';
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
import {logger} from '../src/logger';
|
||||
import {MatrixEvent} from "../src/models/event";
|
||||
|
||||
/**
|
||||
* Return a promise that is resolved when the client next emits a
|
||||
@@ -13,7 +11,7 @@ const MatrixEvent = sdk.MatrixEvent;
|
||||
* @param {Number=} count Number of syncs to wait for (default 1)
|
||||
* @return {Promise} Resolves once the client has emitted a SYNCING event
|
||||
*/
|
||||
module.exports.syncPromise = function(client, count) {
|
||||
export function syncPromise(client, count) {
|
||||
if (count === undefined) {
|
||||
count = 1;
|
||||
}
|
||||
@@ -24,7 +22,7 @@ module.exports.syncPromise = function(client, count) {
|
||||
const p = new Promise((resolve, reject) => {
|
||||
const cb = (state) => {
|
||||
logger.log(`${Date.now()} syncPromise(${count}): ${state}`);
|
||||
if (state == 'SYNCING') {
|
||||
if (state === 'SYNCING') {
|
||||
resolve();
|
||||
} else {
|
||||
client.once('sync', cb);
|
||||
@@ -34,9 +32,9 @@ module.exports.syncPromise = function(client, count) {
|
||||
});
|
||||
|
||||
return p.then(() => {
|
||||
return module.exports.syncPromise(client, count-1);
|
||||
return syncPromise(client, count-1);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a spy for an object and automatically spy its methods.
|
||||
@@ -44,7 +42,7 @@ module.exports.syncPromise = function(client, count) {
|
||||
* @param {string} name The name of the class
|
||||
* @return {Object} An instantiated object with spied methods/properties.
|
||||
*/
|
||||
module.exports.mock = function(constr, name) {
|
||||
export function mock(constr, name) {
|
||||
// Based on
|
||||
// http://eclipsesource.com/blogs/2014/03/27/mocks-in-jasmine-tests/
|
||||
const HelperConstr = new Function(); // jshint ignore:line
|
||||
@@ -65,7 +63,7 @@ module.exports.mock = function(constr, name) {
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Event.
|
||||
@@ -78,7 +76,7 @@ module.exports.mock = function(constr, name) {
|
||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||
* @return {Object} a JSON object representing this event.
|
||||
*/
|
||||
module.exports.mkEvent = function(opts) {
|
||||
export function mkEvent(opts) {
|
||||
if (!opts.type || !opts.content) {
|
||||
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
|
||||
}
|
||||
@@ -97,14 +95,14 @@ module.exports.mkEvent = function(opts) {
|
||||
event.state_key = "";
|
||||
}
|
||||
return opts.event ? new MatrixEvent(event) : event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an m.presence event.
|
||||
* @param {Object} opts Values for the presence.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
*/
|
||||
module.exports.mkPresence = function(opts) {
|
||||
export function mkPresence(opts) {
|
||||
if (!opts.user) {
|
||||
throw new Error("Missing user");
|
||||
}
|
||||
@@ -120,7 +118,7 @@ module.exports.mkPresence = function(opts) {
|
||||
},
|
||||
};
|
||||
return opts.event ? new MatrixEvent(event) : event;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an m.room.member event.
|
||||
@@ -135,7 +133,7 @@ module.exports.mkPresence = function(opts) {
|
||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
*/
|
||||
module.exports.mkMembership = function(opts) {
|
||||
export function mkMembership(opts) {
|
||||
opts.type = "m.room.member";
|
||||
if (!opts.skey) {
|
||||
opts.skey = opts.sender || opts.user;
|
||||
@@ -152,8 +150,8 @@ module.exports.mkMembership = function(opts) {
|
||||
if (opts.url) {
|
||||
opts.content.avatar_url = opts.url;
|
||||
}
|
||||
return module.exports.mkEvent(opts);
|
||||
};
|
||||
return mkEvent(opts);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an m.room.message event.
|
||||
@@ -164,7 +162,7 @@ module.exports.mkMembership = function(opts) {
|
||||
* @param {boolean} opts.event True to make a MatrixEvent.
|
||||
* @return {Object|MatrixEvent} The event
|
||||
*/
|
||||
module.exports.mkMessage = function(opts) {
|
||||
export function mkMessage(opts) {
|
||||
opts.type = "m.room.message";
|
||||
if (!opts.msg) {
|
||||
opts.msg = "Random->" + Math.random();
|
||||
@@ -176,8 +174,8 @@ module.exports.mkMessage = function(opts) {
|
||||
msgtype: "m.text",
|
||||
body: opts.msg,
|
||||
};
|
||||
return module.exports.mkEvent(opts);
|
||||
};
|
||||
return mkEvent(opts);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
@@ -185,10 +183,10 @@ module.exports.mkMessage = function(opts) {
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
module.exports.MockStorageApi = function() {
|
||||
export function MockStorageApi() {
|
||||
this.data = {};
|
||||
};
|
||||
module.exports.MockStorageApi.prototype = {
|
||||
}
|
||||
MockStorageApi.prototype = {
|
||||
get length() {
|
||||
return Object.keys(this.data).length;
|
||||
},
|
||||
@@ -213,7 +211,7 @@ module.exports.MockStorageApi.prototype = {
|
||||
* @param {MatrixEvent} event
|
||||
* @returns {Promise} promise which resolves (to `event`) when the event has been decrypted
|
||||
*/
|
||||
module.exports.awaitDecryption = function(event) {
|
||||
export function awaitDecryption(event) {
|
||||
if (!event.isBeingDecrypted()) {
|
||||
return Promise.resolve(event);
|
||||
}
|
||||
@@ -226,19 +224,19 @@ module.exports.awaitDecryption = function(event) {
|
||||
resolve(ev);
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const HttpResponse = module.exports.HttpResponse = function(
|
||||
export function HttpResponse(
|
||||
httpLookups, acceptKeepalives, ignoreUnhandledSync,
|
||||
) {
|
||||
this.httpLookups = httpLookups;
|
||||
this.acceptKeepalives = acceptKeepalives === undefined ? true : acceptKeepalives;
|
||||
this.ignoreUnhandledSync = ignoreUnhandledSync;
|
||||
this.pendingLookup = null;
|
||||
};
|
||||
}
|
||||
|
||||
HttpResponse.prototype.request = function HttpResponse(
|
||||
HttpResponse.prototype.request = function(
|
||||
cb, method, path, qp, data, prefix,
|
||||
) {
|
||||
if (path === HttpResponse.KEEP_ALIVE_PATH && this.acceptKeepalives) {
|
||||
@@ -351,7 +349,7 @@ HttpResponse.defaultResponses = function(userId) {
|
||||
];
|
||||
};
|
||||
|
||||
module.exports.setHttpResponses = function setHttpResponses(
|
||||
export function setHttpResponses(
|
||||
client, responses, acceptKeepalives, ignoreUnhandledSyncs,
|
||||
) {
|
||||
const httpResponseObj = new HttpResponse(
|
||||
@@ -367,4 +365,4 @@ module.exports.setHttpResponses = function setHttpResponses(
|
||||
client._http.authedRequestWithPrefix.mockImplementation(httpReq);
|
||||
client._http.requestWithPrefix.mockImplementation(httpReq);
|
||||
client._http.request.mockImplementation(httpReq);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -13,15 +14,10 @@ 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.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
|
||||
const AutoDiscovery = sdk.AutoDiscovery;
|
||||
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import * as sdk from "../../src";
|
||||
import {AutoDiscovery} from "../../src/autodiscovery";
|
||||
|
||||
describe("AutoDiscovery", function() {
|
||||
let httpBackend = null;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const ContentRepo = require("../../lib/content-repo");
|
||||
import {getHttpUriForMxc} from "../../src/content-repo";
|
||||
|
||||
describe("ContentRepo", function() {
|
||||
const baseUrl = "https://my.home.server";
|
||||
@@ -9,7 +7,7 @@ describe("ContentRepo", function() {
|
||||
it("should do nothing to HTTP URLs when allowing direct links", function() {
|
||||
const httpUrl = "http://example.com/image.jpeg";
|
||||
expect(
|
||||
ContentRepo.getHttpUriForMxc(
|
||||
getHttpUriForMxc(
|
||||
baseUrl, httpUrl, undefined, undefined, undefined, true,
|
||||
),
|
||||
).toEqual(httpUrl);
|
||||
@@ -17,25 +15,25 @@ describe("ContentRepo", function() {
|
||||
|
||||
it("should return the empty string HTTP URLs by default", function() {
|
||||
const httpUrl = "http://example.com/image.jpeg";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
|
||||
expect(getHttpUriForMxc(baseUrl, httpUrl)).toEqual("");
|
||||
});
|
||||
|
||||
it("should return a download URL if no width/height/resize are specified",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid",
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the empty string for null input", function() {
|
||||
expect(ContentRepo.getHttpUriForMxc(null)).toEqual("");
|
||||
expect(getHttpUriForMxc(null)).toEqual("");
|
||||
});
|
||||
|
||||
it("should return a thumbnail URL if a width/height/resize is specified",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32, 64, "crop")).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
|
||||
"?width=32&height=64&method=crop",
|
||||
);
|
||||
@@ -44,7 +42,7 @@ describe("ContentRepo", function() {
|
||||
it("should put fragments from mxc:// URIs after any query parameters",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri, 32)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/thumbnail/server.name/resourceid" +
|
||||
"?width=32#automade",
|
||||
);
|
||||
@@ -53,36 +51,9 @@ describe("ContentRepo", function() {
|
||||
it("should put fragments from mxc:// URIs at the end of the HTTP URI",
|
||||
function() {
|
||||
const mxcUri = "mxc://server.name/resourceid#automade";
|
||||
expect(ContentRepo.getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
expect(getHttpUriForMxc(baseUrl, mxcUri)).toEqual(
|
||||
baseUrl + "/_matrix/media/r0/download/server.name/resourceid#automade",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getIdenticonUri", function() {
|
||||
it("should do nothing for null input", function() {
|
||||
expect(ContentRepo.getIdenticonUri(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it("should set w/h by default to 96", function() {
|
||||
expect(ContentRepo.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(ContentRepo.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(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foo%23bar" +
|
||||
"?width=32&height=64",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+76
-17
@@ -1,26 +1,21 @@
|
||||
import 'source-map-support/register';
|
||||
|
||||
import '../olm-loader';
|
||||
|
||||
import Crypto from '../../lib/crypto';
|
||||
|
||||
import WebStorageSessionStore from '../../lib/store/session/webstorage';
|
||||
import MemoryCryptoStore from '../../lib/crypto/store/memory-crypto-store.js';
|
||||
import MockStorageApi from '../MockStorageApi';
|
||||
import TestClient from '../TestClient';
|
||||
import {MatrixEvent} from '../../lib/models/event';
|
||||
import Room from '../../lib/models/room';
|
||||
import olmlib from '../../lib/crypto/olmlib';
|
||||
import {Crypto} from "../../src/crypto";
|
||||
import {WebStorageSessionStore} from "../../src/store/session/webstorage";
|
||||
import {MemoryCryptoStore} from "../../src/crypto/store/memory-crypto-store";
|
||||
import {MockStorageApi} from "../MockStorageApi";
|
||||
import {TestClient} from "../TestClient";
|
||||
import {MatrixEvent} from "../../src/models/event";
|
||||
import {Room} from "../../src/models/room";
|
||||
import * as olmlib from "../../src/crypto/olmlib";
|
||||
import {sleep} from "../../src/utils";
|
||||
|
||||
const EventEmitter = require("events").EventEmitter;
|
||||
|
||||
const sdk = require("../..");
|
||||
import {EventEmitter} from "events";
|
||||
import {CRYPTO_ENABLED} from "../../src/client";
|
||||
import {DeviceInfo} from "../../src/crypto/deviceinfo";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("Crypto", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,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 = {
|
||||
@@ -319,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);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -15,10 +16,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import DeviceList from '../../../lib/crypto/DeviceList';
|
||||
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
|
||||
import utils from '../../../lib/utils';
|
||||
import logger from '../../../lib/logger';
|
||||
import {logger} from "../../../src/logger";
|
||||
import * as utils from "../../../src/utils";
|
||||
import {MemoryCryptoStore} from "../../../src/crypto/store/memory-crypto-store";
|
||||
import {DeviceList} from "../../../src/crypto/DeviceList";
|
||||
|
||||
const signedDeviceList = {
|
||||
"failures": {},
|
||||
@@ -71,6 +72,8 @@ describe('DeviceList', function() {
|
||||
function createTestDeviceList() {
|
||||
const baseApis = {
|
||||
downloadKeysForUsers: downloadSpy,
|
||||
getUserId: () => '@test1:sw1v.org',
|
||||
deviceId: 'HGKAWHRVJQ',
|
||||
};
|
||||
const mockOlm = {
|
||||
verifySignature: function(key, message, signature) {},
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import '../../../olm-loader';
|
||||
import * as algorithms from "../../../../src/crypto/algorithms";
|
||||
import {MemoryCryptoStore} from "../../../../src/crypto/store/memory-crypto-store";
|
||||
import {MockStorageApi} from "../../../MockStorageApi";
|
||||
import * as testUtils from "../../../test-utils";
|
||||
import {OlmDevice} from "../../../../src/crypto/OlmDevice";
|
||||
import {Crypto} from "../../../../src/crypto";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import {TestClient} from "../../../TestClient";
|
||||
import {Room} from "../../../../src/models/room";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
|
||||
import sdk from '../../../..';
|
||||
import algorithms from '../../../../lib/crypto/algorithms';
|
||||
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
|
||||
import MockStorageApi from '../../../MockStorageApi';
|
||||
import testUtils from '../../../test-utils';
|
||||
import OlmDevice from '../../../../lib/crypto/OlmDevice';
|
||||
import Crypto from '../../../../lib/crypto';
|
||||
import logger from '../../../../lib/logger';
|
||||
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
|
||||
@@ -296,6 +297,10 @@ describe("MegolmDecryption", function() {
|
||||
},
|
||||
}));
|
||||
|
||||
mockCrypto.checkDeviceTrust.mockReturnValue({
|
||||
isVerified: () => false,
|
||||
});
|
||||
|
||||
const megolmEncryption = new MegolmEncryption({
|
||||
userId: '@user:id',
|
||||
crypto: mockCrypto,
|
||||
@@ -319,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();
|
||||
@@ -342,4 +347,348 @@ describe("MegolmDecryption", function() {
|
||||
expect(ct2.session_id).toEqual(ct1.session_id);
|
||||
});
|
||||
});
|
||||
|
||||
it("notifies devices that have been blocked", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient1 = (new TestClient(
|
||||
"@bob:example.com", "bobdevice1",
|
||||
)).client;
|
||||
const bobClient2 = (new TestClient(
|
||||
"@bob:example.com", "bobdevice2",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient1.initCrypto(),
|
||||
bobClient2.initCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient._crypto._olmDevice;
|
||||
const bobDevice1 = bobClient1._crypto._olmDevice;
|
||||
const bobDevice2 = bobClient2._crypto._olmDevice;
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const room = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
room.getEncryptionTargetMembers = async function() {
|
||||
return [{userId: "@bob:example.com"}];
|
||||
};
|
||||
room.setBlacklistUnverifiedDevices(true);
|
||||
aliceClient.store.storeRoom(room);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
|
||||
const BOB_DEVICES = {
|
||||
bobdevice1: {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "bobdevice1",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Dynabook": bobDevice1.deviceEd25519Key,
|
||||
"curve25519:Dynabook": bobDevice1.deviceCurve25519Key,
|
||||
},
|
||||
verified: 0,
|
||||
},
|
||||
bobdevice2: {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "bobdevice2",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:Dynabook": bobDevice2.deviceEd25519Key,
|
||||
"curve25519:Dynabook": bobDevice2.deviceCurve25519Key,
|
||||
},
|
||||
verified: -1,
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient._crypto._deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
|
||||
return this._getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
let run = false;
|
||||
aliceClient.sendToDevice = async (msgtype, contentMap) => {
|
||||
run = true;
|
||||
expect(msgtype).toBe("org.matrix.room_key.withheld");
|
||||
delete contentMap["@bob:example.com"].bobdevice1.session_id;
|
||||
delete contentMap["@bob:example.com"].bobdevice2.session_id;
|
||||
expect(contentMap).toStrictEqual({
|
||||
'@bob:example.com': {
|
||||
bobdevice1: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
code: 'm.unverified',
|
||||
reason:
|
||||
'The sender has disabled encrypting to unverified devices.',
|
||||
sender_key: aliceDevice.deviceCurve25519Key,
|
||||
},
|
||||
bobdevice2: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
code: 'm.blacklisted',
|
||||
reason: 'The sender has blocked you.',
|
||||
sender_key: aliceDevice.deviceCurve25519Key,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$event",
|
||||
content: {
|
||||
msgtype: "m.text",
|
||||
body: "secret",
|
||||
},
|
||||
});
|
||||
await aliceClient._crypto.encryptEvent(event, room);
|
||||
|
||||
expect(run).toBe(true);
|
||||
|
||||
aliceClient.stopClient();
|
||||
bobClient1.stopClient();
|
||||
bobClient2.stopClient();
|
||||
});
|
||||
|
||||
it("notifies devices when unable to create olm session", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const aliceDevice = aliceClient._crypto._olmDevice;
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
|
||||
const encryptionCfg = {
|
||||
"algorithm": "m.megolm.v1.aes-sha2",
|
||||
};
|
||||
const roomId = "!someroom";
|
||||
const aliceRoom = new Room(roomId, aliceClient, "@alice:example.com", {});
|
||||
const bobRoom = new Room(roomId, bobClient, "@bob:example.com", {});
|
||||
aliceClient.store.storeRoom(aliceRoom);
|
||||
bobClient.store.storeRoom(bobRoom);
|
||||
await aliceClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
await bobClient.setRoomEncryption(roomId, encryptionCfg);
|
||||
|
||||
aliceRoom.getEncryptionTargetMembers = async () => {
|
||||
return [
|
||||
{
|
||||
userId: "@alice:example.com",
|
||||
membership: "join",
|
||||
},
|
||||
{
|
||||
userId: "@bob:example.com",
|
||||
membership: "join",
|
||||
},
|
||||
];
|
||||
};
|
||||
const BOB_DEVICES = {
|
||||
bobdevice: {
|
||||
user_id: "@bob:example.com",
|
||||
device_id: "bobdevice",
|
||||
algorithms: [olmlib.OLM_ALGORITHM, olmlib.MEGOLM_ALGORITHM],
|
||||
keys: {
|
||||
"ed25519:bobdevice": bobDevice.deviceEd25519Key,
|
||||
"curve25519:bobdevice": bobDevice.deviceCurve25519Key,
|
||||
},
|
||||
known: true,
|
||||
verified: 1,
|
||||
},
|
||||
};
|
||||
|
||||
aliceClient._crypto._deviceList.storeDevicesForUser(
|
||||
"@bob:example.com", BOB_DEVICES,
|
||||
);
|
||||
aliceClient._crypto._deviceList.downloadKeys = async function(userIds) {
|
||||
return this._getDevicesFromStore(userIds);
|
||||
};
|
||||
|
||||
aliceClient.claimOneTimeKeys = async () => {
|
||||
// Bob has no one-time keys
|
||||
return {
|
||||
one_time_keys: {},
|
||||
};
|
||||
};
|
||||
|
||||
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",
|
||||
sender: "@alice:example.com",
|
||||
room_id: roomId,
|
||||
event_id: "$event",
|
||||
content: {},
|
||||
});
|
||||
await aliceClient._crypto.encryptEvent(event, aliceRoom);
|
||||
await sendPromise;
|
||||
});
|
||||
|
||||
it("throws an error describing why it doesn't have a key", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
||||
type: "org.matrix.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
session_id: "session_id",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
code: "m.blacklisted",
|
||||
reason: "You have been blocked",
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
room_id: roomId,
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: "blablabla",
|
||||
device_id: "bobdevice",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
session_id: "session_id",
|
||||
},
|
||||
}))).rejects.toThrow("The sender has blocked you.");
|
||||
});
|
||||
|
||||
it("throws an error describing the lack of an olm session", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
aliceClient._crypto.downloadKeys = async () => {};
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
||||
type: "org.matrix.room_key.withheld",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
room_id: roomId,
|
||||
session_id: "session_id",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
code: "m.no_olm",
|
||||
reason: "Unable to establish a secure channel.",
|
||||
},
|
||||
}));
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
room_id: roomId,
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: "blablabla",
|
||||
device_id: "bobdevice",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
session_id: "session_id",
|
||||
},
|
||||
origin_server_ts: now,
|
||||
}))).rejects.toThrow("The sender was unable to establish a secure channel.");
|
||||
});
|
||||
|
||||
it("throws an error to indicate a wedged olm session", async function() {
|
||||
const aliceClient = (new TestClient(
|
||||
"@alice:example.com", "alicedevice",
|
||||
)).client;
|
||||
const bobClient = (new TestClient(
|
||||
"@bob:example.com", "bobdevice",
|
||||
)).client;
|
||||
await Promise.all([
|
||||
aliceClient.initCrypto(),
|
||||
bobClient.initCrypto(),
|
||||
]);
|
||||
const bobDevice = bobClient._crypto._olmDevice;
|
||||
aliceClient._crypto.downloadKeys = async () => {};
|
||||
|
||||
const roomId = "!someroom";
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
// pretend we got an event that we can't decrypt
|
||||
aliceClient._crypto._onToDeviceEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
content: {
|
||||
msgtype: "m.bad.encrypted",
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
session_id: "session_id",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
},
|
||||
}));
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
|
||||
await expect(aliceClient._crypto.decryptEvent(new MatrixEvent({
|
||||
type: "m.room.encrypted",
|
||||
sender: "@bob:example.com",
|
||||
event_id: "$event",
|
||||
room_id: roomId,
|
||||
content: {
|
||||
algorithm: "m.megolm.v1.aes-sha2",
|
||||
ciphertext: "blablabla",
|
||||
device_id: "bobdevice",
|
||||
sender_key: bobDevice.deviceCurve25519Key,
|
||||
session_id: "session_id",
|
||||
},
|
||||
origin_server_ts: now,
|
||||
}))).rejects.toThrow("The secure channel with the sender was corrupted.");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2018,2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -15,14 +16,12 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import '../../../olm-loader';
|
||||
|
||||
import MemoryCryptoStore from '../../../../lib/crypto/store/memory-crypto-store.js';
|
||||
import MockStorageApi from '../../../MockStorageApi';
|
||||
import logger from '../../../../lib/logger';
|
||||
|
||||
import OlmDevice from '../../../../lib/crypto/OlmDevice';
|
||||
import olmlib from '../../../../lib/crypto/olmlib';
|
||||
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
|
||||
import {MemoryCryptoStore} from "../../../../src/crypto/store/memory-crypto-store";
|
||||
import {MockStorageApi} from "../../../MockStorageApi";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {OlmDevice} from "../../../../src/crypto/OlmDevice";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
|
||||
|
||||
function makeOlmDevice() {
|
||||
const mockStorage = new MockStorageApi();
|
||||
@@ -42,7 +41,7 @@ async function setupSession(initiator, opponent) {
|
||||
return sid;
|
||||
}
|
||||
|
||||
describe("OlmDecryption", function() {
|
||||
describe("OlmDevice", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running megolm unit tests: libolm not present');
|
||||
return;
|
||||
@@ -82,6 +81,60 @@ describe("OlmDecryption", function() {
|
||||
);
|
||||
});
|
||||
|
||||
it('exports picked account and olm sessions', async function() {
|
||||
const sessionId = await setupSession(aliceOlmDevice, bobOlmDevice);
|
||||
|
||||
const exported = await bobOlmDevice.export();
|
||||
// At this moment only Alice (the “initiator” in setupSession) has a session
|
||||
expect(exported.sessions).toEqual([]);
|
||||
|
||||
const MESSAGE = (
|
||||
"The olm or proteus is an aquatic salamander"
|
||||
+ " in the family Proteidae"
|
||||
);
|
||||
const ciphertext = await aliceOlmDevice.encryptMessage(
|
||||
bobOlmDevice.deviceCurve25519Key,
|
||||
sessionId,
|
||||
MESSAGE,
|
||||
);
|
||||
|
||||
const bobRecreatedOlmDevice = makeOlmDevice();
|
||||
bobRecreatedOlmDevice.init({ fromExportedDevice: exported });
|
||||
|
||||
const decrypted = await bobRecreatedOlmDevice.createInboundSession(
|
||||
aliceOlmDevice.deviceCurve25519Key,
|
||||
ciphertext.type,
|
||||
ciphertext.body,
|
||||
);
|
||||
expect(decrypted.payload).toEqual(MESSAGE);
|
||||
|
||||
const exportedAgain = await bobRecreatedOlmDevice.export();
|
||||
// this time we expect Bob to have a session to export
|
||||
expect(exportedAgain.sessions).toHaveLength(1);
|
||||
|
||||
const MESSAGE_2 = (
|
||||
"In contrast to most amphibians,"
|
||||
+ " the olm is entirely aquatic"
|
||||
);
|
||||
const ciphertext2 = await aliceOlmDevice.encryptMessage(
|
||||
bobOlmDevice.deviceCurve25519Key,
|
||||
sessionId,
|
||||
MESSAGE_2,
|
||||
);
|
||||
|
||||
const bobRecreatedAgainOlmDevice = makeOlmDevice();
|
||||
bobRecreatedAgainOlmDevice.init({ fromExportedDevice: exportedAgain });
|
||||
|
||||
// Note: "decrypted_2" does not have the same structure as "decrypted"
|
||||
const decrypted2 = await bobRecreatedAgainOlmDevice.decryptMessage(
|
||||
aliceOlmDevice.deviceCurve25519Key,
|
||||
decrypted.session_id,
|
||||
ciphertext2.type,
|
||||
ciphertext2.body,
|
||||
);
|
||||
expect(decrypted2).toEqual(MESSAGE_2);
|
||||
});
|
||||
|
||||
it("creates only one session at a time", async function() {
|
||||
// if we call ensureOlmSessionsForDevices multiple times, it should
|
||||
// only try to create one session at a time, even if the server is
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -15,23 +16,21 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import '../../olm-loader';
|
||||
|
||||
import sdk from '../../..';
|
||||
import algorithms from '../../../lib/crypto/algorithms';
|
||||
import WebStorageSessionStore from '../../../lib/store/session/webstorage';
|
||||
import MemoryCryptoStore from '../../../lib/crypto/store/memory-crypto-store.js';
|
||||
import MockStorageApi from '../../MockStorageApi';
|
||||
import testUtils from '../../test-utils';
|
||||
|
||||
import OlmDevice from '../../../lib/crypto/OlmDevice';
|
||||
import Crypto from '../../../lib/crypto';
|
||||
import logger from '../../../lib/logger';
|
||||
import olmlib from '../../../lib/crypto/olmlib';
|
||||
import {logger} from "../../../src/logger";
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import {MatrixClient} from "../../../src/client";
|
||||
import {MatrixEvent} from "../../../src/models/event";
|
||||
import * as algorithms from "../../../src/crypto/algorithms";
|
||||
import {WebStorageSessionStore} from "../../../src/store/session/webstorage";
|
||||
import {MemoryCryptoStore} from "../../../src/crypto/store/memory-crypto-store";
|
||||
import {MockStorageApi} from "../../MockStorageApi";
|
||||
import * 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;
|
||||
|
||||
const MatrixClient = sdk.MatrixClient;
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
|
||||
|
||||
const ROOM_ID = '!ROOM:ID';
|
||||
@@ -334,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(
|
||||
@@ -519,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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -543,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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,14 +16,13 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import '../../olm-loader';
|
||||
|
||||
import anotherjson from 'another-json';
|
||||
|
||||
import olmlib from '../../../lib/crypto/olmlib';
|
||||
|
||||
import TestClient from '../../TestClient';
|
||||
|
||||
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 = {};
|
||||
@@ -51,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;
|
||||
}
|
||||
|
||||
@@ -70,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"},
|
||||
@@ -82,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: {
|
||||
@@ -178,7 +232,7 @@ describe("Cross Signing", function() {
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
path: "/keys/upload",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
@@ -241,7 +295,7 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
path: "/keys/upload",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
@@ -277,7 +331,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();
|
||||
@@ -367,7 +421,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,
|
||||
@@ -433,7 +487,7 @@ describe("Cross Signing", function() {
|
||||
HttpResponse.PUSH_RULES_RESPONSE,
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
path: "/keys/upload",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
@@ -491,7 +545,7 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
path: "/keys/upload",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
@@ -524,7 +578,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();
|
||||
@@ -592,7 +646,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();
|
||||
@@ -744,7 +798,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"],
|
||||
@@ -770,7 +824,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);
|
||||
});
|
||||
});
|
||||
@@ -15,28 +15,53 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import '../../olm-loader';
|
||||
import * as olmlib from "../../../src/crypto/olmlib";
|
||||
import {SECRET_STORAGE_ALGORITHM_V1_AES} from "../../../src/crypto/SecretStorage";
|
||||
import {MatrixEvent} from "../../../src/models/event";
|
||||
import {TestClient} from '../../TestClient';
|
||||
import {makeTestClients} from './verification/util';
|
||||
import {encryptAES} from "../../../src/crypto/aes";
|
||||
import {resetCrossSigningKeys, createSecretStorageKey} from "./crypto-utils";
|
||||
import {logger} from '../../../src/logger';
|
||||
|
||||
import { MatrixEvent } from '../../../lib/models/event';
|
||||
import { SECRET_STORAGE_ALGORITHM_V1 } from '../../../lib/crypto/SecretStorage';
|
||||
import * as utils from "../../../src/utils";
|
||||
|
||||
import olmlib from '../../../lib/crypto/olmlib';
|
||||
|
||||
import TestClient from '../../TestClient';
|
||||
import { makeTestClients } from './verification/util';
|
||||
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(
|
||||
userInfo.userId, userInfo.deviceId, undefined, undefined, options,
|
||||
)).client;
|
||||
|
||||
// Make it seem as if we've synced and thus the store can be trusted to
|
||||
// contain valid account data.
|
||||
client.isInitialSyncComplete = function() {
|
||||
return true;
|
||||
};
|
||||
|
||||
await client.initCrypto();
|
||||
|
||||
// No need to download keys for these tests
|
||||
client._crypto.downloadKeys = async function() {};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -45,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();
|
||||
@@ -63,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(
|
||||
@@ -94,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');
|
||||
|
||||
@@ -106,11 +129,11 @@ describe("Secrets", function() {
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(secretStorage.isStored("foo")).toBe(false);
|
||||
expect(await secretStorage.isStored("foo")).toBeFalsy();
|
||||
|
||||
await secretStorage.store("foo", "bar", ["abc"]);
|
||||
|
||||
expect(secretStorage.isStored("foo")).toBe(true);
|
||||
expect(await secretStorage.isStored("foo")).toBeTruthy();
|
||||
expect(await secretStorage.get("foo")).toBe("bar");
|
||||
|
||||
expect(getKey).toHaveBeenCalled();
|
||||
@@ -143,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"},
|
||||
@@ -150,6 +180,7 @@ describe("Secrets", function() {
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => keys[t],
|
||||
saveCrossSigningKeys: k => keys = k,
|
||||
getSecretStorageKey: getKey,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -161,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
|
||||
@@ -195,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";
|
||||
},
|
||||
},
|
||||
@@ -245,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(crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
|
||||
expect(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: request => {
|
||||
const defaultKeyId = 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(crossSigning.isStoredInSecretStorage(secretStorage)).toBeTruthy();
|
||||
expect(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==");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
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 {InRoomChannel} from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
|
||||
describe("InRoomChannel tests", function() {
|
||||
const ALICE = "@alice:hs.tld";
|
||||
const BOB = "@bob:hs.tld";
|
||||
const MALORY = "@malory:hs.tld";
|
||||
const client = {
|
||||
getUserId() { return ALICE; },
|
||||
};
|
||||
|
||||
it("getEventType only returns .request for a message with a msgtype", function() {
|
||||
const invalidEvent = new MatrixEvent({
|
||||
type: "m.key.verification.request",
|
||||
});
|
||||
expect(InRoomChannel.getEventType(invalidEvent)).toStrictEqual("");
|
||||
const validEvent = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: { msgtype: "m.key.verification.request" },
|
||||
});
|
||||
expect(InRoomChannel.getEventType(validEvent)).
|
||||
toStrictEqual("m.key.verification.request");
|
||||
const validFooEvent = new MatrixEvent({ type: "m.foo" });
|
||||
expect(InRoomChannel.getEventType(validFooEvent)).
|
||||
toStrictEqual("m.foo");
|
||||
});
|
||||
|
||||
it("getEventType should return m.room.message for messages", function() {
|
||||
const messageEvent = new MatrixEvent({
|
||||
type: "m.room.message",
|
||||
content: { msgtype: "m.text" },
|
||||
});
|
||||
// XXX: The event type doesn't matter too much, just as long as it's not a verification event
|
||||
expect(InRoomChannel.getEventType(messageEvent)).
|
||||
toStrictEqual("m.room.message");
|
||||
});
|
||||
|
||||
it("getEventType should return actual type for non-message events", function() {
|
||||
const event = new MatrixEvent({
|
||||
type: "m.room.member",
|
||||
content: { },
|
||||
});
|
||||
expect(InRoomChannel.getEventType(event)).
|
||||
toStrictEqual("m.room.member");
|
||||
});
|
||||
|
||||
it("getOtherPartyUserId should not return anything for a request not " +
|
||||
"directed at me", function() {
|
||||
const event = new MatrixEvent({
|
||||
sender: BOB,
|
||||
type: "m.room.message",
|
||||
content: { msgtype: "m.key.verification.request", to: MALORY },
|
||||
});
|
||||
expect(InRoomChannel.getOtherPartyUserId(event, client)).toStrictEqual(undefined);
|
||||
});
|
||||
|
||||
it("getOtherPartyUserId should not return anything an event that is not of a valid " +
|
||||
"request type", function() {
|
||||
// invalid because this should be a room message with msgtype
|
||||
const invalidRequest = new MatrixEvent({
|
||||
sender: BOB,
|
||||
type: "m.key.verification.request",
|
||||
content: { to: ALICE },
|
||||
});
|
||||
expect(InRoomChannel.getOtherPartyUserId(invalidRequest, client))
|
||||
.toStrictEqual(undefined);
|
||||
const startEvent = new MatrixEvent({
|
||||
sender: BOB,
|
||||
type: "m.key.verification.start",
|
||||
content: { to: ALICE },
|
||||
});
|
||||
expect(InRoomChannel.getOtherPartyUserId(startEvent, client))
|
||||
.toStrictEqual(undefined);
|
||||
const fooEvent = new MatrixEvent({
|
||||
sender: BOB,
|
||||
type: "m.foo",
|
||||
content: { to: ALICE },
|
||||
});
|
||||
expect(InRoomChannel.getOtherPartyUserId(fooEvent, client))
|
||||
.toStrictEqual(undefined);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -13,17 +14,8 @@ 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 logger from '../../../../lib/logger';
|
||||
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
} catch (e) {
|
||||
logger.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
|
||||
|
||||
import {ShowQRCode, ScanQRCode} from '../../../../lib/crypto/verification/QRCode';
|
||||
import "../../../olm-loader";
|
||||
import {logger} from "../../../../src/logger";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
@@ -37,124 +29,13 @@ describe("QR code verification", function() {
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
describe("showing", function() {
|
||||
it("should emit an event to show a QR code", async function() {
|
||||
const channel = {
|
||||
send: jest.fn(),
|
||||
};
|
||||
const qrCode = new ShowQRCode(channel, {
|
||||
getUserId: () => "@alice:example.com",
|
||||
deviceId: "ABCDEFG",
|
||||
getDeviceEd25519Key: function() {
|
||||
return "device+ed25519+key";
|
||||
},
|
||||
});
|
||||
const spy = jest.fn((e) => {
|
||||
qrCode.done();
|
||||
});
|
||||
qrCode.on("show_qr_code", spy);
|
||||
await qrCode.verify();
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
url: "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
|
||||
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("scanning", function() {
|
||||
const QR_CODE_URL = "https://matrix.to/#/@alice:example.com?device=ABCDEFG"
|
||||
+ "&action=verify&key_ed25519%3AABCDEFG=device%2Bed25519%2Bkey";
|
||||
it("should verify when a QR code is sent", async function() {
|
||||
const device = DeviceInfo.fromStorage(
|
||||
{
|
||||
algorithms: [],
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "device+curve25519+key",
|
||||
"ed25519:ABCDEFG": "device+ed25519+key",
|
||||
},
|
||||
verified: false,
|
||||
known: false,
|
||||
unsigned: {},
|
||||
},
|
||||
"ABCDEFG",
|
||||
);
|
||||
const client = {
|
||||
getStoredDevice: jest.fn().mockReturnValue(device),
|
||||
setDeviceVerified: jest.fn(),
|
||||
};
|
||||
const channel = {
|
||||
send: jest.fn(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(channel, client);
|
||||
qrCode.on("confirm_user_id", ({userId, confirm}) => {
|
||||
if (userId === "@alice:example.com") {
|
||||
confirm();
|
||||
} else {
|
||||
qrCode.cancel(new Error("Incorrect user"));
|
||||
}
|
||||
});
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
await qrCode.verify();
|
||||
expect(client.getStoredDevice)
|
||||
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
|
||||
expect(client.setDeviceVerified)
|
||||
.toHaveBeenCalledWith("@alice:example.com", "ABCDEFG");
|
||||
});
|
||||
|
||||
it("should error when the user ID doesn't match", async function() {
|
||||
const client = {
|
||||
getStoredDevice: jest.fn(),
|
||||
setDeviceVerified: jest.fn(),
|
||||
};
|
||||
const channel = {
|
||||
send: jest.fn(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(channel, client, "@bob:example.com", "ABCDEFG");
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
const spy = jest.fn();
|
||||
await qrCode.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(channel.send).toHaveBeenCalled();
|
||||
expect(client.getStoredDevice).not.toHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should error if the key doesn't match", async function() {
|
||||
const device = DeviceInfo.fromStorage(
|
||||
{
|
||||
algorithms: [],
|
||||
keys: {
|
||||
"curve25519:ABCDEFG": "device+curve25519+key",
|
||||
"ed25519:ABCDEFG": "a+different+device+ed25519+key",
|
||||
},
|
||||
verified: false,
|
||||
known: false,
|
||||
unsigned: {},
|
||||
},
|
||||
"ABCDEFG",
|
||||
);
|
||||
const client = {
|
||||
getStoredDevice: jest.fn().mockReturnValue(device),
|
||||
setDeviceVerified: jest.fn(),
|
||||
};
|
||||
const channel = {
|
||||
send: jest.fn(),
|
||||
};
|
||||
const qrCode = new ScanQRCode(
|
||||
channel, client, "@alice:example.com", "ABCDEFG");
|
||||
qrCode.on("scan", ({done}) => {
|
||||
done(QR_CODE_URL);
|
||||
});
|
||||
const spy = jest.fn();
|
||||
await qrCode.verify().catch(spy);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(channel.send).toHaveBeenCalled();
|
||||
expect(client.getStoredDevice).toHaveBeenCalled();
|
||||
expect(client.setDeviceVerified).not.toHaveBeenCalled();
|
||||
describe("reciprocate", () => {
|
||||
it("should verify the secret", () => {
|
||||
// TODO: Actually write a test for this.
|
||||
// Tests are hard because we are running before the verification
|
||||
// process actually begins, and are largely UI-driven rather than
|
||||
// logic-driven (compared to something like SAS). In the interest
|
||||
// of time, tests are currently excluded.
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -13,32 +14,31 @@ 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 logger from '../../../../lib/logger';
|
||||
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
} catch (e) {
|
||||
logger.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import {verificationMethods} from '../../../../lib/crypto';
|
||||
|
||||
import SAS from '../../../../lib/crypto/verification/SAS';
|
||||
import "../../../olm-loader";
|
||||
import {verificationMethods} from "../../../../src/crypto";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {SAS} from "../../../../src/crypto/verification/SAS";
|
||||
import {makeTestClients, setupWebcrypto, teardownWebcrypto} from './util';
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
import {makeTestClients} from './util';
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("verification request", function() {
|
||||
describe("verification request integration tests with crypto layer", function() {
|
||||
if (!global.Olm) {
|
||||
logger.warn('Not running device verification unit tests: libolm not present');
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("should request and accept a verification", async function() {
|
||||
const [alice, bob] = await makeTestClients(
|
||||
[
|
||||
@@ -71,7 +71,9 @@ describe("verification request", function() {
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
bobVerifier._endTimer();
|
||||
});
|
||||
const aliceVerifier = await alice.client.requestVerification("@bob:example.com");
|
||||
const aliceRequest = await alice.client.requestVerification("@bob:example.com");
|
||||
await aliceRequest.waitFor(r => r.started);
|
||||
const aliceVerifier = aliceRequest.verifier;
|
||||
expect(aliceVerifier).toBeInstanceOf(SAS);
|
||||
|
||||
// XXX: Private function access (but it's a test, so we're okay)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2018-2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -13,29 +14,18 @@ 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 logger from '../../../../lib/logger';
|
||||
|
||||
try {
|
||||
global.Olm = require('olm');
|
||||
} catch (e) {
|
||||
logger.warn("unable to run device verification tests: libolm not available");
|
||||
}
|
||||
|
||||
import olmlib from '../../../../lib/crypto/olmlib';
|
||||
|
||||
import sdk from '../../../..';
|
||||
|
||||
import {verificationMethods} from '../../../../lib/crypto';
|
||||
import DeviceInfo from '../../../../lib/crypto/deviceinfo';
|
||||
|
||||
import SAS from '../../../../lib/crypto/verification/SAS';
|
||||
import "../../../olm-loader";
|
||||
import {makeTestClients, setupWebcrypto, teardownWebcrypto} from './util';
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import {SAS} from "../../../../src/crypto/verification/SAS";
|
||||
import {DeviceInfo} from "../../../../src/crypto/deviceinfo";
|
||||
import {verificationMethods} from "../../../../src/crypto";
|
||||
import * as olmlib from "../../../../src/crypto/olmlib";
|
||||
import {logger} from "../../../../src/logger";
|
||||
import {resetCrossSigningKeys} from "../crypto-utils";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
import {makeTestClients} from './util';
|
||||
|
||||
let ALICE_DEVICES;
|
||||
let BOB_DEVICES;
|
||||
|
||||
@@ -46,11 +36,25 @@ describe("SAS verification", function() {
|
||||
}
|
||||
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
return Olm.init();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
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",
|
||||
@@ -128,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) {
|
||||
@@ -145,7 +149,7 @@ describe("SAS verification", function() {
|
||||
}
|
||||
}
|
||||
});
|
||||
resolve(verifier);
|
||||
resolve(request.verifier);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -178,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);
|
||||
};
|
||||
@@ -209,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
|
||||
@@ -281,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", {
|
||||
@@ -345,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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -453,9 +461,11 @@ describe("SAS verification", function() {
|
||||
});
|
||||
});
|
||||
|
||||
aliceVerifier = await alice.client.requestVerificationDM(
|
||||
bob.client.getUserId(), "!room_id", [verificationMethods.SAS],
|
||||
const aliceRequest = await alice.client.requestVerificationDM(
|
||||
bob.client.getUserId(), "!room_id",
|
||||
);
|
||||
await aliceRequest.waitFor(r => r.started);
|
||||
aliceVerifier = aliceRequest.verifier;
|
||||
aliceVerifier.on("show_sas", (e) => {
|
||||
if (!e.sas.emoji || !e.sas.decimal) {
|
||||
e.cancel();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2019 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,16 +15,16 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import TestClient from '../../../TestClient';
|
||||
|
||||
import sdk from '../../../..';
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
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)) {
|
||||
@@ -34,15 +35,13 @@ export async function makeTestClients(userInfos, options) {
|
||||
content: msg,
|
||||
});
|
||||
const client = clientMap[userId][deviceId];
|
||||
if (event.isEncrypted()) {
|
||||
event.attemptDecryption(client._crypto)
|
||||
.then(() => client.emit("toDeviceEvent", event));
|
||||
} else {
|
||||
setTimeout(
|
||||
() => client.emit("toDeviceEvent", event),
|
||||
0,
|
||||
);
|
||||
}
|
||||
const decryptionPromise = event.isEncrypted() ?
|
||||
event.attemptDecryption(client._crypto) :
|
||||
Promise.resolve();
|
||||
|
||||
decryptionPromise.then(
|
||||
() => client.emit("toDeviceEvent", event),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -51,21 +50,33 @@ 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 event = new MatrixEvent({
|
||||
const rawEvent = {
|
||||
sender: this.getUserId(), // eslint-disable-line babel/no-invalid-this
|
||||
type: type,
|
||||
content: content,
|
||||
room_id: room,
|
||||
event_id: eventId,
|
||||
});
|
||||
for (const tc of clients) {
|
||||
setTimeout(
|
||||
() => tc.client.emit("Room.timeline", event),
|
||||
0,
|
||||
);
|
||||
}
|
||||
origin_server_ts: Date.now(),
|
||||
};
|
||||
const event = new MatrixEvent(rawEvent);
|
||||
const remoteEcho = new MatrixEvent(Object.assign({}, rawEvent, {
|
||||
unsigned: {
|
||||
transaction_id: this.makeTxnId(), // eslint-disable-line babel/no-invalid-this
|
||||
},
|
||||
}));
|
||||
|
||||
return {event_id: eventId};
|
||||
setImmediate(() => {
|
||||
for (const tc of clients) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.resolve({event_id: eventId});
|
||||
};
|
||||
|
||||
for (const userInfo of userInfos) {
|
||||
@@ -93,3 +104,15 @@ export async function makeTestClients(userInfos, options) {
|
||||
|
||||
return clients;
|
||||
}
|
||||
|
||||
export function setupWebcrypto() {
|
||||
global.crypto = {
|
||||
getRandomValues: (buf) => {
|
||||
return nodeCrypto.randomFillSync(buf);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function teardownWebcrypto() {
|
||||
global.crypto = undefined;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
/*
|
||||
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 {VerificationRequest, READY_TYPE, START_TYPE, DONE_TYPE} from
|
||||
"../../../../src/crypto/verification/request/VerificationRequest";
|
||||
import {InRoomChannel} from "../../../../src/crypto/verification/request/InRoomChannel";
|
||||
import {ToDeviceChannel} from
|
||||
"../../../../src/crypto/verification/request/ToDeviceChannel";
|
||||
import {MatrixEvent} from "../../../../src/models/event";
|
||||
import {setupWebcrypto, teardownWebcrypto} from "./util";
|
||||
|
||||
function makeMockClient(userId, deviceId) {
|
||||
let counter = 1;
|
||||
let events = [];
|
||||
const deviceEvents = {};
|
||||
return {
|
||||
getUserId() { return userId; },
|
||||
getDeviceId() { return deviceId; },
|
||||
|
||||
sendEvent(roomId, type, content) {
|
||||
counter = counter + 1;
|
||||
const eventId = `$${userId}-${deviceId}-${counter}`;
|
||||
events.push(new MatrixEvent({
|
||||
sender: userId,
|
||||
event_id: eventId,
|
||||
room_id: roomId,
|
||||
type,
|
||||
content,
|
||||
origin_server_ts: Date.now(),
|
||||
}));
|
||||
return Promise.resolve({event_id: eventId});
|
||||
},
|
||||
|
||||
sendToDevice(type, msgMap) {
|
||||
for (const userId of Object.keys(msgMap)) {
|
||||
const deviceMap = msgMap[userId];
|
||||
for (const deviceId of Object.keys(deviceMap)) {
|
||||
const content = deviceMap[deviceId];
|
||||
const event = new MatrixEvent({content, type});
|
||||
deviceEvents[userId] = deviceEvents[userId] || {};
|
||||
deviceEvents[userId][deviceId] = deviceEvents[userId][deviceId] || [];
|
||||
deviceEvents[userId][deviceId].push(event);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
popEvents() {
|
||||
const e = events;
|
||||
events = [];
|
||||
return e;
|
||||
},
|
||||
|
||||
popDeviceEvents(userId, deviceId) {
|
||||
const forDevice = deviceEvents[userId];
|
||||
const events = forDevice && forDevice[deviceId];
|
||||
const result = events || [];
|
||||
if (events) {
|
||||
delete forDevice[deviceId];
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const MOCK_METHOD = "mock-verify";
|
||||
class MockVerifier {
|
||||
constructor(channel, client, userId, deviceId, startEvent) {
|
||||
this._channel = channel;
|
||||
this._startEvent = startEvent;
|
||||
}
|
||||
|
||||
get events() {
|
||||
return [DONE_TYPE];
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this._startEvent) {
|
||||
await this._channel.send(DONE_TYPE, {});
|
||||
} else {
|
||||
await this._channel.send(START_TYPE, {method: MOCK_METHOD});
|
||||
}
|
||||
}
|
||||
|
||||
async handleEvent(event) {
|
||||
if (event.getType() === DONE_TYPE && !this._startEvent) {
|
||||
await this._channel.send(DONE_TYPE, {});
|
||||
}
|
||||
}
|
||||
|
||||
canSwitchStartEvent() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function makeRemoteEcho(event) {
|
||||
return new MatrixEvent(Object.assign({}, event.event, {
|
||||
unsigned: {
|
||||
transaction_id: "abc",
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
async function distributeEvent(ownRequest, theirRequest, event) {
|
||||
await ownRequest.channel.handleEvent(
|
||||
makeRemoteEcho(event), ownRequest, true);
|
||||
await theirRequest.channel.handleEvent(event, theirRequest, true);
|
||||
}
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
describe("verification request unit tests", function() {
|
||||
beforeAll(function() {
|
||||
setupWebcrypto();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
teardownWebcrypto();
|
||||
});
|
||||
|
||||
it("transition from UNSENT to DONE through happy path", 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([[MOCK_METHOD, MockVerifier]]), alice);
|
||||
const bobRequest = new VerificationRequest(
|
||||
new InRoomChannel(bob, "!room"),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), bob);
|
||||
expect(aliceRequest.invalid).toBe(true);
|
||||
expect(bobRequest.invalid).toBe(true);
|
||||
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
expect(requestEvent.getType()).toBe("m.room.message");
|
||||
await distributeEvent(aliceRequest, bobRequest, requestEvent);
|
||||
expect(aliceRequest.requested).toBe(true);
|
||||
expect(bobRequest.requested).toBe(true);
|
||||
|
||||
await bobRequest.accept();
|
||||
const [readyEvent] = bob.popEvents();
|
||||
expect(readyEvent.getType()).toBe(READY_TYPE);
|
||||
await distributeEvent(bobRequest, aliceRequest, readyEvent);
|
||||
expect(bobRequest.ready).toBe(true);
|
||||
expect(aliceRequest.ready).toBe(true);
|
||||
|
||||
const verifier = aliceRequest.beginKeyVerification(MOCK_METHOD);
|
||||
await verifier.start();
|
||||
const [startEvent] = alice.popEvents();
|
||||
expect(startEvent.getType()).toBe(START_TYPE);
|
||||
await distributeEvent(aliceRequest, bobRequest, startEvent);
|
||||
expect(aliceRequest.started).toBe(true);
|
||||
expect(aliceRequest.verifier).toBeInstanceOf(MockVerifier);
|
||||
expect(bobRequest.started).toBe(true);
|
||||
expect(bobRequest.verifier).toBeInstanceOf(MockVerifier);
|
||||
|
||||
await bobRequest.verifier.start();
|
||||
const [bobDoneEvent] = bob.popEvents();
|
||||
expect(bobDoneEvent.getType()).toBe(DONE_TYPE);
|
||||
await distributeEvent(bobRequest, aliceRequest, bobDoneEvent);
|
||||
const [aliceDoneEvent] = alice.popEvents();
|
||||
expect(aliceDoneEvent.getType()).toBe(DONE_TYPE);
|
||||
await distributeEvent(aliceRequest, bobRequest, aliceDoneEvent);
|
||||
expect(aliceRequest.done).toBe(true);
|
||||
expect(bobRequest.done).toBe(true);
|
||||
});
|
||||
|
||||
it("methods only contains common methods", 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([["c", function() {}], ["a", function() {}]]), alice);
|
||||
const bobRequest = new VerificationRequest(
|
||||
new InRoomChannel(bob, "!room"),
|
||||
new Map([["c", function() {}], ["b", function() {}]]), bob);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
await distributeEvent(aliceRequest, bobRequest, requestEvent);
|
||||
await bobRequest.accept();
|
||||
const [readyEvent] = bob.popEvents();
|
||||
await distributeEvent(bobRequest, aliceRequest, readyEvent);
|
||||
expect(aliceRequest.methods).toStrictEqual(["c"]);
|
||||
expect(bobRequest.methods).toStrictEqual(["c"]);
|
||||
});
|
||||
|
||||
it("other client accepting request puts it in observeOnly mode", async function() {
|
||||
const alice = makeMockClient("@alice:matrix.tld", "device1");
|
||||
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
|
||||
const aliceRequest = new VerificationRequest(
|
||||
new InRoomChannel(alice, "!room", bob1.getUserId()), new Map(), alice);
|
||||
await aliceRequest.sendRequest();
|
||||
const [requestEvent] = alice.popEvents();
|
||||
const bob1Request = new VerificationRequest(
|
||||
new InRoomChannel(bob1, "!room"), new Map(), bob1);
|
||||
const bob2Request = new VerificationRequest(
|
||||
new InRoomChannel(bob2, "!room"), new Map(), bob2);
|
||||
|
||||
await bob1Request.channel.handleEvent(requestEvent, bob1Request, true);
|
||||
await bob2Request.channel.handleEvent(requestEvent, bob2Request, true);
|
||||
|
||||
await bob1Request.accept();
|
||||
const [readyEvent] = bob1.popEvents();
|
||||
expect(bob2Request.observeOnly).toBe(false);
|
||||
await bob2Request.channel.handleEvent(readyEvent, bob2Request, true);
|
||||
expect(bob2Request.observeOnly).toBe(true);
|
||||
});
|
||||
|
||||
it("verify own device with to_device messages", async function() {
|
||||
const bob1 = makeMockClient("@bob:matrix.tld", "device1");
|
||||
const bob2 = makeMockClient("@bob:matrix.tld", "device2");
|
||||
const bob1Request = new VerificationRequest(
|
||||
new ToDeviceChannel(bob1, bob1.getUserId(), ["device1", "device2"],
|
||||
ToDeviceChannel.makeTransactionId(), "device2"),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), bob1);
|
||||
const to = {userId: "@bob:matrix.tld", deviceId: "device2"};
|
||||
const verifier = bob1Request.beginKeyVerification(MOCK_METHOD, to);
|
||||
expect(verifier).toBeInstanceOf(MockVerifier);
|
||||
await verifier.start();
|
||||
const [startEvent] = bob1.popDeviceEvents(to.userId, to.deviceId);
|
||||
expect(startEvent.getType()).toBe(START_TYPE);
|
||||
const bob2Request = new VerificationRequest(
|
||||
new ToDeviceChannel(bob2, bob2.getUserId(), ["device1"]),
|
||||
new Map([[MOCK_METHOD, MockVerifier]]), bob2);
|
||||
|
||||
await bob2Request.channel.handleEvent(startEvent, bob2Request, true);
|
||||
await bob2Request.verifier.start();
|
||||
const [doneEvent1] = bob2.popDeviceEvents("@bob:matrix.tld", "device1");
|
||||
expect(doneEvent1.getType()).toBe(DONE_TYPE);
|
||||
await bob1Request.channel.handleEvent(doneEvent1, bob1Request, true);
|
||||
const [doneEvent2] = bob1.popDeviceEvents("@bob:matrix.tld", "device2");
|
||||
expect(doneEvent2.getType()).toBe(DONE_TYPE);
|
||||
await bob2Request.channel.handleEvent(doneEvent2, bob2Request, true);
|
||||
|
||||
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());
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,10 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const EventTimeline = sdk.EventTimeline;
|
||||
const utils = require("../test-utils");
|
||||
import * as utils from "../test-utils";
|
||||
import {EventTimeline} from "../../src/models/event-timeline";
|
||||
import {RoomState} from "../../src/models/room-state";
|
||||
|
||||
function mockRoomStates(timeline) {
|
||||
timeline._startState = utils.mock(sdk.RoomState, "startState");
|
||||
timeline._endState = utils.mock(sdk.RoomState, "endState");
|
||||
timeline._startState = utils.mock(RoomState, "startState");
|
||||
timeline._endState = utils.mock(RoomState, "endState");
|
||||
}
|
||||
|
||||
describe("EventTimeline", function() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundaction C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,10 +15,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import sdk from '../..';
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
import logger from '../../lib/logger';
|
||||
import {logger} from "../../src/logger";
|
||||
import {MatrixEvent} from "../../src/models/event";
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
describe(".attemptDecryption", () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,4 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const Filter = sdk.Filter;
|
||||
import {Filter} from "../../src/filter";
|
||||
|
||||
describe("Filter", function() {
|
||||
const filterId = "f1lt3ring15g00d4ursoul";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -13,15 +14,10 @@ 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.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
|
||||
const InteractiveAuth = sdk.InteractiveAuth;
|
||||
const MatrixError = sdk.MatrixError;
|
||||
|
||||
import logger from '../../lib/logger';
|
||||
import {logger} from "../../src/logger";
|
||||
import {InteractiveAuth} from "../../src/interactive-auth";
|
||||
import {MatrixError} from "../../src/http-api";
|
||||
|
||||
// Trivial client object to test interactive auth
|
||||
// (we do not need TestClient here)
|
||||
@@ -101,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: [
|
||||
@@ -147,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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import TestClient from '../TestClient';
|
||||
import {TestClient} from '../TestClient';
|
||||
|
||||
describe('Login request', function() {
|
||||
let client;
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const MatrixClient = sdk.MatrixClient;
|
||||
|
||||
import logger from '../../lib/logger';
|
||||
import {logger} from "../../src/logger";
|
||||
import {MatrixClient} from "../../src/client";
|
||||
import {Filter} from "../../src/filter";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -180,7 +177,7 @@ describe("MatrixClient", function() {
|
||||
httpLookups.push(SYNC_RESPONSE);
|
||||
const filterId = "ehfewf";
|
||||
store.getFilterIdByName.mockReturnValue(filterId);
|
||||
const filter = new sdk.Filter(0, filterId);
|
||||
const filter = new Filter(0, filterId);
|
||||
filter.setDefinition({"room": {"timeline": {"limit": 8}}});
|
||||
store.getFilter.mockReturnValue(filter);
|
||||
const syncPromise = new Promise((resolve, reject) => {
|
||||
@@ -247,7 +244,7 @@ describe("MatrixClient", function() {
|
||||
|
||||
const filterName = getFilterName(client.credentials.userId);
|
||||
client.store.setFilterIdByName(filterName, invalidFilterId);
|
||||
const filter = new sdk.Filter(client.credentials.userId);
|
||||
const filter = new Filter(client.credentials.userId);
|
||||
|
||||
client.getOrCreateFilter(filterName, filter).then(function(filterId) {
|
||||
expect(filterId).toEqual(FILTER_RESPONSE.data.filter_id);
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const PushProcessor = require("../../lib/pushprocessor");
|
||||
const utils = require("../test-utils");
|
||||
import * as utils from "../test-utils";
|
||||
import {PushProcessor} from "../../src/pushprocessor";
|
||||
|
||||
describe('NotificationService', function() {
|
||||
const testUserId = "@ali:matrix.org";
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
"use strict";
|
||||
|
||||
import 'source-map-support/register';
|
||||
const callbacks = require("../../src/realtime-callbacks");
|
||||
import * as callbacks from "../../src/realtime-callbacks";
|
||||
|
||||
let wallTime = 1234567890;
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const RoomMember = sdk.RoomMember;
|
||||
const utils = require("../test-utils");
|
||||
import * as utils from "../test-utils";
|
||||
import {RoomMember} from "../../src/models/room-member";
|
||||
|
||||
describe("RoomMember", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -36,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);
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const RoomState = sdk.RoomState;
|
||||
const RoomMember = sdk.RoomMember;
|
||||
const utils = require("../test-utils");
|
||||
import * as utils from "../test-utils";
|
||||
import {RoomState} from "../../src/models/room-state";
|
||||
import {RoomMember} from "../../src/models/room-member";
|
||||
|
||||
describe("RoomState", function() {
|
||||
const roomId = "!foo:bar";
|
||||
|
||||
+13
-28
@@ -1,12 +1,8 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const Room = sdk.Room;
|
||||
const RoomState = sdk.RoomState;
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
const EventStatus = sdk.EventStatus;
|
||||
const EventTimeline = sdk.EventTimeline;
|
||||
const utils = require("../test-utils");
|
||||
import * as utils from "../test-utils";
|
||||
import {EventStatus, MatrixEvent} from "../../src/models/event";
|
||||
import {EventTimeline} from "../../src/models/event-timeline";
|
||||
import {RoomState} from "../../src/models/room-state";
|
||||
import {Room} from "../../src/models/room";
|
||||
|
||||
describe("Room", function() {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -20,9 +16,9 @@ describe("Room", function() {
|
||||
room = new Room(roomId);
|
||||
// mock RoomStates
|
||||
room.oldState = room.getLiveTimeline()._startState =
|
||||
utils.mock(sdk.RoomState, "oldState");
|
||||
utils.mock(RoomState, "oldState");
|
||||
room.currentState = room.getLiveTimeline()._endState =
|
||||
utils.mock(sdk.RoomState, "currentState");
|
||||
utils.mock(RoomState, "currentState");
|
||||
});
|
||||
|
||||
describe("getAvatarUrl", function() {
|
||||
@@ -49,12 +45,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);
|
||||
@@ -621,15 +611,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,
|
||||
})]);
|
||||
};
|
||||
@@ -866,7 +851,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);
|
||||
@@ -876,7 +861,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);
|
||||
@@ -1388,7 +1373,7 @@ describe("Room", function() {
|
||||
let hasThrown = false;
|
||||
try {
|
||||
await room.loadMembersIfNeeded();
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
hasThrown = true;
|
||||
}
|
||||
expect(hasThrown).toEqual(true);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
// This file had a function whose name is all caps, which displeases eslint
|
||||
/* eslint new-cap: "off" */
|
||||
|
||||
import 'source-map-support/register';
|
||||
import {defer} from '../../src/utils';
|
||||
const sdk = require("../..");
|
||||
const MatrixScheduler = sdk.MatrixScheduler;
|
||||
const MatrixError = sdk.MatrixError;
|
||||
const utils = require("../test-utils");
|
||||
import {MatrixError} from "../../src/http-api";
|
||||
import {MatrixScheduler} from "../../src/scheduler";
|
||||
import * as utils from "../test-utils";
|
||||
|
||||
jest.useFakeTimers();
|
||||
|
||||
@@ -145,7 +143,7 @@ describe("MatrixScheduler", function() {
|
||||
deferA.reject({});
|
||||
try {
|
||||
await globalA;
|
||||
} catch(err) {
|
||||
} catch (err) {
|
||||
await Promise.resolve();
|
||||
expect(procCount).toEqual(2);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -14,11 +15,40 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
import sdk from "../..";
|
||||
import {SyncAccumulator} from "../../src/sync-accumulator";
|
||||
|
||||
const SyncAccumulator = sdk.SyncAccumulator;
|
||||
// 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;
|
||||
@@ -371,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]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const EventTimeline = sdk.EventTimeline;
|
||||
const TimelineWindow = sdk.TimelineWindow;
|
||||
const TimelineIndex = require("../../lib/timeline-window").TimelineIndex;
|
||||
|
||||
const utils = require("../test-utils");
|
||||
import {EventTimeline} from "../../src/models/event-timeline";
|
||||
import {TimelineIndex, TimelineWindow} from "../../src/timeline-window";
|
||||
import * as utils from "../test-utils";
|
||||
|
||||
const ROOM_ID = "roomId";
|
||||
const USER_ID = "userId";
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const sdk = require("../..");
|
||||
const User = sdk.User;
|
||||
const utils = require("../test-utils");
|
||||
import {User} from "../../src/models/user";
|
||||
import * as utils from "../test-utils";
|
||||
|
||||
describe("User", function() {
|
||||
const userId = "@alice:bar";
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
const utils = require("../../lib/utils");
|
||||
import * as utils from "../../src/utils";
|
||||
|
||||
describe("utils", function() {
|
||||
describe("encodeParams", function() {
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
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} 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() {}
|
||||
}
|
||||
|
||||
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 = () => {};
|
||||
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() {
|
||||
await call.placeVoiceCall();
|
||||
await call.onAnswerReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 0,
|
||||
call_id: call.callId,
|
||||
party_id: 'the_correct_party_id',
|
||||
answer: {
|
||||
sdp: DUMMY_SDP,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
call.peerConn.addIceCandidate = jest.fn();
|
||||
call.onRemoteIceCandidatesReceived({
|
||||
getContent: () => {
|
||||
return {
|
||||
version: 0,
|
||||
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: 0,
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
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
|
||||
|
||||
// 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",
|
||||
KeyVerificationRequest = "m.key.verification.request",
|
||||
KeyVerificationStart = "m.key.verification.start",
|
||||
KeyVerificationCancel = "m.key.verification.cancel",
|
||||
KeyVerificationMac = "m.key.verification.mac",
|
||||
// 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",
|
||||
}
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
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 MediaDevices {
|
||||
// This is experimental and types don't know about it yet
|
||||
// https://github.com/microsoft/TypeScript/issues/33232
|
||||
getDisplayMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
|
||||
}
|
||||
|
||||
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
-1
@@ -20,7 +20,7 @@ limitations under the License.
|
||||
* @module
|
||||
*/
|
||||
|
||||
export default class Reemitter {
|
||||
export class ReEmitter {
|
||||
constructor(target) {
|
||||
this.target = target;
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ limitations under the License.
|
||||
|
||||
/** @module auto-discovery */
|
||||
|
||||
import logger from './logger';
|
||||
import { URL as NodeURL } from "url";
|
||||
import {logger} from './logger';
|
||||
import {URL as NodeURL} from "url";
|
||||
|
||||
// Dev note: Auto discovery is part of the spec.
|
||||
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+218
-287
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import * as matrixcs from "./matrix";
|
||||
import request from "browser-request";
|
||||
import queryString from "qs";
|
||||
|
||||
matrixcs.request(function(opts, fn) {
|
||||
// We manually fix the query string for browser-request because
|
||||
// it doesn't correctly handle cases like ?via=one&via=two. Instead
|
||||
// we mimic `request`'s query string interface to make it all work
|
||||
// as expected.
|
||||
// browser-request will happily take the constructed string as the
|
||||
// query string without trying to modify it further.
|
||||
opts.qs = queryString.stringify(opts.qs || {}, opts.qsStringifyOptions);
|
||||
return request(opts, fn);
|
||||
});
|
||||
|
||||
// just *accessing* indexedDB throws an exception in firefox with
|
||||
// indexeddb disabled.
|
||||
let indexedDB;
|
||||
try {
|
||||
indexedDB = global.indexedDB;
|
||||
} catch (e) {}
|
||||
|
||||
// if our browser (appears to) support indexeddb, use an indexeddb crypto store.
|
||||
if (indexedDB) {
|
||||
matrixcs.setCryptoStoreFactory(
|
||||
function() {
|
||||
return new matrixcs.IndexedDBCryptoStore(
|
||||
indexedDB, "matrix-js-sdk:crypto",
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// We export 3 things to make browserify happy as well as downstream projects.
|
||||
// It's awkward, but required.
|
||||
export * from "./matrix";
|
||||
export default matrixcs; // keep export for browserify package deps
|
||||
global.matrixcs = matrixcs;
|
||||
+949
-579
File diff suppressed because it is too large
Load Diff
+77
-78
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -13,88 +14,86 @@ 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.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/** @module ContentHelpers */
|
||||
module.exports = {
|
||||
/**
|
||||
* Generates the content for a HTML Message event
|
||||
* @param {string} body the plaintext body of the message
|
||||
* @param {string} htmlBody the HTML representation of the message
|
||||
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
||||
*/
|
||||
makeHtmlMessage: function(body, htmlBody) {
|
||||
return {
|
||||
msgtype: "m.text",
|
||||
format: "org.matrix.custom.html",
|
||||
body: body,
|
||||
formatted_body: htmlBody,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Generates the content for a HTML Notice event
|
||||
* @param {string} body the plaintext body of the notice
|
||||
* @param {string} htmlBody the HTML representation of the notice
|
||||
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
||||
*/
|
||||
makeHtmlNotice: function(body, htmlBody) {
|
||||
return {
|
||||
msgtype: "m.notice",
|
||||
format: "org.matrix.custom.html",
|
||||
body: body,
|
||||
formatted_body: htmlBody,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Generates the content for a HTML Message event
|
||||
* @param {string} body the plaintext body of the message
|
||||
* @param {string} htmlBody the HTML representation of the message
|
||||
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
||||
*/
|
||||
export function makeHtmlMessage(body, htmlBody) {
|
||||
return {
|
||||
msgtype: "m.text",
|
||||
format: "org.matrix.custom.html",
|
||||
body: body,
|
||||
formatted_body: htmlBody,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the content for a HTML Emote event
|
||||
* @param {string} body the plaintext body of the emote
|
||||
* @param {string} htmlBody the HTML representation of the emote
|
||||
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
||||
*/
|
||||
makeHtmlEmote: function(body, htmlBody) {
|
||||
return {
|
||||
msgtype: "m.emote",
|
||||
format: "org.matrix.custom.html",
|
||||
body: body,
|
||||
formatted_body: htmlBody,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Generates the content for a HTML Notice event
|
||||
* @param {string} body the plaintext body of the notice
|
||||
* @param {string} htmlBody the HTML representation of the notice
|
||||
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
||||
*/
|
||||
export function makeHtmlNotice(body, htmlBody) {
|
||||
return {
|
||||
msgtype: "m.notice",
|
||||
format: "org.matrix.custom.html",
|
||||
body: body,
|
||||
formatted_body: htmlBody,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the content for a Plaintext Message event
|
||||
* @param {string} body the plaintext body of the emote
|
||||
* @returns {{msgtype: string, body: string}}
|
||||
*/
|
||||
makeTextMessage: function(body) {
|
||||
return {
|
||||
msgtype: "m.text",
|
||||
body: body,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Generates the content for a HTML Emote event
|
||||
* @param {string} body the plaintext body of the emote
|
||||
* @param {string} htmlBody the HTML representation of the emote
|
||||
* @returns {{msgtype: string, format: string, body: string, formatted_body: string}}
|
||||
*/
|
||||
export function makeHtmlEmote(body, htmlBody) {
|
||||
return {
|
||||
msgtype: "m.emote",
|
||||
format: "org.matrix.custom.html",
|
||||
body: body,
|
||||
formatted_body: htmlBody,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the content for a Plaintext Notice event
|
||||
* @param {string} body the plaintext body of the notice
|
||||
* @returns {{msgtype: string, body: string}}
|
||||
*/
|
||||
makeNotice: function(body) {
|
||||
return {
|
||||
msgtype: "m.notice",
|
||||
body: body,
|
||||
};
|
||||
},
|
||||
/**
|
||||
* Generates the content for a Plaintext Message event
|
||||
* @param {string} body the plaintext body of the emote
|
||||
* @returns {{msgtype: string, body: string}}
|
||||
*/
|
||||
export function makeTextMessage(body) {
|
||||
return {
|
||||
msgtype: "m.text",
|
||||
body: body,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the content for a Plaintext Emote event
|
||||
* @param {string} body the plaintext body of the emote
|
||||
* @returns {{msgtype: string, body: string}}
|
||||
*/
|
||||
makeEmoteMessage: function(body) {
|
||||
return {
|
||||
msgtype: "m.emote",
|
||||
body: body,
|
||||
};
|
||||
},
|
||||
};
|
||||
/**
|
||||
* Generates the content for a Plaintext Notice event
|
||||
* @param {string} body the plaintext body of the notice
|
||||
* @returns {{msgtype: string, body: string}}
|
||||
*/
|
||||
export function makeNotice(body) {
|
||||
return {
|
||||
msgtype: "m.notice",
|
||||
body: body,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the content for a Plaintext Emote event
|
||||
* @param {string} body the plaintext body of the emote
|
||||
* @returns {{msgtype: string, body: string}}
|
||||
*/
|
||||
export function makeEmoteMessage(body) {
|
||||
return {
|
||||
msgtype: "m.emote",
|
||||
body: body,
|
||||
};
|
||||
}
|
||||
|
||||
+54
-87
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2015, 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -16,95 +17,61 @@ limitations under the License.
|
||||
/**
|
||||
* @module content-repo
|
||||
*/
|
||||
const utils = require("./utils");
|
||||
|
||||
/** Content Repo utility functions */
|
||||
module.exports = {
|
||||
/**
|
||||
* Get the HTTP URL for an MXC URI.
|
||||
* @param {string} baseUrl The base homeserver url which has a content repo.
|
||||
* @param {string} mxc The mxc:// URI.
|
||||
* @param {Number} width The desired width of the thumbnail.
|
||||
* @param {Number} height The desired height of the thumbnail.
|
||||
* @param {string} resizeMethod The thumbnail resize method to use, either
|
||||
* "crop" or "scale".
|
||||
* @param {Boolean} allowDirectLinks If true, return any non-mxc URLs
|
||||
* directly. Fetching such URLs will leak information about the user to
|
||||
* anyone they share a room with. If false, will return the emptry string
|
||||
* for such URLs.
|
||||
* @return {string} The complete URL to the content.
|
||||
*/
|
||||
getHttpUriForMxc: function(baseUrl, mxc, width, height,
|
||||
resizeMethod, allowDirectLinks) {
|
||||
if (typeof mxc !== "string" || !mxc) {
|
||||
import * as utils from "./utils";
|
||||
|
||||
/**
|
||||
* Get the HTTP URL for an MXC URI.
|
||||
* @param {string} baseUrl The base homeserver url which has a content repo.
|
||||
* @param {string} mxc The mxc:// URI.
|
||||
* @param {Number} width The desired width of the thumbnail.
|
||||
* @param {Number} height The desired height of the thumbnail.
|
||||
* @param {string} resizeMethod The thumbnail resize method to use, either
|
||||
* "crop" or "scale".
|
||||
* @param {Boolean} allowDirectLinks If true, return any non-mxc URLs
|
||||
* directly. Fetching such URLs will leak information about the user to
|
||||
* anyone they share a room with. If false, will return the emptry string
|
||||
* for such URLs.
|
||||
* @return {string} The complete URL to the content.
|
||||
*/
|
||||
export function getHttpUriForMxc(baseUrl, mxc, width, height,
|
||||
resizeMethod, allowDirectLinks) {
|
||||
if (typeof mxc !== "string" || !mxc) {
|
||||
return '';
|
||||
}
|
||||
if (mxc.indexOf("mxc://") !== 0) {
|
||||
if (allowDirectLinks) {
|
||||
return mxc;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
if (mxc.indexOf("mxc://") !== 0) {
|
||||
if (allowDirectLinks) {
|
||||
return mxc;
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
let serverAndMediaId = mxc.slice(6); // strips mxc://
|
||||
let prefix = "/_matrix/media/r0/download/";
|
||||
const params = {};
|
||||
}
|
||||
let serverAndMediaId = mxc.slice(6); // strips mxc://
|
||||
let prefix = "/_matrix/media/r0/download/";
|
||||
const params = {};
|
||||
|
||||
if (width) {
|
||||
params.width = Math.round(width);
|
||||
}
|
||||
if (height) {
|
||||
params.height = Math.round(height);
|
||||
}
|
||||
if (resizeMethod) {
|
||||
params.method = resizeMethod;
|
||||
}
|
||||
if (utils.keys(params).length > 0) {
|
||||
// these are thumbnailing params so they probably want the
|
||||
// thumbnailing API...
|
||||
prefix = "/_matrix/media/r0/thumbnail/";
|
||||
}
|
||||
if (width) {
|
||||
params.width = Math.round(width);
|
||||
}
|
||||
if (height) {
|
||||
params.height = Math.round(height);
|
||||
}
|
||||
if (resizeMethod) {
|
||||
params.method = resizeMethod;
|
||||
}
|
||||
if (utils.keys(params).length > 0) {
|
||||
// these are thumbnailing params so they probably want the
|
||||
// thumbnailing API...
|
||||
prefix = "/_matrix/media/r0/thumbnail/";
|
||||
}
|
||||
|
||||
const fragmentOffset = serverAndMediaId.indexOf("#");
|
||||
let fragment = "";
|
||||
if (fragmentOffset >= 0) {
|
||||
fragment = serverAndMediaId.substr(fragmentOffset);
|
||||
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
|
||||
}
|
||||
return baseUrl + prefix + serverAndMediaId +
|
||||
(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.
|
||||
*/
|
||||
getIdenticonUri: function(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)));
|
||||
},
|
||||
};
|
||||
const fragmentOffset = serverAndMediaId.indexOf("#");
|
||||
let fragment = "";
|
||||
if (fragmentOffset >= 0) {
|
||||
fragment = serverAndMediaId.substr(fragmentOffset);
|
||||
serverAndMediaId = serverAndMediaId.substr(0, fragmentOffset);
|
||||
}
|
||||
return baseUrl + prefix + serverAndMediaId +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params))) + fragment;
|
||||
}
|
||||
|
||||
+334
-57
@@ -20,9 +20,13 @@ limitations under the License.
|
||||
* @module crypto/CrossSigning
|
||||
*/
|
||||
|
||||
import {pkSign, pkVerify, encodeBase64, decodeBase64} from './olmlib';
|
||||
import {decodeBase64, encodeBase64, pkSign, pkVerify} from './olmlib';
|
||||
import {EventEmitter} from 'events';
|
||||
import logger from '../logger';
|
||||
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
|
||||
*/
|
||||
isStoredInSecretStorage(secretStorage) {
|
||||
let stored = true;
|
||||
for (const type of ["master", "self_signing", "user_signing"]) {
|
||||
stored &= secretStorage.isStored(`m.cross_signing.${type}`, false);
|
||||
async isStoredInSecretStorage(secretStorage) {
|
||||
// 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(
|
||||
@@ -348,6 +459,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
|
||||
async signUser(key) {
|
||||
if (!this.keys.user_signing) {
|
||||
logger.info("No user signing key: not signing user");
|
||||
return;
|
||||
}
|
||||
return this.signObject(key.keys.master, "user_signing");
|
||||
@@ -360,6 +472,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
);
|
||||
}
|
||||
if (!this.keys.self_signing) {
|
||||
logger.info("No self signing key: not signing device");
|
||||
return;
|
||||
}
|
||||
return this.signObject(
|
||||
@@ -387,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;
|
||||
@@ -405,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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -414,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);
|
||||
@@ -436,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) {
|
||||
@@ -463,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;
|
||||
}
|
||||
|
||||
@@ -482,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
|
||||
*/
|
||||
@@ -494,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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -512,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()
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -537,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);
|
||||
});
|
||||
}
|
||||
|
||||
+77
-34
@@ -15,7 +15,6 @@ 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.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @module crypto/DeviceList
|
||||
@@ -24,12 +23,11 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
|
||||
import logger from '../logger';
|
||||
import DeviceInfo from './deviceinfo';
|
||||
import {logger} from '../logger';
|
||||
import {DeviceInfo} from './deviceinfo';
|
||||
import {CrossSigningInfo} from './CrossSigning';
|
||||
import olmlib from './olmlib';
|
||||
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
import * as olmlib from './olmlib';
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
import {defer, sleep} from '../utils';
|
||||
|
||||
|
||||
@@ -63,7 +61,7 @@ const TRACKING_STATUS_UP_TO_DATE = 3;
|
||||
/**
|
||||
* @alias module:crypto/DeviceList
|
||||
*/
|
||||
export default class DeviceList extends EventEmitter {
|
||||
export class DeviceList extends EventEmitter {
|
||||
constructor(baseApis, cryptoStore, olmDevice) {
|
||||
super();
|
||||
|
||||
@@ -111,6 +109,9 @@ export default 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,6 +121,7 @@ export default 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 || {} : {};
|
||||
@@ -199,16 +201,16 @@ export default 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({
|
||||
@@ -219,7 +221,13 @@ export default 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);
|
||||
}
|
||||
@@ -313,6 +321,15 @@ export default 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
|
||||
*
|
||||
@@ -375,6 +392,26 @@ export default 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
|
||||
*
|
||||
@@ -384,19 +421,11 @@ export default 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;
|
||||
@@ -596,7 +625,7 @@ export default class DeviceList extends EventEmitter {
|
||||
*
|
||||
* @param {String[]} users list of userIds
|
||||
*
|
||||
* @return {module:client.Promise} resolves when all the users listed have
|
||||
* @return {Promise} resolves when all the users listed have
|
||||
* been updated. rejects if there was a problem updating any of the
|
||||
* users.
|
||||
*/
|
||||
@@ -627,6 +656,7 @@ export default class DeviceList extends EventEmitter {
|
||||
});
|
||||
|
||||
const finished = (success) => {
|
||||
this.emit("crypto.willUpdateDevices", users, !this._hasFetched);
|
||||
users.forEach((u) => {
|
||||
this._dirty = true;
|
||||
|
||||
@@ -652,7 +682,8 @@ export default class DeviceList extends EventEmitter {
|
||||
}
|
||||
});
|
||||
this.saveIfDirty();
|
||||
this.emit("crypto.devicesUpdated", users);
|
||||
this.emit("crypto.devicesUpdated", users, !this._hasFetched);
|
||||
this._hasFetched = true;
|
||||
};
|
||||
|
||||
return prom;
|
||||
@@ -701,7 +732,7 @@ class DeviceListUpdateSerialiser {
|
||||
* @param {String} syncToken sync token to pass in the query request, to
|
||||
* help the HS give the most recent results
|
||||
*
|
||||
* @return {module:client.Promise} resolves when all the users listed have
|
||||
* @return {Promise} resolves when all the users listed have
|
||||
* been updated. rejects if there was a problem updating any of the
|
||||
* users.
|
||||
*/
|
||||
@@ -751,31 +782,34 @@ class DeviceListUpdateSerialiser {
|
||||
|
||||
this._baseApis.downloadKeysForUsers(
|
||||
downloadUsers, opts,
|
||||
).then((res) => {
|
||||
).then(async (res) => {
|
||||
const dk = res.device_keys || {};
|
||||
const masterKeys = res.master_keys || {};
|
||||
const ssks = res.self_signing_keys || {};
|
||||
const usks = res.user_signing_keys || {};
|
||||
|
||||
// do each user in a separate promise, to avoid wedging the CPU
|
||||
// (https://github.com/vector-im/riot-web/issues/3158)
|
||||
// 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);
|
||||
|
||||
@@ -796,7 +830,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);
|
||||
@@ -814,6 +848,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
|
||||
@@ -851,8 +886,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
|
||||
@@ -862,6 +898,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+308
-34
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2017, 2019 New Vector Ltd
|
||||
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.
|
||||
@@ -15,8 +16,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import logger from '../logger';
|
||||
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
import {logger} from '../logger';
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
import * as algorithms from './algorithms';
|
||||
|
||||
// The maximum size of an event is 65K, and we base64 the content, so this is a
|
||||
// reasonable approximation to the biggest plaintext we can encrypt.
|
||||
@@ -34,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +77,7 @@ function checkPayloadLength(payloadString) {
|
||||
* @property {string} deviceCurve25519Key Curve25519 key for the account
|
||||
* @property {string} deviceEd25519Key Ed25519 key for the account
|
||||
*/
|
||||
function OlmDevice(cryptoStore) {
|
||||
export function OlmDevice(cryptoStore) {
|
||||
this._cryptoStore = cryptoStore;
|
||||
this._pickleKey = "DEFAULT_KEY";
|
||||
|
||||
@@ -103,22 +111,61 @@ 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise the OlmAccount. This must be called before any other operations
|
||||
* on the OlmDevice.
|
||||
*
|
||||
* Data from an exported Olm device can be provided
|
||||
* in order to re-create this device.
|
||||
*
|
||||
* Attempts to load the OlmAccount from the crypto store, or creates one if none is
|
||||
* found.
|
||||
*
|
||||
* Reads the device keys from the OlmAccount object.
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {object} opts.fromExportedDevice (Optional) data from exported device
|
||||
* that must be re-created.
|
||||
* If present, opts.pickleKey is ignored
|
||||
* (exported data already provides a pickle key)
|
||||
* @param {object} opts.pickleKey (Optional) pickle key to set instead of default one
|
||||
*/
|
||||
OlmDevice.prototype.init = async function() {
|
||||
OlmDevice.prototype.init = async function(opts = {}) {
|
||||
let e2eKeys;
|
||||
const account = new global.Olm.Account();
|
||||
|
||||
const { pickleKey, fromExportedDevice } = opts;
|
||||
|
||||
try {
|
||||
await _initialiseAccount(this._cryptoStore, this._pickleKey, account);
|
||||
if (fromExportedDevice) {
|
||||
if (pickleKey) {
|
||||
logger.warn(
|
||||
'ignoring opts.pickleKey'
|
||||
+ ' because opts.fromExportedDevice is present.',
|
||||
);
|
||||
}
|
||||
this._pickleKey = fromExportedDevice.pickleKey;
|
||||
await _initialiseFromExportedDevice(
|
||||
fromExportedDevice,
|
||||
this._cryptoStore,
|
||||
this._pickleKey,
|
||||
account,
|
||||
);
|
||||
} else {
|
||||
if (pickleKey) {
|
||||
this._pickleKey = pickleKey;
|
||||
}
|
||||
await _initialiseAccount(
|
||||
this._cryptoStore,
|
||||
this._pickleKey,
|
||||
account,
|
||||
);
|
||||
}
|
||||
e2eKeys = JSON.parse(account.identity_keys());
|
||||
|
||||
this._maxOneTimeKeys = account.max_number_of_one_time_keys();
|
||||
@@ -130,18 +177,67 @@ OlmDevice.prototype.init = async function() {
|
||||
this.deviceEd25519Key = e2eKeys.ed25519;
|
||||
};
|
||||
|
||||
async function _initialiseAccount(cryptoStore, pickleKey, account) {
|
||||
await cryptoStore.doTxn('readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], (txn) => {
|
||||
cryptoStore.getAccount(txn, (pickledAccount) => {
|
||||
if (pickledAccount !== null) {
|
||||
account.unpickle(pickleKey, pickledAccount);
|
||||
} else {
|
||||
account.create();
|
||||
pickledAccount = account.pickle(pickleKey);
|
||||
cryptoStore.storeAccount(txn, pickledAccount);
|
||||
}
|
||||
});
|
||||
/**
|
||||
* Populates the crypto store using data that was exported from an existing device.
|
||||
* Note that for now only the “account” and “sessions” stores are populated;
|
||||
* Other stores will be as with a new device.
|
||||
*
|
||||
* @param {Object} exportedData Data exported from another device
|
||||
* through the “export” method.
|
||||
* @param {module:crypto/store/base~CryptoStore} cryptoStore storage for the crypto layer
|
||||
* @param {string} pickleKey the key that was used to pickle the exported data
|
||||
* @param {Olm.Account} account an olm account to initialize
|
||||
*/
|
||||
async function _initialiseFromExportedDevice(
|
||||
exportedData,
|
||||
cryptoStore,
|
||||
pickleKey,
|
||||
account,
|
||||
) {
|
||||
await cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[
|
||||
IndexedDBCryptoStore.STORE_ACCOUNT,
|
||||
IndexedDBCryptoStore.STORE_SESSIONS,
|
||||
],
|
||||
(txn) => {
|
||||
cryptoStore.storeAccount(txn, exportedData.pickledAccount);
|
||||
exportedData.sessions.forEach((session) => {
|
||||
const {
|
||||
deviceKey,
|
||||
sessionId,
|
||||
} = session;
|
||||
const sessionInfo = {
|
||||
session: session.session,
|
||||
lastReceivedMessageTs: session.lastReceivedMessageTs,
|
||||
};
|
||||
cryptoStore.storeEndToEndSession(
|
||||
deviceKey,
|
||||
sessionId,
|
||||
sessionInfo,
|
||||
txn,
|
||||
);
|
||||
});
|
||||
});
|
||||
account.unpickle(pickleKey, exportedData.pickledAccount);
|
||||
}
|
||||
|
||||
async function _initialiseAccount(cryptoStore, pickleKey, account) {
|
||||
await cryptoStore.doTxn(
|
||||
'readwrite',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
cryptoStore.getAccount(txn, (pickledAccount) => {
|
||||
if (pickledAccount !== null) {
|
||||
account.unpickle(pickleKey, pickledAccount);
|
||||
} else {
|
||||
account.create();
|
||||
pickledAccount = account.pickle(pickleKey);
|
||||
cryptoStore.storeAccount(txn, pickledAccount);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,6 +285,38 @@ OlmDevice.prototype._storeAccount = function(txn, account) {
|
||||
this._cryptoStore.storeAccount(txn, account.pickle(this._pickleKey));
|
||||
};
|
||||
|
||||
/**
|
||||
* Export data for re-creating the Olm device later.
|
||||
* TODO export data other than just account and (P2P) sessions.
|
||||
*
|
||||
* @return {Promise<object>} The exported data
|
||||
*/
|
||||
OlmDevice.prototype.export = async function() {
|
||||
const result = {
|
||||
pickleKey: this._pickleKey,
|
||||
};
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly',
|
||||
[
|
||||
IndexedDBCryptoStore.STORE_ACCOUNT,
|
||||
IndexedDBCryptoStore.STORE_SESSIONS,
|
||||
],
|
||||
(txn) => {
|
||||
this._cryptoStore.getAccount(txn, (pickledAccount) => {
|
||||
result.pickledAccount = pickledAccount;
|
||||
});
|
||||
result.sessions = [];
|
||||
// Note that the pickledSession object we get in the callback
|
||||
// is not exactly the same thing you get in method _getSession
|
||||
// see documentation of IndexedDBCryptoStore.getAllEndToEndSessions
|
||||
this._cryptoStore.getAllEndToEndSessions(txn, (pickledSession) => {
|
||||
result.sessions.push(pickledSession);
|
||||
});
|
||||
},
|
||||
);
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* extract an OlmSession from the session store and call the given function
|
||||
* The session is useable only within the callback passed to this
|
||||
@@ -349,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
|
||||
*
|
||||
@@ -671,6 +829,18 @@ OlmDevice.prototype.matchesSession = async function(
|
||||
return matches;
|
||||
};
|
||||
|
||||
OlmDevice.prototype.recordSessionProblem = async function(deviceKey, type, fixed) {
|
||||
await this._cryptoStore.storeEndToEndSessionProblem(deviceKey, type, fixed);
|
||||
};
|
||||
|
||||
OlmDevice.prototype.sessionMayHaveProblems = async function(deviceKey, timestamp) {
|
||||
return await this._cryptoStore.getEndToEndSessionProblem(deviceKey, timestamp);
|
||||
};
|
||||
|
||||
OlmDevice.prototype.filterOutNotifiedErrorDevices = async function(devices) {
|
||||
return await this._cryptoStore.filterOutNotifiedErrorDevices(devices);
|
||||
};
|
||||
|
||||
|
||||
// Outbound group session
|
||||
// ======================
|
||||
@@ -818,9 +988,9 @@ OlmDevice.prototype._getInboundGroupSession = function(
|
||||
roomId, senderKey, sessionId, txn, func,
|
||||
) {
|
||||
this._cryptoStore.getEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, txn, (sessionData) => {
|
||||
senderKey, sessionId, txn, (sessionData, withheld) => {
|
||||
if (sessionData === null) {
|
||||
func(null);
|
||||
func(null, null, withheld);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -834,7 +1004,7 @@ OlmDevice.prototype._getInboundGroupSession = function(
|
||||
}
|
||||
|
||||
this._unpickleInboundGroupSession(sessionData, (session) => {
|
||||
func(session, sessionData);
|
||||
func(session, sessionData, withheld);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -852,14 +1022,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], (txn) => {
|
||||
'readwrite', [
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
|
||||
], (txn) => {
|
||||
/* if we already have this session, consider updating it */
|
||||
this._getInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn,
|
||||
@@ -895,12 +1069,17 @@ 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,
|
||||
@@ -914,6 +1093,60 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Record in the data store why an inbound group session was withheld.
|
||||
*
|
||||
* @param {string} roomId room that the session belongs to
|
||||
* @param {string} senderKey base64-encoded curve25519 key of the sender
|
||||
* @param {string} sessionId session identifier
|
||||
* @param {string} code reason code
|
||||
* @param {string} reason human-readable version of `code`
|
||||
*/
|
||||
OlmDevice.prototype.addInboundGroupSessionWithheld = async function(
|
||||
roomId, senderKey, sessionId, code, reason,
|
||||
) {
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD],
|
||||
(txn) => {
|
||||
this._cryptoStore.storeEndToEndInboundGroupSessionWithheld(
|
||||
senderKey, sessionId,
|
||||
{
|
||||
room_id: roomId,
|
||||
code: code,
|
||||
reason: reason,
|
||||
},
|
||||
txn,
|
||||
);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const WITHHELD_MESSAGES = {
|
||||
"m.unverified": "The sender has disabled encrypting to unverified devices.",
|
||||
"m.blacklisted": "The sender has blocked you.",
|
||||
"m.unauthorised": "You are not authorised to read the message.",
|
||||
"m.no_olm": "Unable to establish a secure channel.",
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate the message to use for the exception when a session key is withheld.
|
||||
*
|
||||
* @param {object} withheld An object that describes why the key was withheld.
|
||||
*
|
||||
* @return {string} the message
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
function _calculateWithheldMessage(withheld) {
|
||||
if (withheld.code && withheld.code in WITHHELD_MESSAGES) {
|
||||
return WITHHELD_MESSAGES[withheld.code];
|
||||
} else if (withheld.reason) {
|
||||
return withheld.reason;
|
||||
} else {
|
||||
return "decryption key withheld";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt a received message with an inbound group session
|
||||
*
|
||||
@@ -934,16 +1167,49 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
roomId, senderKey, sessionId, body, eventId, timestamp,
|
||||
) {
|
||||
let result;
|
||||
// when the localstorage crypto store is used as an indexeddb backend,
|
||||
// exceptions thrown from within the inner function are not passed through
|
||||
// to the top level, so we store exceptions in a variable and raise them at
|
||||
// the end
|
||||
let error;
|
||||
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
|
||||
'readwrite', [
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
|
||||
], (txn) => {
|
||||
this._getInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn, (session, sessionData) => {
|
||||
roomId, senderKey, sessionId, txn, (session, sessionData, withheld) => {
|
||||
if (session === null) {
|
||||
if (withheld) {
|
||||
error = new algorithms.DecryptionError(
|
||||
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
|
||||
_calculateWithheldMessage(withheld),
|
||||
{
|
||||
session: senderKey + '|' + sessionId,
|
||||
},
|
||||
);
|
||||
}
|
||||
result = null;
|
||||
return;
|
||||
}
|
||||
const res = session.decrypt(body);
|
||||
let res;
|
||||
try {
|
||||
res = session.decrypt(body);
|
||||
} catch (e) {
|
||||
if (e && e.message === 'OLM.UNKNOWN_MESSAGE_INDEX' && withheld) {
|
||||
error = new algorithms.DecryptionError(
|
||||
"MEGOLM_UNKNOWN_INBOUND_SESSION_ID",
|
||||
_calculateWithheldMessage(withheld),
|
||||
{
|
||||
session: senderKey + '|' + sessionId,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
error = e;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let plaintext = res.plaintext;
|
||||
if (plaintext === undefined) {
|
||||
@@ -965,10 +1231,11 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
msgInfo.id !== eventId ||
|
||||
msgInfo.timestamp !== timestamp
|
||||
) {
|
||||
throw new Error(
|
||||
error = new Error(
|
||||
"Duplicate message index, possible replay attack: " +
|
||||
messageIndexKey,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._inboundGroupSessionMessageIndexes[messageIndexKey] = {
|
||||
@@ -988,12 +1255,16 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
forwardingCurve25519KeyChain: (
|
||||
sessionData.forwardingCurve25519KeyChain || []
|
||||
),
|
||||
untrusted: sessionData.untrusted,
|
||||
};
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -1002,14 +1273,17 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
*
|
||||
* @param {string} roomId room in which the message was received
|
||||
* @param {string} senderKey base64-encoded curve25519 key of the sender
|
||||
* @param {sring} sessionId session identifier
|
||||
* @param {string} sessionId session identifier
|
||||
*
|
||||
* @returns {Promise<boolean>} true if we have the keys to this session
|
||||
*/
|
||||
OlmDevice.prototype.hasInboundSessionKeys = async function(roomId, senderKey, sessionId) {
|
||||
let result;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
|
||||
'readonly', [
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
|
||||
], (txn) => {
|
||||
this._cryptoStore.getEndToEndInboundGroupSession(
|
||||
senderKey, sessionId, txn, (sessionData) => {
|
||||
if (sessionData === null) {
|
||||
@@ -1060,7 +1334,10 @@ OlmDevice.prototype.getInboundGroupSessionKey = async function(
|
||||
) {
|
||||
let result;
|
||||
await this._cryptoStore.doTxn(
|
||||
'readonly', [IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS], (txn) => {
|
||||
'readonly', [
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
|
||||
], (txn) => {
|
||||
this._getInboundGroupSession(
|
||||
roomId, senderKey, sessionId, txn, (session, sessionData) => {
|
||||
if (session === null) {
|
||||
@@ -1139,6 +1416,3 @@ OlmDevice.prototype.verifySignature = function(
|
||||
util.ed25519_verify(key, message, signature);
|
||||
});
|
||||
};
|
||||
|
||||
/** */
|
||||
module.exports = OlmDevice;
|
||||
|
||||
@@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import logger from '../logger';
|
||||
import utils from '../utils';
|
||||
import {logger} from '../logger';
|
||||
import * as utils from '../utils';
|
||||
|
||||
/**
|
||||
* Internal module. Management of outgoing room key requests.
|
||||
@@ -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,
|
||||
|
||||
@@ -75,7 +75,7 @@ const ROOM_KEY_REQUEST_STATES = {
|
||||
CANCELLATION_PENDING_AND_WILL_RESEND: 3,
|
||||
};
|
||||
|
||||
export default class OutgoingRoomKeyRequestManager {
|
||||
export class OutgoingRoomKeyRequestManager {
|
||||
constructor(baseApis, deviceId, cryptoStore) {
|
||||
this._baseApis = baseApis;
|
||||
this._deviceId = deviceId;
|
||||
@@ -97,10 +97,6 @@ export default 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 default 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 default 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 default 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 default 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 default 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 default 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 default class OutgoingRoomKeyRequestManager {
|
||||
}).catch((e) => {
|
||||
logger.error("Error sending room key request; will retry later.", e);
|
||||
this._sendOutgoingRoomKeyRequestsTimer = null;
|
||||
this._startTimer();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,12 +20,12 @@ limitations under the License.
|
||||
* Manages the list of encrypted rooms
|
||||
*/
|
||||
|
||||
import IndexedDBCryptoStore from './store/indexeddb-crypto-store';
|
||||
import {IndexedDBCryptoStore} from './store/indexeddb-crypto-store';
|
||||
|
||||
/**
|
||||
* @alias module:crypto/RoomList
|
||||
*/
|
||||
export default class RoomList {
|
||||
export class RoomList {
|
||||
constructor(cryptoStore) {
|
||||
this._cryptoStore = cryptoStore;
|
||||
|
||||
|
||||
+212
-215
@@ -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.
|
||||
@@ -15,37 +15,40 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
import logger from '../logger';
|
||||
import olmlib from './olmlib';
|
||||
import { randomString } from '../randomstring';
|
||||
import { pkVerify } from './olmlib';
|
||||
import {logger} from '../logger';
|
||||
import * as olmlib 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 default class SecretStorage extends EventEmitter {
|
||||
constructor(baseApis, cryptoCallbacks, crossSigningInfo) {
|
||||
export class SecretStorage extends EventEmitter {
|
||||
constructor(baseApis, cryptoCallbacks) {
|
||||
super();
|
||||
this._baseApis = baseApis;
|
||||
this._cryptoCallbacks = cryptoCallbacks;
|
||||
this._crossSigningInfo = crossSigningInfo;
|
||||
this._requests = {};
|
||||
this._incomingRequests = {};
|
||||
}
|
||||
|
||||
getDefaultKeyId() {
|
||||
const defaultKeyEvent = this._baseApis.getAccountData(
|
||||
async getDefaultKeyId() {
|
||||
const defaultKey = await this._baseApis.getAccountDataFromServer(
|
||||
'm.secret_storage.default_key',
|
||||
);
|
||||
if (!defaultKeyEvent) return null;
|
||||
return defaultKeyEvent.getContent().key;
|
||||
if (!defaultKey) return null;
|
||||
return defaultKey.key;
|
||||
}
|
||||
|
||||
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 default 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,80 +81,72 @@ export default 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}`);
|
||||
}
|
||||
|
||||
if (!keyId) {
|
||||
do {
|
||||
keyId = randomString(32);
|
||||
} while (this._baseApis.getAccountData(`m.secret_storage.key.${keyId}`));
|
||||
} while (
|
||||
await this._baseApis.getAccountDataFromServer(
|
||||
`m.secret_storage.key.${keyId}`,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await this._crossSigningInfo.signObject(keyData, 'master');
|
||||
|
||||
await this._baseApis.setAccountData(
|
||||
`m.secret_storage.key.${keyId}`, keyData,
|
||||
);
|
||||
|
||||
return keyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a given secret storage key with the cross-signing master key.
|
||||
*
|
||||
* @param {string} [keyId = default key's ID] The ID of the key to sign.
|
||||
* Defaults to the default key ID if not provided.
|
||||
*/
|
||||
async signKey(keyId = this.getDefaultKeyId()) {
|
||||
if (!keyId) {
|
||||
throw new Error("signKey requires a key ID");
|
||||
}
|
||||
|
||||
const keyInfoEvent = this._baseApis.getAccountData(
|
||||
`m.secret_storage.key.${keyId}`,
|
||||
);
|
||||
if (!keyInfoEvent) {
|
||||
throw new Error(`Key ${keyId} does not exist in account data`);
|
||||
}
|
||||
const keyInfo = keyInfoEvent.getContent();
|
||||
|
||||
await this._crossSigningInfo.signObject(keyInfo, 'master');
|
||||
await this._baseApis.setAccountData(
|
||||
`m.secret_storage.key.${keyId}`, keyInfo,
|
||||
);
|
||||
|
||||
return {
|
||||
keyId,
|
||||
keyInfo,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the key information for a given ID.
|
||||
*
|
||||
* @param {string} [keyId = default key's ID] The ID of the key to check
|
||||
* for. Defaults to the default key ID if not provided.
|
||||
* @returns {Array?} If the key was found, the return value is an array of
|
||||
* the form [keyId, keyInfo]. Otherwise, null is returned.
|
||||
*/
|
||||
async getKey(keyId) {
|
||||
if (!keyId) {
|
||||
keyId = await this.getDefaultKeyId();
|
||||
}
|
||||
if (!keyId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
return keyInfo ? [keyId, keyInfo] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,15 +156,34 @@ export default class SecretStorage extends EventEmitter {
|
||||
* for. Defaults to the default key ID if not provided.
|
||||
* @return {boolean} Whether we have the key.
|
||||
*/
|
||||
hasKey(keyId = this.getDefaultKeyId()) {
|
||||
if (!keyId) {
|
||||
return false;
|
||||
}
|
||||
async hasKey(keyId) {
|
||||
return !!(await this.getKey(keyId));
|
||||
}
|
||||
|
||||
const keyInfo = this._baseApis.getAccountData(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
return keyInfo && keyInfo.getContent();
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,7 +198,7 @@ export default class SecretStorage extends EventEmitter {
|
||||
const encrypted = {};
|
||||
|
||||
if (!keys) {
|
||||
const defaultKeyId = this.getDefaultKeyId();
|
||||
const defaultKeyId = await this.getDefaultKeyId();
|
||||
if (!defaultKeyId) {
|
||||
throw new Error("No keys specified and no default key present");
|
||||
}
|
||||
@@ -192,37 +211,21 @@ export default class SecretStorage extends EventEmitter {
|
||||
|
||||
for (const keyId of keys) {
|
||||
// get key information from key storage
|
||||
const keyInfo = this._baseApis.getAccountData(
|
||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
if (!keyInfo) {
|
||||
throw new Error("Unknown key: " + keyId);
|
||||
}
|
||||
const keyInfoContent = keyInfo.getContent();
|
||||
|
||||
// check signature of key info
|
||||
pkVerify(
|
||||
keyInfoContent,
|
||||
this._crossSigningInfo.getId('master'),
|
||||
this._crossSigningInfo.userId,
|
||||
);
|
||||
|
||||
// encrypt secret, based on the algorithm
|
||||
switch (keyInfoContent.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
{
|
||||
const encryption = new global.Olm.PkEncryption();
|
||||
try {
|
||||
encryption.set_recipient_key(keyInfoContent.pubkey);
|
||||
encrypted[keyId] = encryption.encrypt(secret);
|
||||
} finally {
|
||||
encryption.free();
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
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
|
||||
+ ": " + keyInfoContent.algorithm);
|
||||
+ ": " + keyInfo.algorithm);
|
||||
// do nothing if we don't understand the encryption algorithm
|
||||
}
|
||||
}
|
||||
@@ -232,24 +235,30 @@ export default class SecretStorage extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Temporary method to fix up existing accounts where secrets
|
||||
* are incorrectly stored without the 'encrypted' level
|
||||
*
|
||||
* @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
|
||||
* @param {object} secretInfo The account data object
|
||||
* @returns {object} The fixed object or null if no fix was performed
|
||||
*/
|
||||
storePassthrough(name, keyId) {
|
||||
return this._baseApis.setAccountData(name, {
|
||||
[keyId]: {
|
||||
passthrough: true,
|
||||
},
|
||||
});
|
||||
async _fixupStoredSecret(name, secretInfo) {
|
||||
// We assume the secret was only stored passthrough for 1
|
||||
// key - this was all the broken code supported.
|
||||
const keys = Object.keys(secretInfo);
|
||||
if (
|
||||
keys.length === 1 && keys[0] !== 'encrypted' &&
|
||||
secretInfo[keys[0]].passthrough
|
||||
) {
|
||||
const hasKey = await this.hasKey(keys[0]);
|
||||
if (hasKey) {
|
||||
logger.log("Fixing up passthrough secret: " + name);
|
||||
await this.storePassthrough(name, keys[0]);
|
||||
const newData = await this._baseApis.getAccountDataFromServer(name);
|
||||
return newData;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,58 +269,55 @@ export default class SecretStorage extends EventEmitter {
|
||||
* @return {string} the contents of the secret
|
||||
*/
|
||||
async get(name) {
|
||||
const secretInfo = this._baseApis.getAccountData(name);
|
||||
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
|
||||
if (!secretInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
const secretContent = secretInfo.getContent();
|
||||
|
||||
if (!secretContent.encrypted) {
|
||||
throw new Error("Content is not encrypted!");
|
||||
if (!secretInfo.encrypted) {
|
||||
// try to fix it up
|
||||
secretInfo = await this._fixupStoredSecret(name, secretInfo);
|
||||
if (!secretInfo || !secretInfo.encrypted) {
|
||||
throw new Error("Content is not encrypted!");
|
||||
}
|
||||
}
|
||||
|
||||
// get possible keys to decrypt
|
||||
const keys = {};
|
||||
for (const keyId of Object.keys(secretContent.encrypted)) {
|
||||
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
||||
// get key information from key storage
|
||||
const keyInfo = this._baseApis.getAccountData(
|
||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
).getContent();
|
||||
const encInfo = secretContent.encrypted[keyId];
|
||||
switch (keyInfo.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
|
||||
&& encInfo.ephemeral) {
|
||||
);
|
||||
const encInfo = secretInfo.encrypted[keyId];
|
||||
// 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 {
|
||||
// fetch private key from app
|
||||
[keyId, decryption] = await this._getSecretStorageKey(keys);
|
||||
[keyId, decryption] = await this._getSecretStorageKey(keys, name);
|
||||
|
||||
const encInfo = secretContent.encrypted[keyId];
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,53 +327,43 @@ export default 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
|
||||
*/
|
||||
isStored(name, checkKey) {
|
||||
async isStored(name, checkKey) {
|
||||
// check if secret exists
|
||||
const secretInfo = this._baseApis.getAccountData(name);
|
||||
if (!secretInfo) {
|
||||
return false;
|
||||
let secretInfo = await this._baseApis.getAccountDataFromServer(name);
|
||||
if (!secretInfo) return null;
|
||||
if (!secretInfo.encrypted) {
|
||||
// try to fix it up
|
||||
secretInfo = await this._fixupStoredSecret(name, secretInfo);
|
||||
if (!secretInfo || !secretInfo.encrypted) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (checkKey === undefined) checkKey = true;
|
||||
|
||||
const secretContent = secretInfo.getContent();
|
||||
const ret = {};
|
||||
|
||||
if (!secretContent.encrypted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// check if secret is encrypted by a known/trusted secret and
|
||||
// encryption looks sane
|
||||
for (const keyId of Object.keys(secretContent.encrypted)) {
|
||||
// filter secret encryption keys with supported algorithm
|
||||
for (const keyId of Object.keys(secretInfo.encrypted)) {
|
||||
// get key information from key storage
|
||||
const keyEvent = this._baseApis.getAccountData(
|
||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
||||
"m.secret_storage.key." + keyId,
|
||||
);
|
||||
if (!keyEvent) return false;
|
||||
const keyInfo = keyEvent.getContent();
|
||||
if (!keyInfo) return false;
|
||||
const encInfo = secretContent.encrypted[keyId];
|
||||
if (checkKey) {
|
||||
pkVerify(
|
||||
keyInfo,
|
||||
this._crossSigningInfo.getId('master'),
|
||||
this._crossSigningInfo.userId,
|
||||
);
|
||||
}
|
||||
switch (keyInfo.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
|
||||
&& encInfo.ephemeral) {
|
||||
return true;
|
||||
if (!keyInfo) continue;
|
||||
const encInfo = secretInfo.encrypted[keyId];
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,6 +378,7 @@ export default class SecretStorage extends EventEmitter {
|
||||
const requestId = this._baseApis.makeTxnId();
|
||||
|
||||
const requestControl = this._requests[requestId] = {
|
||||
name,
|
||||
devices,
|
||||
};
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
@@ -419,6 +416,7 @@ export default 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,
|
||||
});
|
||||
@@ -464,14 +462,15 @@ export default 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: {
|
||||
@@ -489,7 +488,7 @@ export default class SecretStorage extends EventEmitter {
|
||||
this._baseApis,
|
||||
{
|
||||
[sender]: [
|
||||
await this._baseApis.getStoredDevice(sender, deviceId),
|
||||
this._baseApis.getStoredDevice(sender, deviceId),
|
||||
],
|
||||
},
|
||||
);
|
||||
@@ -499,7 +498,7 @@ export default 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 = {
|
||||
@@ -508,7 +507,10 @@ export default 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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -520,7 +522,7 @@ export default 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
|
||||
@@ -540,16 +542,20 @@ export default class SecretStorage extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
logger.log(
|
||||
`Successfully received secret ${requestControl.name} ` +
|
||||
`from ${deviceInfo.deviceId}`,
|
||||
);
|
||||
requestControl.resolve(content.secret);
|
||||
}
|
||||
}
|
||||
|
||||
async _getSecretStorageKey(keys) {
|
||||
async _getSecretStorageKey(keys, name) {
|
||||
if (!this._cryptoCallbacks.getSecretStorageKey) {
|
||||
throw new Error("No getSecretStorageKey callback supplied");
|
||||
}
|
||||
|
||||
const returned = await this._cryptoCallbacks.getSecretStorageKey({ keys });
|
||||
const returned = await this._cryptoCallbacks.getSecretStorageKey({ keys }, name);
|
||||
|
||||
if (!returned) {
|
||||
throw new Error("getSecretStorageKey callback returned falsey");
|
||||
@@ -563,27 +569,18 @@ export default 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);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export const DECRYPTION_CLASSES = {};
|
||||
* @param {string} params.roomId The ID of the room we will be sending to
|
||||
* @param {object} params.config The body of the m.room.encryption event
|
||||
*/
|
||||
class EncryptionAlgorithm {
|
||||
export class EncryptionAlgorithm {
|
||||
constructor(params) {
|
||||
this._userId = params.userId;
|
||||
this._deviceId = params.deviceId;
|
||||
@@ -60,6 +60,15 @@ 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
|
||||
*
|
||||
@@ -70,7 +79,7 @@ class EncryptionAlgorithm {
|
||||
* @param {string} eventType
|
||||
* @param {object} plaintext event content
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the new event body
|
||||
* @return {Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -84,7 +93,6 @@ class EncryptionAlgorithm {
|
||||
onRoomMembership(event, member, oldMembership) {
|
||||
}
|
||||
}
|
||||
export {EncryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
|
||||
|
||||
/**
|
||||
* base type for decryption implementations
|
||||
@@ -98,7 +106,7 @@ export {EncryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
|
||||
* @param {string=} params.roomId The ID of the room we will be receiving
|
||||
* from. Null for to-device events.
|
||||
*/
|
||||
class DecryptionAlgorithm {
|
||||
export class DecryptionAlgorithm {
|
||||
constructor(params) {
|
||||
this._userId = params.userId;
|
||||
this._crypto = params.crypto;
|
||||
@@ -159,8 +167,17 @@ class DecryptionAlgorithm {
|
||||
shareKeysWithDevice(keyRequest) {
|
||||
throw new Error("shareKeysWithDevice not supported for this DecryptionAlgorithm");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry decrypting all the events from a sender that haven't been
|
||||
* decrypted yet.
|
||||
*
|
||||
* @param {string} senderKey the sender's key
|
||||
*/
|
||||
async retryDecryptionFromSender(senderKey) {
|
||||
// ignore by default
|
||||
}
|
||||
}
|
||||
export {DecryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
|
||||
|
||||
/**
|
||||
* Exception thrown when decryption fails
|
||||
@@ -173,7 +190,7 @@ export {DecryptionAlgorithm}; // https://github.com/jsdoc3/jsdoc/issues/1272
|
||||
*
|
||||
* @extends Error
|
||||
*/
|
||||
class DecryptionError extends Error {
|
||||
export class DecryptionError extends Error {
|
||||
constructor(code, msg, details) {
|
||||
super(msg);
|
||||
this.code = code;
|
||||
@@ -181,7 +198,6 @@ class DecryptionError extends Error {
|
||||
this.detailedString = _detailedStringForDecryptionError(this, details);
|
||||
}
|
||||
}
|
||||
export {DecryptionError}; // https://github.com/jsdoc3/jsdoc/issues/1272
|
||||
|
||||
function _detailedStringForDecryptionError(err, details) {
|
||||
let result = err.name + '[msg: ' + err.message;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -13,28 +14,12 @@ 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.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @module crypto/algorithms
|
||||
*/
|
||||
|
||||
const base = require("./base");
|
||||
import "./olm";
|
||||
import "./megolm";
|
||||
|
||||
require("./olm");
|
||||
require("./megolm");
|
||||
|
||||
/**
|
||||
* @see module:crypto/algorithms/base.ENCRYPTION_CLASSES
|
||||
*/
|
||||
module.exports.ENCRYPTION_CLASSES = base.ENCRYPTION_CLASSES;
|
||||
|
||||
/**
|
||||
* @see module:crypto/algorithms/base.DECRYPTION_CLASSES
|
||||
*/
|
||||
module.exports.DECRYPTION_CLASSES = base.DECRYPTION_CLASSES;
|
||||
|
||||
/**
|
||||
* @see module:crypto/algorithms/base.DecryptionError
|
||||
*/
|
||||
module.exports.DecryptionError = base.DecryptionError;
|
||||
export * from "./base";
|
||||
|
||||
+697
-181
File diff suppressed because it is too large
Load Diff
@@ -13,42 +13,48 @@ 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.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Defines m.olm encryption/decryption
|
||||
*
|
||||
* @module crypto/algorithms/olm
|
||||
*/
|
||||
import logger from '../../logger';
|
||||
const utils = require("../../utils");
|
||||
const olmlib = require("../olmlib");
|
||||
const DeviceInfo = require("../deviceinfo");
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
const base = require("./base");
|
||||
import {logger} from '../../logger';
|
||||
import * as utils from "../../utils";
|
||||
import {polyfillSuper} from "../../utils";
|
||||
import * as olmlib from "../olmlib";
|
||||
import {DeviceInfo} from "../deviceinfo";
|
||||
import {
|
||||
DecryptionAlgorithm,
|
||||
DecryptionError,
|
||||
EncryptionAlgorithm,
|
||||
registerAlgorithm,
|
||||
} from "./base";
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
/**
|
||||
* Olm encryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto/algorithms/base.EncryptionAlgorithm}
|
||||
* @extends {module:crypto/algorithms/EncryptionAlgorithm}
|
||||
*
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto/algorithms/base.EncryptionAlgorithm}
|
||||
* {@link module:crypto/algorithms/EncryptionAlgorithm}
|
||||
*/
|
||||
function OlmEncryption(params) {
|
||||
base.EncryptionAlgorithm.call(this, params);
|
||||
polyfillSuper(this, EncryptionAlgorithm, params);
|
||||
this._sessionPrepared = false;
|
||||
this._prepPromise = null;
|
||||
}
|
||||
utils.inherits(OlmEncryption, base.EncryptionAlgorithm);
|
||||
utils.inherits(OlmEncryption, EncryptionAlgorithm);
|
||||
|
||||
/**
|
||||
* @private
|
||||
|
||||
* @param {string[]} roomMembers list of currently-joined users in the room
|
||||
* @return {module:client.Promise} Promise which resolves when setup is complete
|
||||
* @return {Promise} Promise which resolves when setup is complete
|
||||
*/
|
||||
OlmEncryption.prototype._ensureSession = function(roomMembers) {
|
||||
if (this._prepPromise) {
|
||||
@@ -79,7 +85,7 @@ OlmEncryption.prototype._ensureSession = function(roomMembers) {
|
||||
* @param {string} eventType
|
||||
* @param {object} content plaintext event content
|
||||
*
|
||||
* @return {module:client.Promise} Promise which resolves to the new event body
|
||||
* @return {Promise} Promise which resolves to the new event body
|
||||
*/
|
||||
OlmEncryption.prototype.encryptMessage = async function(room, eventType, content) {
|
||||
// pick the list of recipients based on the membership list.
|
||||
@@ -143,14 +149,14 @@ OlmEncryption.prototype.encryptMessage = async function(room, eventType, content
|
||||
* Olm decryption implementation
|
||||
*
|
||||
* @constructor
|
||||
* @extends {module:crypto/algorithms/base.DecryptionAlgorithm}
|
||||
* @extends {module:crypto/algorithms/DecryptionAlgorithm}
|
||||
* @param {object} params parameters, as per
|
||||
* {@link module:crypto/algorithms/base.DecryptionAlgorithm}
|
||||
* {@link module:crypto/algorithms/DecryptionAlgorithm}
|
||||
*/
|
||||
function OlmDecryption(params) {
|
||||
base.DecryptionAlgorithm.call(this, params);
|
||||
polyfillSuper(this, DecryptionAlgorithm, params);
|
||||
}
|
||||
utils.inherits(OlmDecryption, base.DecryptionAlgorithm);
|
||||
utils.inherits(OlmDecryption, DecryptionAlgorithm);
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
@@ -168,14 +174,14 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
|
||||
const ciphertext = content.ciphertext;
|
||||
|
||||
if (!ciphertext) {
|
||||
throw new base.DecryptionError(
|
||||
throw new DecryptionError(
|
||||
"OLM_MISSING_CIPHERTEXT",
|
||||
"Missing ciphertext",
|
||||
);
|
||||
}
|
||||
|
||||
if (!(this._olmDevice.deviceCurve25519Key in ciphertext)) {
|
||||
throw new base.DecryptionError(
|
||||
throw new DecryptionError(
|
||||
"OLM_NOT_INCLUDED_IN_RECIPIENTS",
|
||||
"Not included in recipients",
|
||||
);
|
||||
@@ -186,7 +192,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
|
||||
try {
|
||||
payloadString = await this._decryptMessage(deviceKey, message);
|
||||
} catch (e) {
|
||||
throw new base.DecryptionError(
|
||||
throw new DecryptionError(
|
||||
"OLM_BAD_ENCRYPTED_MESSAGE",
|
||||
"Bad Encrypted Message", {
|
||||
sender: deviceKey,
|
||||
@@ -200,14 +206,14 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
|
||||
// check that we were the intended recipient, to avoid unknown-key attack
|
||||
// https://github.com/vector-im/vector-web/issues/2483
|
||||
if (payload.recipient != this._userId) {
|
||||
throw new base.DecryptionError(
|
||||
throw new DecryptionError(
|
||||
"OLM_BAD_RECIPIENT",
|
||||
"Message was intented for " + payload.recipient,
|
||||
);
|
||||
}
|
||||
|
||||
if (payload.recipient_keys.ed25519 != this._olmDevice.deviceEd25519Key) {
|
||||
throw new base.DecryptionError(
|
||||
throw new DecryptionError(
|
||||
"OLM_BAD_RECIPIENT_KEY",
|
||||
"Message not intended for this device", {
|
||||
intended: payload.recipient_keys.ed25519,
|
||||
@@ -221,7 +227,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
|
||||
// (this check is also provided via the sender's embedded ed25519 key,
|
||||
// which is checked elsewhere).
|
||||
if (payload.sender != event.getSender()) {
|
||||
throw new base.DecryptionError(
|
||||
throw new DecryptionError(
|
||||
"OLM_FORWARDED_MESSAGE",
|
||||
"Message forwarded from " + payload.sender, {
|
||||
reported_sender: event.getSender(),
|
||||
@@ -231,7 +237,7 @@ OlmDecryption.prototype.decryptEvent = async function(event) {
|
||||
|
||||
// Olm events intended for a room have a room_id.
|
||||
if (payload.room_id !== event.getRoomId()) {
|
||||
throw new base.DecryptionError(
|
||||
throw new DecryptionError(
|
||||
"OLM_BAD_ROOM",
|
||||
"Message intended for room " + payload.room_id, {
|
||||
reported_room: event.room_id,
|
||||
@@ -258,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,
|
||||
@@ -334,4 +359,4 @@ OlmDecryption.prototype._decryptMessage = async function(
|
||||
};
|
||||
|
||||
|
||||
base.registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
|
||||
registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
/*
|
||||
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",
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
async setDehydrationKey(
|
||||
key: Uint8Array, keyInfo: {[props: string]: any} = {},
|
||||
deviceDisplayName: string = undefined,
|
||||
): Promise<void> {
|
||||
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 deviceDisplayNamme 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;
|
||||
// start dehydration in the background
|
||||
this.dehydrateDevice();
|
||||
}
|
||||
}
|
||||
private async dehydrateDevice(): Promise<void> {
|
||||
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,
|
||||
);
|
||||
} finally {
|
||||
this.inProgress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2016 OpenMarket Ltd
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -13,8 +14,6 @@ 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.
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
|
||||
/**
|
||||
* @module crypto/deviceinfo
|
||||
@@ -44,7 +43,7 @@ limitations under the License.
|
||||
*
|
||||
* @param {string} deviceId id of the device
|
||||
*/
|
||||
function DeviceInfo(deviceId) {
|
||||
export function DeviceInfo(deviceId) {
|
||||
// you can't change the deviceId
|
||||
Object.defineProperty(this, 'deviceId', {
|
||||
enumerable: true,
|
||||
@@ -167,5 +166,3 @@ DeviceInfo.DeviceVerification = {
|
||||
|
||||
const DeviceVerification = DeviceInfo.DeviceVerification;
|
||||
|
||||
/** */
|
||||
module.exports = DeviceInfo;
|
||||
|
||||
+1394
-438
File diff suppressed because it is too large
Load Diff
@@ -15,10 +15,12 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { randomString } from '../randomstring';
|
||||
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);
|
||||
|
||||
+137
-39
@@ -22,25 +22,24 @@ limitations under the License.
|
||||
* Utilities common to olm encryption algorithms
|
||||
*/
|
||||
|
||||
const anotherjson = require('another-json');
|
||||
|
||||
import logger from '../logger';
|
||||
const utils = require("../utils");
|
||||
import {logger} from '../logger';
|
||||
import * as utils from "../utils";
|
||||
import anotherjson from "another-json";
|
||||
|
||||
/**
|
||||
* matrix algorithm tag for olm
|
||||
*/
|
||||
module.exports.OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
|
||||
export const OLM_ALGORITHM = "m.olm.v1.curve25519-aes-sha2";
|
||||
|
||||
/**
|
||||
* matrix algorithm tag for megolm
|
||||
*/
|
||||
module.exports.MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
|
||||
export const MEGOLM_ALGORITHM = "m.megolm.v1.aes-sha2";
|
||||
|
||||
/**
|
||||
* matrix algorithm tag for megolm backups
|
||||
*/
|
||||
module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2";
|
||||
export const MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2";
|
||||
|
||||
|
||||
/**
|
||||
@@ -59,7 +58,7 @@ module.exports.MEGOLM_BACKUP_ALGORITHM = "m.megolm_backup.v1.curve25519-aes-sha2
|
||||
* Returns a promise which resolves (to undefined) when the payload
|
||||
* has been encrypted into `resultsObject`
|
||||
*/
|
||||
module.exports.encryptMessageForDevice = async function(
|
||||
export async function encryptMessageForDevice(
|
||||
resultsObject,
|
||||
ourUserId, ourDeviceId, olmDevice, recipientUserId, recipientDevice,
|
||||
payloadFields,
|
||||
@@ -112,7 +111,58 @@ module.exports.encryptMessageForDevice = async function(
|
||||
resultsObject[deviceKey] = await olmDevice.encryptMessage(
|
||||
deviceKey, sessionId, JSON.stringify(payload),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
@@ -124,32 +174,58 @@ module.exports.encryptMessageForDevice = async function(
|
||||
* @param {object<string, module:crypto/deviceinfo[]>} devicesByUser
|
||||
* map from userid to list of devices to ensure sessions for
|
||||
*
|
||||
* @param {bolean} 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.
|
||||
*
|
||||
* @return {module:client.Promise} resolves once the sessions are complete, to
|
||||
* @param {Number} [otkTimeout] The timeout in milliseconds when requesting
|
||||
* one-time keys for establishing new olm sessions.
|
||||
*
|
||||
* @param {Array} [failedServers] An array to fill with remote servers that
|
||||
* failed to respond to one-time-key requests.
|
||||
*
|
||||
* @return {Promise} resolves once the sessions are complete, to
|
||||
* an Object mapping from userId to deviceId to
|
||||
* {@link module:crypto~OlmSessionResult}
|
||||
*/
|
||||
module.exports.ensureOlmSessionsForDevices = async function(
|
||||
olmDevice, baseApis, devicesByUser, force,
|
||||
export async function ensureOlmSessionsForDevices(
|
||||
olmDevice, baseApis, devicesByUser, force, otkTimeout, failedServers,
|
||||
) {
|
||||
if (typeof force === "number") {
|
||||
failedServers = otkTimeout;
|
||||
otkTimeout = force;
|
||||
force = false;
|
||||
}
|
||||
|
||||
const devicesWithoutSession = [
|
||||
// [userId, deviceId], ...
|
||||
];
|
||||
const result = {};
|
||||
const resolveSession = {};
|
||||
|
||||
for (const userId in devicesByUser) {
|
||||
if (!devicesByUser.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
result[userId] = {};
|
||||
const devices = devicesByUser[userId];
|
||||
for (let j = 0; j < devices.length; j++) {
|
||||
const deviceInfo = devices[j];
|
||||
for (const deviceInfo of devices) {
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
const key = deviceInfo.getIdentityKey();
|
||||
|
||||
if (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.
|
||||
logger.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;
|
||||
}
|
||||
|
||||
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
|
||||
@@ -181,6 +257,11 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
||||
delete resolveSession[key];
|
||||
}
|
||||
if (sessionId === null || force) {
|
||||
if (force) {
|
||||
logger.info("Forcing new Olm session for " + userId + ":" + deviceId);
|
||||
} else {
|
||||
logger.info("Making new Olm session for " + userId + ":" + deviceId);
|
||||
}
|
||||
devicesWithoutSession.push([userId, deviceId]);
|
||||
}
|
||||
result[userId][deviceId] = {
|
||||
@@ -198,7 +279,7 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
||||
let res;
|
||||
try {
|
||||
res = await baseApis.claimOneTimeKeys(
|
||||
devicesWithoutSession, oneTimeKeyAlgorithm,
|
||||
devicesWithoutSession, oneTimeKeyAlgorithm, otkTimeout,
|
||||
);
|
||||
} catch (e) {
|
||||
for (const resolver of Object.values(resolveSession)) {
|
||||
@@ -208,18 +289,26 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (failedServers && "failures" in res) {
|
||||
failedServers.push(...Object.keys(res.failures));
|
||||
}
|
||||
|
||||
const otk_res = res.one_time_keys || {};
|
||||
const promises = [];
|
||||
for (const userId in devicesByUser) {
|
||||
if (!devicesByUser.hasOwnProperty(userId)) {
|
||||
continue;
|
||||
}
|
||||
for (const [userId, devices] of Object.entries(devicesByUser)) {
|
||||
const userRes = otk_res[userId] || {};
|
||||
const devices = devicesByUser[userId];
|
||||
for (let j = 0; j < devices.length; j++) {
|
||||
const deviceInfo = devices[j];
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
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;
|
||||
@@ -263,12 +352,12 @@ module.exports.ensureOlmSessionsForDevices = async function(
|
||||
|
||||
await Promise.all(promises);
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceInfo) {
|
||||
const deviceId = deviceInfo.deviceId;
|
||||
try {
|
||||
await _verifySignature(
|
||||
await verifySignature(
|
||||
olmDevice, oneTimeKey, userId, deviceId,
|
||||
deviceInfo.getFingerprint(),
|
||||
);
|
||||
@@ -314,7 +403,7 @@ async function _verifyKeyAndStartSession(olmDevice, oneTimeKey, userId, deviceIn
|
||||
* Returns a promise which resolves (to undefined) if the the signature is good,
|
||||
* or rejects with an Error if it is bad.
|
||||
*/
|
||||
const _verifySignature = module.exports.verifySignature = async function(
|
||||
export async function verifySignature(
|
||||
olmDevice, obj, signingUserId, signingDeviceId, signingKey,
|
||||
) {
|
||||
const signKeyId = "ed25519:" + signingDeviceId;
|
||||
@@ -335,7 +424,7 @@ const _verifySignature = module.exports.verifySignature = async function(
|
||||
olmDevice.verifySignature(
|
||||
signingKey, json, signature,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a JSON object using public key cryptography
|
||||
@@ -347,7 +436,7 @@ const _verifySignature = module.exports.verifySignature = async function(
|
||||
* @param {string} pubkey The public key (ignored if key is a seed)
|
||||
* @returns {string} the signature for the object
|
||||
*/
|
||||
module.exports.pkSign = function(obj, key, userId, pubkey) {
|
||||
export function pkSign(obj, key, userId, pubkey) {
|
||||
let createdKey = false;
|
||||
if (key instanceof Uint8Array) {
|
||||
const keyObj = new global.Olm.PkSigning();
|
||||
@@ -371,7 +460,7 @@ module.exports.pkSign = function(obj, key, userId, pubkey) {
|
||||
key.free();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a signed JSON object
|
||||
@@ -379,7 +468,7 @@ module.exports.pkSign = function(obj, key, userId, pubkey) {
|
||||
* @param {string} pubkey The public key to use to verify
|
||||
* @param {string} userId The user ID who signed the object
|
||||
*/
|
||||
module.exports.pkVerify = function(obj, pubkey, userId) {
|
||||
export function pkVerify(obj, pubkey, userId) {
|
||||
const keyId = "ed25519:" + pubkey;
|
||||
if (!(obj.signatures && obj.signatures[userId] && obj.signatures[userId][keyId])) {
|
||||
throw new Error("No signature");
|
||||
@@ -397,22 +486,31 @@ module.exports.pkVerify = function(obj, pubkey, userId) {
|
||||
if (unsigned) obj.unsigned = unsigned;
|
||||
util.free();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode a typed array of uint8 as base64.
|
||||
* @param {Uint8Array} uint8Array The data to encode.
|
||||
* @return {string} The base64.
|
||||
*/
|
||||
module.exports.encodeBase64 = function(uint8Array) {
|
||||
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.
|
||||
* @return {Uint8Array} The decoded data.
|
||||
*/
|
||||
module.exports.decodeBase64 = function(base64) {
|
||||
export function decodeBase64(base64) {
|
||||
return Buffer.from(base64, "base64");
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
Copyright 2017 Vector Creations Ltd
|
||||
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.
|
||||
@@ -15,10 +16,10 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import logger from '../../logger';
|
||||
import utils from '../../utils';
|
||||
import {logger} from '../../logger';
|
||||
import * as utils from "../../utils";
|
||||
|
||||
export const VERSION = 7;
|
||||
export const VERSION = 9;
|
||||
|
||||
/**
|
||||
* Implementation of a CryptoStore which is backed by an existing
|
||||
@@ -79,7 +80,7 @@ export class Backend {
|
||||
`enqueueing key request for ${requestBody.room_id} / ` +
|
||||
requestBody.session_id,
|
||||
);
|
||||
txn.oncomplete = () => { resolve(request); };
|
||||
txn.oncomplete = () => {resolve(request);};
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
store.add(request);
|
||||
});
|
||||
@@ -130,7 +131,7 @@ export class Backend {
|
||||
|
||||
cursorReq.onsuccess = (ev) => {
|
||||
const cursor = ev.target.result;
|
||||
if(!cursor) {
|
||||
if (!cursor) {
|
||||
// no match found
|
||||
callback(null);
|
||||
return;
|
||||
@@ -202,6 +203,23 @@ export class Backend {
|
||||
return promiseifyTxn(txn).then(() => result);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Number} wantedState
|
||||
* @return {Promise<Array<*>>} All elements in a given state
|
||||
*/
|
||||
getAllOutgoingRoomKeyRequestsByState(wantedState) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const txn = this._db.transaction("outgoingRoomKeyRequests", "readonly");
|
||||
const store = txn.objectStore("outgoingRoomKeyRequests");
|
||||
const index = store.index("state");
|
||||
const request = index.getAll(wantedState);
|
||||
|
||||
request.onsuccess = (ev) => resolve(ev.target.result);
|
||||
request.onerror = (ev) => reject(ev.target.error);
|
||||
});
|
||||
}
|
||||
|
||||
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
|
||||
let stateIndex = 0;
|
||||
const results = [];
|
||||
@@ -340,18 +358,39 @@ export class Backend {
|
||||
};
|
||||
}
|
||||
|
||||
getSecretStorePrivateKey(txn, func, type) {
|
||||
const objectStore = txn.objectStore("account");
|
||||
const getReq = objectStore.get(`ssss_cache:${type}`);
|
||||
getReq.onsuccess = function() {
|
||||
try {
|
||||
func(getReq.result || null);
|
||||
} catch (e) {
|
||||
abortWithException(txn, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
storeCrossSigningKeys(txn, keys) {
|
||||
const objectStore = txn.objectStore("account");
|
||||
objectStore.put(keys, "crossSigningKeys");
|
||||
}
|
||||
|
||||
storeSecretStorePrivateKey(txn, type, key) {
|
||||
const objectStore = txn.objectStore("account");
|
||||
objectStore.put(key, `ssss_cache:${type}`);
|
||||
}
|
||||
|
||||
// Olm Sessions
|
||||
|
||||
countEndToEndSessions(txn, func) {
|
||||
const objectStore = txn.objectStore("sessions");
|
||||
const countReq = objectStore.count();
|
||||
countReq.onsuccess = function() {
|
||||
func(countReq.result);
|
||||
try {
|
||||
func(countReq.result);
|
||||
} catch (e) {
|
||||
abortWithException(txn, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -401,16 +440,16 @@ export class Backend {
|
||||
const objectStore = txn.objectStore("sessions");
|
||||
const getReq = objectStore.openCursor();
|
||||
getReq.onsuccess = function() {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
func(cursor.value);
|
||||
cursor.continue();
|
||||
} else {
|
||||
try {
|
||||
try {
|
||||
const cursor = getReq.result;
|
||||
if (cursor) {
|
||||
func(cursor.value);
|
||||
cursor.continue();
|
||||
} else {
|
||||
func(null);
|
||||
} catch (e) {
|
||||
abortWithException(txn, e);
|
||||
}
|
||||
} catch (e) {
|
||||
abortWithException(txn, e);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -425,17 +464,107 @@ export class Backend {
|
||||
});
|
||||
}
|
||||
|
||||
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
|
||||
const txn = this._db.transaction("session_problems", "readwrite");
|
||||
const objectStore = txn.objectStore("session_problems");
|
||||
objectStore.put({
|
||||
deviceKey,
|
||||
type,
|
||||
fixed,
|
||||
time: Date.now(),
|
||||
});
|
||||
return promiseifyTxn(txn);
|
||||
}
|
||||
|
||||
async getEndToEndSessionProblem(deviceKey, timestamp) {
|
||||
let result;
|
||||
const txn = this._db.transaction("session_problems", "readwrite");
|
||||
const objectStore = txn.objectStore("session_problems");
|
||||
const index = objectStore.index("deviceKey");
|
||||
const req = index.getAll(deviceKey);
|
||||
req.onsuccess = (event) => {
|
||||
const problems = req.result;
|
||||
if (!problems.length) {
|
||||
result = null;
|
||||
return;
|
||||
}
|
||||
problems.sort((a, b) => {
|
||||
return a.time - b.time;
|
||||
});
|
||||
const lastProblem = problems[problems.length - 1];
|
||||
for (const problem of problems) {
|
||||
if (problem.time > timestamp) {
|
||||
result = Object.assign({}, problem, {fixed: lastProblem.fixed});
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (lastProblem.fixed) {
|
||||
result = null;
|
||||
} else {
|
||||
result = lastProblem;
|
||||
}
|
||||
};
|
||||
await promiseifyTxn(txn);
|
||||
return result;
|
||||
}
|
||||
|
||||
// FIXME: we should probably prune this when devices get deleted
|
||||
async filterOutNotifiedErrorDevices(devices) {
|
||||
const txn = this._db.transaction("notified_error_devices", "readwrite");
|
||||
const objectStore = txn.objectStore("notified_error_devices");
|
||||
|
||||
const ret = [];
|
||||
|
||||
await Promise.all(devices.map((device) => {
|
||||
return new Promise((resolve) => {
|
||||
const {userId, deviceInfo} = device;
|
||||
const getReq = objectStore.get([userId, deviceInfo.deviceId]);
|
||||
getReq.onsuccess = function() {
|
||||
if (!getReq.result) {
|
||||
objectStore.put({userId, deviceId: deviceInfo.deviceId});
|
||||
ret.push(device);
|
||||
}
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Inbound group sessions
|
||||
|
||||
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
|
||||
let session = false;
|
||||
let withheld = false;
|
||||
const objectStore = txn.objectStore("inbound_group_sessions");
|
||||
const getReq = objectStore.get([senderCurve25519Key, sessionId]);
|
||||
getReq.onsuccess = function() {
|
||||
try {
|
||||
if (getReq.result) {
|
||||
func(getReq.result.session);
|
||||
session = getReq.result.session;
|
||||
} else {
|
||||
func(null);
|
||||
session = null;
|
||||
}
|
||||
if (withheld !== false) {
|
||||
func(session, withheld);
|
||||
}
|
||||
} catch (e) {
|
||||
abortWithException(txn, e);
|
||||
}
|
||||
};
|
||||
|
||||
const withheldObjectStore = txn.objectStore("inbound_group_sessions_withheld");
|
||||
const withheldGetReq = withheldObjectStore.get([senderCurve25519Key, sessionId]);
|
||||
withheldGetReq.onsuccess = function() {
|
||||
try {
|
||||
if (withheldGetReq.result) {
|
||||
withheld = withheldGetReq.result.session;
|
||||
} else {
|
||||
withheld = null;
|
||||
}
|
||||
if (session !== false) {
|
||||
func(session, withheld);
|
||||
}
|
||||
} catch (e) {
|
||||
abortWithException(txn, e);
|
||||
@@ -499,6 +628,15 @@ export class Backend {
|
||||
});
|
||||
}
|
||||
|
||||
storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
) {
|
||||
const objectStore = txn.objectStore("inbound_group_sessions_withheld");
|
||||
objectStore.put({
|
||||
senderCurve25519Key, sessionId, session: sessionData,
|
||||
});
|
||||
}
|
||||
|
||||
getEndToEndDeviceData(txn, func) {
|
||||
const objectStore = txn.objectStore("device_data");
|
||||
const getReq = objectStore.get("-");
|
||||
@@ -662,6 +800,21 @@ export function upgradeDatabase(db, oldVersion) {
|
||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||
});
|
||||
}
|
||||
if (oldVersion < 8) {
|
||||
db.createObjectStore("inbound_group_sessions_withheld", {
|
||||
keyPath: ["senderCurve25519Key", "sessionId"],
|
||||
});
|
||||
}
|
||||
if (oldVersion < 9) {
|
||||
const problemsStore = db.createObjectStore("session_problems", {
|
||||
keyPath: ["deviceKey", "time"],
|
||||
});
|
||||
problemsStore.createIndex("deviceKey", "deviceKey");
|
||||
|
||||
db.createObjectStore("notified_error_devices", {
|
||||
keyPath: ["userId", "deviceId"],
|
||||
});
|
||||
}
|
||||
// Expand as needed.
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user