Compare commits
646 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 195f3a7550 | |||
| 1c9343ed8f | |||
| 5c7d9981f8 | |||
| 63bc17a6b4 | |||
| 629490c4ae | |||
| d59d62f96a | |||
| 8a460c2368 | |||
| 6f60183316 | |||
| a21c62519b | |||
| 1b8f6d1739 | |||
| e087bce61a | |||
| d2f24c3e87 | |||
| 5d606bba66 | |||
| 864fe459b7 | |||
| 8992438aa6 | |||
| 197d5ebb44 | |||
| 4039498eee | |||
| a686231a5b | |||
| 97cf4bff1f | |||
| 11727833a2 | |||
| df38fde336 | |||
| 00233d610b | |||
| d1c9030fac | |||
| 70071eef41 | |||
| 65dd56f53a | |||
| c8fb4af369 | |||
| 9e1ba992e3 | |||
| bad09fed44 | |||
| 5bd146bb85 | |||
| 75703f273f | |||
| ca7b49d209 | |||
| 30bce8a682 | |||
| f413f0ee5f | |||
| be6778a931 | |||
| 84637c6ebd | |||
| 2ed6e9ba2f | |||
| 2e3cb54d38 | |||
| 0efac7eae0 | |||
| dc56f05d03 | |||
| 4d2625278c | |||
| bff8a947a1 | |||
| 9cb7406ebd | |||
| 38681ca6ca | |||
| 916696c906 | |||
| 1b3c4c935e | |||
| 66417e6742 | |||
| 6ad4d6dd62 | |||
| 59607f8dbf | |||
| 62380a48d5 | |||
| 4245372495 | |||
| 964a448470 | |||
| 2151f28d4c | |||
| 060549656e | |||
| 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 | |||
| 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
|
||||
+400
@@ -1,3 +1,403 @@
|
||||
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)
|
||||
|
||||
+10
-2
@@ -36,8 +36,16 @@ 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://yarnpkg.com/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"
|
||||
};
|
||||
@@ -191,10 +200,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 +256,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 +360,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");
|
||||
|
||||
@@ -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();
|
||||
+38
-40
@@ -1,21 +1,24 @@
|
||||
{
|
||||
"name": "matrix-js-sdk",
|
||||
"version": "3.0.0",
|
||||
"version": "5.3.1-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:ts && yarn lint:js",
|
||||
"lint:js": "eslint --max-warnings 81 src spec",
|
||||
"lint:types": "tsc --noEmit",
|
||||
"lint:ts": "tslint --project ./tsconfig.json -t stylish",
|
||||
"test": "jest spec/ --coverage --testEnvironment node",
|
||||
"test:watch": "jest spec/ --coverage --testEnvironment node --watch"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -24,30 +27,26 @@
|
||||
"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",
|
||||
"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.8.3",
|
||||
"another-json": "^0.2.0",
|
||||
"browser-request": "^0.3.3",
|
||||
"bs58": "^4.0.1",
|
||||
@@ -64,35 +63,34 @@
|
||||
"@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/plugin-transform-runtime": "^7.8.3",
|
||||
"@babel/preset-env": "^7.7.6",
|
||||
"@babel/preset-typescript": "^7.7.4",
|
||||
"@babel/register": "^7.7.4",
|
||||
"@babel/runtime": "^7.7.6",
|
||||
"@types/node": "12",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-jest": "^24.9.0",
|
||||
"babelify": "^10.0.0",
|
||||
"better-docs": "^1.4.7",
|
||||
"browserify": "^16.2.3",
|
||||
"browserify-shim": "^3.8.13",
|
||||
"browserify": "^16.5.0",
|
||||
"eslint": "^5.12.0",
|
||||
"eslint-config-google": "^0.7.1",
|
||||
"eslint-plugin-babel": "^5.3.0",
|
||||
"eslint-plugin-jest": "^23.0.4",
|
||||
"exorcist": "^1.0.1",
|
||||
"fake-indexeddb": "^3.0.0",
|
||||
"jest": "^24.9.0",
|
||||
"jest-localstorage-mock": "^2.4.0",
|
||||
"jsdoc": "^3.5.5",
|
||||
"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"
|
||||
"terser": "^4.4.3",
|
||||
"tsify": "^4.0.1",
|
||||
"tslint": "^5.20.1",
|
||||
"typescript": "^3.7.3"
|
||||
},
|
||||
"browserify": {
|
||||
"transform": [
|
||||
"sourceify"
|
||||
]
|
||||
"jest": {
|
||||
"testEnvironment": "node"
|
||||
}
|
||||
}
|
||||
|
||||
+32
-12
@@ -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 riot-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"
|
||||
;;
|
||||
@@ -94,10 +101,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
|
||||
@@ -294,7 +303,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"
|
||||
@@ -329,8 +347,10 @@ 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
|
||||
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;
|
||||
|
||||
+13
-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,7 +65,7 @@ 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 = {};
|
||||
@@ -97,7 +97,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 +185,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,7 +225,7 @@ 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`);
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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,8 @@
|
||||
"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";
|
||||
|
||||
describe("MatrixClient opts", function() {
|
||||
const baseUrl = "http://localhost.or.something";
|
||||
@@ -71,7 +70,7 @@ describe("MatrixClient opts", function() {
|
||||
baseUrl: baseUrl,
|
||||
userId: userId,
|
||||
accessToken: accessToken,
|
||||
scheduler: new sdk.MatrixScheduler(),
|
||||
scheduler: new MatrixScheduler(),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,7 +123,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,
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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'),
|
||||
|
||||
+2
-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,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import logger from '../lib/logger';
|
||||
import {logger} from '../src/logger';
|
||||
|
||||
// try to load the olm library.
|
||||
try {
|
||||
|
||||
+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, getIdenticonUri} 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,7 +51,7 @@ 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",
|
||||
);
|
||||
});
|
||||
@@ -61,25 +59,25 @@ describe("ContentRepo", function() {
|
||||
|
||||
describe("getIdenticonUri", function() {
|
||||
it("should do nothing for null input", function() {
|
||||
expect(ContentRepo.getIdenticonUri(null)).toEqual(null);
|
||||
expect(getIdenticonUri(null)).toEqual(null);
|
||||
});
|
||||
|
||||
it("should set w/h by default to 96", function() {
|
||||
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar")).toEqual(
|
||||
expect(getIdenticonUri(baseUrl, "foobar")).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
|
||||
"?width=96&height=96",
|
||||
);
|
||||
});
|
||||
|
||||
it("should be able to set custom w/h", function() {
|
||||
expect(ContentRepo.getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
|
||||
expect(getIdenticonUri(baseUrl, "foobar", 32, 64)).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foobar" +
|
||||
"?width=32&height=64",
|
||||
);
|
||||
});
|
||||
|
||||
it("should URL encode the identicon string", function() {
|
||||
expect(ContentRepo.getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
|
||||
expect(getIdenticonUri(baseUrl, "foo#bar", 32, 64)).toEqual(
|
||||
baseUrl + "/_matrix/media/unstable/identicon/foo%23bar" +
|
||||
"?width=32&height=64",
|
||||
);
|
||||
|
||||
+11
-17
@@ -1,26 +1,20 @@
|
||||
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";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("Crypto", function() {
|
||||
if (!sdk.CRYPTO_ENABLED) {
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
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';
|
||||
|
||||
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: false },
|
||||
{ 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) {
|
||||
console.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, ab] = await info.getCrossSigningKey("master", masterKeyPub);
|
||||
expect(pubKey).toEqual(masterKeyPub);
|
||||
expect(ab).toEqual({a: 106712, b: 106712});
|
||||
});
|
||||
|
||||
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 { getCrossSigningKeyCache, storeCrossSigningKeyCache } =
|
||||
createCryptoStoreCacheCallbacks(store);
|
||||
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(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": {},
|
||||
|
||||
@@ -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,20 @@ 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";
|
||||
|
||||
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';
|
||||
@@ -543,5 +541,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(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,13 +16,9 @@ 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';
|
||||
|
||||
async function makeTestClient(userInfo, options, keys) {
|
||||
@@ -178,7 +174,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 +237,7 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
path: "/keys/upload",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
@@ -433,7 +429,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 +487,7 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/upload/Osborne2",
|
||||
path: "/keys/upload",
|
||||
data: {
|
||||
one_time_key_counts: {
|
||||
curve25519: 100,
|
||||
|
||||
@@ -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,25 +15,48 @@ 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 { 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) {
|
||||
console.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');
|
||||
@@ -45,9 +68,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 +85,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 +116,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 +127,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 +164,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 +178,7 @@ describe("Secrets", function() {
|
||||
cryptoCallbacks: {
|
||||
getCrossSigningKey: t => keys[t],
|
||||
saveCrossSigningKeys: k => keys = k,
|
||||
getSecretStorageKey: getKey,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -164,7 +193,7 @@ describe("Secrets", function() {
|
||||
alice.resetCrossSigningKeys();
|
||||
|
||||
const newKeyId = await alice.addSecretStorageKey(
|
||||
SECRET_STORAGE_ALGORITHM_V1,
|
||||
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
|
||||
@@ -245,89 +274,708 @@ 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 backupKey = new Uint8Array(
|
||||
olmlib.decodeBase64(
|
||||
"OTmOKrPsWP1qxmt0s2qat4vYxULG+U9cmFAWFco3A5U=",
|
||||
),
|
||||
);
|
||||
const SSSSKey = new Uint8Array(
|
||||
olmlib.decodeBase64(
|
||||
"XrmITOOdBhw6yY5Bh7trb/bgp1FRdIGyCUxxMP873R0=",
|
||||
),
|
||||
);
|
||||
const SSSSPubKey = "v3A8HTypbccUm6jaRCRw5l+7lSdzQUACeY9xpQ5BVmE";
|
||||
|
||||
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,
|
||||
});
|
||||
this.store.storeAccountDataEvents([
|
||||
event,
|
||||
]);
|
||||
this.emit("accountData", event);
|
||||
};
|
||||
bob._crypto.checkKeyBackup = async () => {};
|
||||
await bob.bootstrapSecretStorage();
|
||||
|
||||
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.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.bootstrapSecretStorage();
|
||||
|
||||
expect(crossSigning.getId()).toBeTruthy();
|
||||
expect(await crossSigning.isStoredInSecretStorage(secretStorage))
|
||||
.toBeTruthy();
|
||||
expect(await secretStorage.hasKey()).toBeTruthy();
|
||||
});
|
||||
|
||||
it("converts asymmetric SSSS to symmetric SSSS", async function() {
|
||||
let crossSigningKeys = {};
|
||||
const secretStorageKeys = {
|
||||
"old_key_id": SSSSKey,
|
||||
"new_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]];
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const encryption = new global.Olm.PkEncryption();
|
||||
encryption.set_recipient_key(SSSSPubKey);
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: "m.secret_storage.default_key",
|
||||
content: {
|
||||
key: "old_key_id",
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.secret_storage.key.old_key_id",
|
||||
content: sign({
|
||||
algorithm: "m.secret_storage.v1.curve25519-aes-sha2",
|
||||
passphrase: {
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: 500000,
|
||||
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
|
||||
},
|
||||
pubkey: SSSSPubKey,
|
||||
}, XSK, "@alice:example.com"),
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.cross_signing.master",
|
||||
content: {
|
||||
encrypted: {
|
||||
old_key_id: encryption.encrypt(olmlib.encodeBase64(XSK)),
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.cross_signing.self_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
old_key_id: encryption.encrypt(olmlib.encodeBase64(SSK)),
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.cross_signing.user_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
old_key_id: encryption.encrypt(olmlib.encodeBase64(USK)),
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.megolm_backup.v1",
|
||||
content: {
|
||||
encrypted: {
|
||||
old_key_id: encryption.encrypt(
|
||||
olmlib.encodeBase64(backupKey),
|
||||
),
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
encryption.free();
|
||||
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"),
|
||||
};
|
||||
};
|
||||
const origAddSecretStorageKey = alice._crypto.addSecretStorageKey;
|
||||
alice._crypto.addSecretStorageKey = async function(algorithm, opts, keyId) {
|
||||
return await origAddSecretStorageKey.call(
|
||||
alice._crypto, algorithm, opts, keyId || "new_key_id",
|
||||
);
|
||||
};
|
||||
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();
|
||||
|
||||
// it should create a new key with the same parameters
|
||||
expect(alice.getAccountData("m.secret_storage.default_key").getContent())
|
||||
.toEqual({key: "new_key_id"});
|
||||
const newKeyInfo = alice.getAccountData("m.secret_storage.key.new_key_id")
|
||||
.getContent();
|
||||
expect(newKeyInfo.algorithm)
|
||||
.toEqual("m.secret_storage.v1.aes-hmac-sha2");
|
||||
expect(newKeyInfo.passphrase).toEqual({
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: 500000,
|
||||
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
|
||||
});
|
||||
expect(newKeyInfo).toHaveProperty("iv");
|
||||
expect(newKeyInfo).toHaveProperty("mac");
|
||||
expect(alice.checkSecretStorageKey(secretStorageKeys.new_key_id, newKeyInfo))
|
||||
.toBeTruthy();
|
||||
|
||||
const crossSigningMaster = alice.getAccountData("m.cross_signing.master")
|
||||
.getContent();
|
||||
expect(crossSigningMaster.encrypted).toHaveProperty("new_key_id");
|
||||
expect(crossSigningMaster.encrypted).not.toHaveProperty("old_key_id");
|
||||
expect(await alice.getSecret("m.cross_signing.master"))
|
||||
.toEqual(olmlib.encodeBase64(XSK));
|
||||
|
||||
const USKInfo = alice.getAccountData("m.cross_signing.self_signing")
|
||||
.getContent();
|
||||
expect(USKInfo.encrypted).toHaveProperty("new_key_id");
|
||||
expect(USKInfo.encrypted).not.toHaveProperty("old_key_id");
|
||||
expect(await alice.getSecret("m.cross_signing.user_signing"))
|
||||
.toEqual(olmlib.encodeBase64(USK));
|
||||
|
||||
const SSKInfo = alice.getAccountData("m.cross_signing.self_signing")
|
||||
.getContent();
|
||||
expect(SSKInfo.encrypted).toHaveProperty("new_key_id");
|
||||
expect(SSKInfo.encrypted).not.toHaveProperty("old_key_id");
|
||||
expect(await alice.getSecret("m.cross_signing.self_signing"))
|
||||
.toEqual(olmlib.encodeBase64(SSK));
|
||||
|
||||
const backupKeyInfo = alice.getAccountData("m.megolm_backup.v1")
|
||||
.getContent();
|
||||
expect(backupKeyInfo.encrypted).toHaveProperty("new_key_id");
|
||||
expect(backupKeyInfo.encrypted).not.toHaveProperty("old_key_id");
|
||||
expect(await alice.getSecret("m.megolm_backup.v1"))
|
||||
.toEqual(olmlib.encodeBase64(backupKey));
|
||||
});
|
||||
it("converts asymmetric SSSS with passthrough", async function() {
|
||||
let crossSigningKeys = {};
|
||||
const secretStorageKeys = {
|
||||
"old_key_id": SSSSKey,
|
||||
"new_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]];
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
const encryption = new global.Olm.PkEncryption();
|
||||
encryption.set_recipient_key(SSSSPubKey);
|
||||
alice.store.storeAccountDataEvents([
|
||||
new MatrixEvent({
|
||||
type: "m.secret_storage.default_key",
|
||||
content: {
|
||||
key: "old_key_id",
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.secret_storage.key.old_key_id",
|
||||
content: sign({
|
||||
algorithm: "m.secret_storage.v1.curve25519-aes-sha2",
|
||||
passphrase: {
|
||||
algorithm: "m.pbkdf2",
|
||||
iterations: 500000,
|
||||
salt: "GbkvwKHVMveo1zGVSb2GMMdCinG2npJK",
|
||||
},
|
||||
pubkey: "v3A8HTypbccUm6jaRCRw5l+7lSdzQUACeY9xpQ5BVmE",
|
||||
}, XSK, "@alice:example.com"),
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.cross_signing.master",
|
||||
content: {
|
||||
encrypted: {
|
||||
old_key_id: encryption.encrypt(olmlib.encodeBase64(XSK)),
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.cross_signing.self_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
old_key_id: encryption.encrypt(olmlib.encodeBase64(SSK)),
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.cross_signing.user_signing",
|
||||
content: {
|
||||
encrypted: {
|
||||
old_key_id: encryption.encrypt(olmlib.encodeBase64(USK)),
|
||||
},
|
||||
},
|
||||
}),
|
||||
new MatrixEvent({
|
||||
type: "m.megolm_backup.v1",
|
||||
content: {
|
||||
encrypted: {
|
||||
old_key_id: {
|
||||
passthrough: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
encryption.free();
|
||||
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: "v3A8HTypbccUm6jaRCRw5l+7lSdzQUACeY9xpQ5BVmE",
|
||||
}, XSK, "@alice:example.com"),
|
||||
};
|
||||
};
|
||||
const origAddSecretStorageKey = alice._crypto.addSecretStorageKey;
|
||||
alice._crypto.addSecretStorageKey = async function(algorithm, opts, keyId) {
|
||||
return await origAddSecretStorageKey.call(
|
||||
alice._crypto, algorithm, opts, keyId || "new_key_id",
|
||||
);
|
||||
};
|
||||
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();
|
||||
|
||||
// the new backup key should be the encoded SSSS key
|
||||
const backupKeyInfo = alice.getAccountData("m.megolm_backup.v1")
|
||||
.getContent();
|
||||
expect(backupKeyInfo.encrypted).toHaveProperty("new_key_id");
|
||||
expect(backupKeyInfo.encrypted).not.toHaveProperty("old_key_id");
|
||||
expect(await alice.getSecret("m.megolm_backup.v1"))
|
||||
.toEqual(olmlib.encodeBase64(SSSSKey));
|
||||
});
|
||||
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,17 @@ 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";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
const MatrixEvent = sdk.MatrixEvent;
|
||||
|
||||
import {makeTestClients} from './util';
|
||||
|
||||
let ALICE_DEVICES;
|
||||
let BOB_DEVICES;
|
||||
|
||||
@@ -46,11 +35,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 +131,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 +148,7 @@ describe("SAS verification", function() {
|
||||
}
|
||||
}
|
||||
});
|
||||
resolve(verifier);
|
||||
resolve(request.verifier);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -345,11 +348,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 +456,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,117 @@
|
||||
/*
|
||||
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 = {
|
||||
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, two cross signing key and the key backup key */
|
||||
expect(cacheCallbacks.storeCrossSigningKeyCache.mock.calls.length).toBe(2);
|
||||
expect(_secretStorage.request.mock.calls.length).toBe(3);
|
||||
|
||||
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,10 +15,9 @@ 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";
|
||||
|
||||
export async function makeTestClients(userInfos, options) {
|
||||
const clients = [];
|
||||
@@ -34,15 +34,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 +49,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
|
||||
console.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 +103,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,249 @@
|
||||
/*
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
+12
-21
@@ -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() {
|
||||
@@ -621,15 +617,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 +857,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 +867,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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,7 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
import 'source-map-support/register';
|
||||
import sdk from "../..";
|
||||
|
||||
const SyncAccumulator = sdk.SyncAccumulator;
|
||||
import {SyncAccumulator} from "../../src/sync-accumulator";
|
||||
|
||||
describe("SyncAccumulator", function() {
|
||||
let sa;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
+197
-281
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;
|
||||
+433
-198
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,
|
||||
};
|
||||
}
|
||||
|
||||
+84
-85
@@ -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,93 @@ 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;
|
||||
},
|
||||
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,
|
||||
};
|
||||
/**
|
||||
* Get an identicon URL from an arbitrary string.
|
||||
* @param {string} baseUrl The base homeserver url which has a content repo.
|
||||
* @param {string} identiconString The string to create an identicon for.
|
||||
* @param {Number} width The desired width of the image in pixels. Default: 96.
|
||||
* @param {Number} height The desired height of the image in pixels. Default: 96.
|
||||
* @return {string} The complete URL to the identicon.
|
||||
* @deprecated This is no longer in the specification.
|
||||
*/
|
||||
export function getIdenticonUri(baseUrl, identiconString, width, height) {
|
||||
if (!identiconString) {
|
||||
return null;
|
||||
}
|
||||
if (!width) {
|
||||
width = 96;
|
||||
}
|
||||
if (!height) {
|
||||
height = 96;
|
||||
}
|
||||
const params = {
|
||||
width: width,
|
||||
height: height,
|
||||
};
|
||||
|
||||
const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
|
||||
$ident: identiconString,
|
||||
});
|
||||
return baseUrl + path +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params)));
|
||||
},
|
||||
};
|
||||
const path = utils.encodeUri("/_matrix/media/unstable/identicon/$ident", {
|
||||
$ident: identiconString,
|
||||
});
|
||||
return baseUrl + path +
|
||||
(utils.keys(params).length === 0 ? "" :
|
||||
("?" + utils.encodeParams(params)));
|
||||
}
|
||||
|
||||
+160
-31
@@ -20,9 +20,10 @@ 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';
|
||||
|
||||
function publicKeyFromKeyInfo(keyInfo) {
|
||||
// `keys` is an object with { [`ed25519:${pubKey}`]: pubKey }
|
||||
@@ -40,8 +41,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,8 +52,15 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
value: userId,
|
||||
});
|
||||
this._callbacks = callbacks || {};
|
||||
this._cacheCallbacks = cacheCallbacks || {};
|
||||
this.keys = {};
|
||||
this.firstUse = true;
|
||||
// This tracks whether we've ever verified this user with any identity.
|
||||
// When you verify a user, any devices online at the time that receive
|
||||
// the verifying signature via the homeserver will latch this to true
|
||||
// and can use it in the future to detect cases where the user has
|
||||
// become unverifed later for any reason.
|
||||
this.crossSigningVerifiedBefore = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -62,6 +71,8 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
* @returns {Array} An array with [ public key, Olm.PkSigning ]
|
||||
*/
|
||||
async getCrossSigningKey(type, expectedPubkey) {
|
||||
const shouldCache = ["self_signing", "user_signing"].indexOf(type) >= 0;
|
||||
|
||||
if (!this._callbacks.getCrossSigningKey) {
|
||||
throw new Error("No getCrossSigningKey callback supplied");
|
||||
}
|
||||
@@ -70,22 +81,47 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
expectedPubkey = this.getId(type);
|
||||
}
|
||||
|
||||
const privkey = await this._callbacks.getCrossSigningKey(type, expectedPubkey);
|
||||
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",
|
||||
);
|
||||
}
|
||||
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];
|
||||
}
|
||||
|
||||
/* 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",
|
||||
);
|
||||
}
|
||||
|
||||
static fromStorage(obj, userId) {
|
||||
@@ -102,6 +138,7 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
return {
|
||||
keys: this.keys,
|
||||
firstUse: this.firstUse,
|
||||
crossSigningVerifiedBefore: this.crossSigningVerifiedBefore,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,14 +148,28 @@ export class CrossSigningInfo extends EventEmitter {
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,6 +310,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 +389,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 +414,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 +427,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 +455,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 +473,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 +486,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 +511,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 +549,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 +569,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 +589,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 +609,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 +636,33 @@ export class DeviceTrustLevel {
|
||||
return this._tofu;
|
||||
}
|
||||
}
|
||||
|
||||
export function createCryptoStoreCacheCallbacks(store) {
|
||||
return {
|
||||
getCrossSigningKeyCache: function(type, _expectedPublicKey) {
|
||||
return new Promise((resolve) => {
|
||||
return store.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
store.getSecretStorePrivateKey(txn, resolve, type);
|
||||
},
|
||||
);
|
||||
});
|
||||
},
|
||||
storeCrossSigningKeyCache: function(type, key) {
|
||||
if (!(key instanceof Uint8Array)) {
|
||||
throw new Error(
|
||||
`storeCrossSigningKeyCache expects Uint8Array, got ${key}`,
|
||||
);
|
||||
}
|
||||
return store.doTxn(
|
||||
'readwrite',
|
||||
[IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
store.storeSecretStorePrivateKey(txn, type, key);
|
||||
},
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+58
-30
@@ -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();
|
||||
|
||||
@@ -199,16 +197,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 +217,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 +317,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 +388,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 +417,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 +621,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.
|
||||
*/
|
||||
@@ -701,7 +726,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 +776,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
|
||||
// yield to other things that want to execute in between users, to
|
||||
// avoid wedging the CPU
|
||||
// (https://github.com/vector-im/riot-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 +824,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);
|
||||
|
||||
+258
-30
@@ -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.
|
||||
@@ -69,7 +71,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";
|
||||
|
||||
@@ -109,16 +111,52 @@ function OlmDevice(cryptoStore) {
|
||||
* 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) {
|
||||
console.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 +168,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 +276,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
|
||||
@@ -671,6 +790,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 +949,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 +965,7 @@ OlmDevice.prototype._getInboundGroupSession = function(
|
||||
}
|
||||
|
||||
this._unpickleInboundGroupSession(sessionData, (session) => {
|
||||
func(session, sessionData);
|
||||
func(session, sessionData, withheld);
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -859,7 +990,10 @@ OlmDevice.prototype.addInboundGroupSession = async function(
|
||||
exportFormat,
|
||||
) {
|
||||
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,
|
||||
@@ -914,6 +1048,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 +1122,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 +1186,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] = {
|
||||
@@ -994,6 +1216,9 @@ OlmDevice.prototype.decryptGroupMessage = async function(
|
||||
},
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -1002,14 +1227,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 +1288,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 +1370,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;
|
||||
@@ -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 `sendRoomKeyRequest` outputs.
|
||||
*/
|
||||
async cancelAndResendAllOutgoingRequests() {
|
||||
const outgoings = await this._cryptoStore.getAllOutgoingRoomKeyRequestsByState(
|
||||
ROOM_KEY_REQUEST_STATES.SENT,
|
||||
);
|
||||
return Promise.all(outgoings.map(({ requestBody, recipients }) =>
|
||||
this.sendRoomKeyRequest(requestBody, recipients, true)));
|
||||
}
|
||||
|
||||
// start the background timer to send queued requests, if the timer isn't
|
||||
// already running
|
||||
_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;
|
||||
|
||||
|
||||
+273
-145
@@ -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,18 +15,26 @@ 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 {pkVerify} from './olmlib';
|
||||
import {randomString} from '../randomstring';
|
||||
import {encryptAES, decryptAES} from './aes';
|
||||
import {encodeBase64} from "./olmlib";
|
||||
|
||||
export const SECRET_STORAGE_ALGORITHM_V1 = "m.secret_storage.v1.curve25519-aes-sha2";
|
||||
export const SECRET_STORAGE_ALGORITHM_V1_AES
|
||||
= "m.secret_storage.v1.aes-hmac-sha2";
|
||||
// don't use curve25519 for writing data.
|
||||
export const SECRET_STORAGE_ALGORITHM_V1_CURVE25519
|
||||
= "m.secret_storage.v1.curve25519-aes-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 {
|
||||
export class SecretStorage extends EventEmitter {
|
||||
constructor(baseApis, cryptoCallbacks, crossSigningInfo) {
|
||||
super();
|
||||
this._baseApis = baseApis;
|
||||
@@ -36,12 +44,12 @@ export default class SecretStorage extends EventEmitter {
|
||||
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) {
|
||||
@@ -85,23 +93,15 @@ export default class SecretStorage extends EventEmitter {
|
||||
}
|
||||
|
||||
switch (algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
||||
{
|
||||
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 (opts.passphrase) {
|
||||
keyData.passphrase = opts.passphrase;
|
||||
}
|
||||
if (opts.key) {
|
||||
const {iv, mac} = await SecretStorage._calculateKeyCheck(opts.key);
|
||||
keyData.iv = iv;
|
||||
keyData.mac = mac;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -112,7 +112,11 @@ export default class SecretStorage extends EventEmitter {
|
||||
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');
|
||||
@@ -130,18 +134,20 @@ export default class SecretStorage extends EventEmitter {
|
||||
* @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()) {
|
||||
async signKey(keyId) {
|
||||
if (!keyId) {
|
||||
keyId = await this.getDefaultKeyId();
|
||||
}
|
||||
if (!keyId) {
|
||||
throw new Error("signKey requires a key ID");
|
||||
}
|
||||
|
||||
const keyInfoEvent = this._baseApis.getAccountData(
|
||||
const keyInfo = await this._baseApis.getAccountDataFromServer(
|
||||
`m.secret_storage.key.${keyId}`,
|
||||
);
|
||||
if (!keyInfoEvent) {
|
||||
if (!keyInfo) {
|
||||
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(
|
||||
@@ -149,6 +155,28 @@ export default class SecretStorage extends EventEmitter {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether we have a key with a given ID.
|
||||
*
|
||||
@@ -156,15 +184,60 @@ 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) {
|
||||
async hasKey(keyId) {
|
||||
return !!(await this.getKey(keyId));
|
||||
}
|
||||
|
||||
async keyNeedsUpgrade(keyId) {
|
||||
const keyInfo = await this.getKey(keyId);
|
||||
if (keyInfo && keyInfo[1].algorithm === SECRET_STORAGE_ALGORITHM_V1_CURVE25519) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
switch (info.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
||||
{
|
||||
if (info.mac) {
|
||||
const {mac} = await SecretStorage._calculateKeyCheck(key, info.iv);
|
||||
return info.mac === mac;
|
||||
} else {
|
||||
// if we have no information, we have to assume the key is right
|
||||
return true;
|
||||
}
|
||||
}
|
||||
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
|
||||
{
|
||||
let decryption = null;
|
||||
try {
|
||||
decryption = new global.Olm.PkDecryption();
|
||||
const gotPubkey = decryption.init_with_private_key(key);
|
||||
// make sure it agrees with the given pubkey
|
||||
return gotPubkey === info.pubkey;
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
if (decryption) decryption.free();
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw new Error("Unknown algorithm");
|
||||
}
|
||||
}
|
||||
|
||||
static async _calculateKeyCheck(key, iv) {
|
||||
return await encryptAES(ZERO_STR, key, "", iv);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,7 +252,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 +265,25 @@ 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:
|
||||
switch (keyInfo.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
||||
{
|
||||
const encryption = new global.Olm.PkEncryption();
|
||||
try {
|
||||
encryption.set_recipient_key(keyInfoContent.pubkey);
|
||||
encrypted[keyId] = encryption.encrypt(secret);
|
||||
} finally {
|
||||
encryption.free();
|
||||
}
|
||||
const keys = {[keyId]: keyInfo};
|
||||
const [, encryption] = await this._getSecretStorageKey(keys, name);
|
||||
encrypted[keyId] = await encryption.encrypt(secret);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
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 +293,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) {
|
||||
console.log("Fixing up passthrough secret: " + name);
|
||||
await this.storePassthrough(name, keys[0]);
|
||||
const newData = await this._baseApis.getAccountDataFromServer(name);
|
||||
return newData;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -260,29 +327,39 @@ 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];
|
||||
);
|
||||
const encInfo = secretInfo.encrypted[keyId];
|
||||
switch (keyInfo.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
|
||||
&& encInfo.ephemeral) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
||||
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
|
||||
keys[keyId] = keyInfo;
|
||||
}
|
||||
break;
|
||||
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
|
||||
if (
|
||||
keyInfo.pubkey && (
|
||||
(encInfo.ciphertext && encInfo.mac && encInfo.ephemeral) ||
|
||||
encInfo.passthrough
|
||||
)
|
||||
) {
|
||||
keys[keyId] = keyInfo;
|
||||
}
|
||||
break;
|
||||
@@ -295,23 +372,18 @@ export default class SecretStorage extends EventEmitter {
|
||||
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 +393,82 @@ 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();
|
||||
|
||||
if (!secretContent.encrypted) {
|
||||
return false;
|
||||
}
|
||||
const ret = {};
|
||||
|
||||
// check if secret is encrypted by a known/trusted secret and
|
||||
// encryption looks sane
|
||||
for (const keyId of Object.keys(secretContent.encrypted)) {
|
||||
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,
|
||||
);
|
||||
if (!keyInfo) continue;
|
||||
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) {
|
||||
try {
|
||||
pkVerify(
|
||||
keyInfo,
|
||||
this._crossSigningInfo.getId('master'),
|
||||
this._crossSigningInfo.userId,
|
||||
);
|
||||
} catch (e) {
|
||||
// not trusted, so move on to the next key
|
||||
continue;
|
||||
}
|
||||
ret[keyId] = keyInfo;
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (keyInfo.algorithm) {
|
||||
case SECRET_STORAGE_ALGORITHM_V1:
|
||||
case SECRET_STORAGE_ALGORITHM_V1_AES:
|
||||
if (encInfo.iv && encInfo.ciphertext && encInfo.mac) {
|
||||
ret[keyId] = keyInfo;
|
||||
}
|
||||
break;
|
||||
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
|
||||
if (keyInfo.pubkey && encInfo.ciphertext && encInfo.mac
|
||||
&& encInfo.ephemeral) {
|
||||
return true;
|
||||
if (checkKey) {
|
||||
try {
|
||||
pkVerify(
|
||||
keyInfo,
|
||||
this._crossSigningInfo.getId('master'),
|
||||
this._crossSigningInfo.userId,
|
||||
);
|
||||
} catch (e) {
|
||||
// not trusted, so move on to the next key
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ret[keyId] = keyInfo;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// do nothing if we don't understand the encryption algorithm
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return Object.keys(ret).length ? ret : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -419,6 +520,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,
|
||||
});
|
||||
@@ -472,6 +574,7 @@ export default class SecretStorage extends EventEmitter {
|
||||
device_trust: this._baseApis.checkDeviceTrust(sender, deviceId),
|
||||
});
|
||||
if (secret) {
|
||||
logger.info(`Preparing ${content.name} secret for ${deviceId}`);
|
||||
const payload = {
|
||||
type: "m.secret.send",
|
||||
content: {
|
||||
@@ -508,7 +611,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 +626,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
|
||||
@@ -544,12 +650,12 @@ export default class SecretStorage extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
@@ -564,26 +670,48 @@ export default class SecretStorage extends EventEmitter {
|
||||
}
|
||||
|
||||
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];
|
||||
case 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];
|
||||
}
|
||||
case SECRET_STORAGE_ALGORITHM_V1_CURVE25519:
|
||||
{
|
||||
const pkDecryption = new global.Olm.PkDecryption();
|
||||
let pubkey;
|
||||
try {
|
||||
pubkey = pkDecryption.init_with_private_key(privateKey);
|
||||
} catch (e) {
|
||||
pkDecryption.free();
|
||||
throw new Error("getSecretStorageKey callback returned invalid key");
|
||||
}
|
||||
default:
|
||||
throw new Error("Unknown key type: " + keys[keyId].algorithm);
|
||||
if (pubkey !== keys[keyId].pubkey) {
|
||||
pkDecryption.free();
|
||||
throw new Error(
|
||||
"getSecretStorageKey callback returned incorrect key",
|
||||
);
|
||||
}
|
||||
const decryption = {
|
||||
free: pkDecryption.free.bind(pkDecryption),
|
||||
decrypt: async function(encInfo) {
|
||||
return pkDecryption.decrypt(
|
||||
encInfo.ephemeral, encInfo.mac, encInfo.ciphertext,
|
||||
);
|
||||
},
|
||||
// needed for passthrough
|
||||
get_private_key: pkDecryption.get_private_key.bind(pkDecryption),
|
||||
};
|
||||
return [keyId, decryption];
|
||||
}
|
||||
default:
|
||||
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");
|
||||
|
||||
if (hmac !== data.mac) {
|
||||
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";
|
||||
|
||||
+662
-174
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,
|
||||
@@ -334,4 +340,4 @@ OlmDecryption.prototype._decryptMessage = async function(
|
||||
};
|
||||
|
||||
|
||||
base.registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
|
||||
registerAlgorithm(olmlib.OLM_ALGORITHM, OlmEncryption, OlmDecryption);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+1024
-304
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);
|
||||
|
||||
+105
-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,30 +174,37 @@ 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 (!olmDevice._sessionsInProgress[key]) {
|
||||
@@ -198,7 +255,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,14 +265,14 @@ 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;
|
||||
@@ -263,12 +320,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 +371,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 +392,7 @@ const _verifySignature = module.exports.verifySignature = async function(
|
||||
olmDevice.verifySignature(
|
||||
signingKey, json, signature,
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign a JSON object using public key cryptography
|
||||
@@ -347,7 +404,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 +428,7 @@ module.exports.pkSign = function(obj, key, userId, pubkey) {
|
||||
key.free();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a signed JSON object
|
||||
@@ -379,7 +436,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 +454,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);
|
||||
});
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
|
||||
@@ -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,9 +16,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import logger from '../../logger';
|
||||
import LocalStorageCryptoStore from './localStorage-crypto-store';
|
||||
import MemoryCryptoStore from './memory-crypto-store';
|
||||
import {logger} from '../../logger';
|
||||
import {LocalStorageCryptoStore} from './localStorage-crypto-store';
|
||||
import {MemoryCryptoStore} from './memory-crypto-store';
|
||||
import * as IndexedDBCryptoStoreBackend from './indexeddb-crypto-store-backend';
|
||||
import {InvalidCryptoStoreError} from '../../errors';
|
||||
import * as IndexedDBHelpers from "../../indexeddb-helpers";
|
||||
@@ -34,7 +35,7 @@ import * as IndexedDBHelpers from "../../indexeddb-helpers";
|
||||
*
|
||||
* @implements {module:crypto/store/base~CryptoStore}
|
||||
*/
|
||||
export default class IndexedDBCryptoStore {
|
||||
export class IndexedDBCryptoStore {
|
||||
/**
|
||||
* Create a new IndexedDBCryptoStore
|
||||
*
|
||||
@@ -45,6 +46,7 @@ export default class IndexedDBCryptoStore {
|
||||
this._indexedDB = indexedDB;
|
||||
this._dbName = dbName;
|
||||
this._backendPromise = null;
|
||||
this._backend = null;
|
||||
}
|
||||
|
||||
static exists(indexedDB, dbName) {
|
||||
@@ -55,10 +57,12 @@ export default class IndexedDBCryptoStore {
|
||||
* Ensure the database exists and is up-to-date, or fall back to
|
||||
* a local storage or in-memory store.
|
||||
*
|
||||
* This must be called before the store can be used.
|
||||
*
|
||||
* @return {Promise} resolves to either an IndexedDBCryptoStoreBackend.Backend,
|
||||
* or a MemoryCryptoStore
|
||||
*/
|
||||
_connect() {
|
||||
startup() {
|
||||
if (this._backendPromise) {
|
||||
return this._backendPromise;
|
||||
}
|
||||
@@ -104,7 +108,10 @@ export default class IndexedDBCryptoStore {
|
||||
// we can fall back to a different backend.
|
||||
return backend.doTxn(
|
||||
'readonly',
|
||||
[IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS],
|
||||
[
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS,
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD,
|
||||
],
|
||||
(txn) => {
|
||||
backend.getEndToEndInboundGroupSession('', '', txn, () => {});
|
||||
}).then(() => {
|
||||
@@ -131,6 +138,8 @@ export default class IndexedDBCryptoStore {
|
||||
);
|
||||
return new MemoryCryptoStore();
|
||||
}
|
||||
}).then(backend => {
|
||||
this._backend = backend;
|
||||
});
|
||||
|
||||
return this._backendPromise;
|
||||
@@ -185,9 +194,7 @@ export default class IndexedDBCryptoStore {
|
||||
* same instance as passed in, or the existing one.
|
||||
*/
|
||||
getOrAddOutgoingRoomKeyRequest(request) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.getOrAddOutgoingRoomKeyRequest(request);
|
||||
});
|
||||
return this._backend.getOrAddOutgoingRoomKeyRequest(request);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -201,9 +208,7 @@ export default class IndexedDBCryptoStore {
|
||||
* not found
|
||||
*/
|
||||
getOutgoingRoomKeyRequest(requestBody) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.getOutgoingRoomKeyRequest(requestBody);
|
||||
});
|
||||
return this._backend.getOutgoingRoomKeyRequest(requestBody);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,9 +222,18 @@ export default class IndexedDBCryptoStore {
|
||||
* requests in those states, an arbitrary one is chosen.
|
||||
*/
|
||||
getOutgoingRoomKeyRequestByState(wantedStates) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.getOutgoingRoomKeyRequestByState(wantedStates);
|
||||
});
|
||||
return this._backend.getOutgoingRoomKeyRequestByState(wantedStates);
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for room key requests by state –
|
||||
* unlike above, return a list of all entries in one state.
|
||||
*
|
||||
* @param {Number} wantedState
|
||||
* @return {Promise<Array<*>>} Returns an array of requests in the given state
|
||||
*/
|
||||
getAllOutgoingRoomKeyRequestsByState(wantedState) {
|
||||
return this._backend.getAllOutgoingRoomKeyRequestsByState(wantedState);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,11 +247,9 @@ export default class IndexedDBCryptoStore {
|
||||
* {@link module:crypto/store/base~OutgoingRoomKeyRequest}
|
||||
*/
|
||||
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.getOutgoingRoomKeyRequestsByTarget(
|
||||
userId, deviceId, wantedStates,
|
||||
);
|
||||
});
|
||||
return this._backend.getOutgoingRoomKeyRequestsByTarget(
|
||||
userId, deviceId, wantedStates,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,11 +265,9 @@ export default class IndexedDBCryptoStore {
|
||||
* updated request, or null if no matching row was found
|
||||
*/
|
||||
updateOutgoingRoomKeyRequest(requestId, expectedState, updates) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.updateOutgoingRoomKeyRequest(
|
||||
requestId, expectedState, updates,
|
||||
);
|
||||
});
|
||||
return this._backend.updateOutgoingRoomKeyRequest(
|
||||
requestId, expectedState, updates,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -270,9 +280,7 @@ export default class IndexedDBCryptoStore {
|
||||
* @returns {Promise} resolves once the operation is completed
|
||||
*/
|
||||
deleteOutgoingRoomKeyRequest(requestId, expectedState) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
|
||||
});
|
||||
return this._backend.deleteOutgoingRoomKeyRequest(requestId, expectedState);
|
||||
}
|
||||
|
||||
// Olm Account
|
||||
@@ -285,9 +293,7 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {function(string)} func Called with the account pickle
|
||||
*/
|
||||
getAccount(txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getAccount(txn, func);
|
||||
});
|
||||
this._backend.getAccount(txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -298,9 +304,7 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {string} newData The new account pickle to store.
|
||||
*/
|
||||
storeAccount(txn, newData) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeAccount(txn, newData);
|
||||
});
|
||||
this._backend.storeAccount(txn, newData);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -312,9 +316,16 @@ export default class IndexedDBCryptoStore {
|
||||
* { key_type: base64 encoded seed } where key type = user_signing_key_seed or self_signing_key_seed
|
||||
*/
|
||||
getCrossSigningKeys(txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getCrossSigningKeys(txn, func);
|
||||
});
|
||||
this._backend.getCrossSigningKeys(txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
* @param {function(string)} func Called with the private key
|
||||
* @param {string} type A key type
|
||||
*/
|
||||
getSecretStorePrivateKey(txn, func, type) {
|
||||
this._backend.getSecretStorePrivateKey(txn, func, type);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -324,9 +335,18 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {string} keys keys object as getCrossSigningKeys()
|
||||
*/
|
||||
storeCrossSigningKeys(txn, keys) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeCrossSigningKeys(txn, keys);
|
||||
});
|
||||
this._backend.storeCrossSigningKeys(txn, keys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the cross-signing private keys back to the store
|
||||
*
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
* @param {string} type The type of cross-signing private key to store
|
||||
* @param {string} key keys object as getCrossSigningKeys()
|
||||
*/
|
||||
storeSecretStorePrivateKey(txn, type, key) {
|
||||
this._backend.storeSecretStorePrivateKey(txn, type, key);
|
||||
}
|
||||
|
||||
// Olm sessions
|
||||
@@ -337,9 +357,7 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {function(int)} func Called with the count of sessions
|
||||
*/
|
||||
countEndToEndSessions(txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.countEndToEndSessions(txn, func);
|
||||
});
|
||||
this._backend.countEndToEndSessions(txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -355,9 +373,7 @@ export default class IndexedDBCryptoStore {
|
||||
* a message.
|
||||
*/
|
||||
getEndToEndSession(deviceKey, sessionId, txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndSession(deviceKey, sessionId, txn, func);
|
||||
});
|
||||
this._backend.getEndToEndSession(deviceKey, sessionId, txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -372,9 +388,7 @@ export default class IndexedDBCryptoStore {
|
||||
* a message.
|
||||
*/
|
||||
getEndToEndSessions(deviceKey, txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndSessions(deviceKey, txn, func);
|
||||
});
|
||||
this._backend.getEndToEndSessions(deviceKey, txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -385,9 +399,7 @@ export default class IndexedDBCryptoStore {
|
||||
* and session keys.
|
||||
*/
|
||||
getAllEndToEndSessions(txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getAllEndToEndSessions(txn, func);
|
||||
});
|
||||
this._backend.getAllEndToEndSessions(txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,11 +410,21 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndSession(
|
||||
deviceKey, sessionId, sessionInfo, txn,
|
||||
);
|
||||
});
|
||||
this._backend.storeEndToEndSession(
|
||||
deviceKey, sessionId, sessionInfo, txn,
|
||||
);
|
||||
}
|
||||
|
||||
storeEndToEndSessionProblem(deviceKey, type, fixed) {
|
||||
return this._backend.storeEndToEndSessionProblem(deviceKey, type, fixed);
|
||||
}
|
||||
|
||||
getEndToEndSessionProblem(deviceKey, timestamp) {
|
||||
return this._backend.getEndToEndSessionProblem(deviceKey, timestamp);
|
||||
}
|
||||
|
||||
filterOutNotifiedErrorDevices(devices) {
|
||||
return this._backend.filterOutNotifiedErrorDevices(devices);
|
||||
}
|
||||
|
||||
// Inbound group sessions
|
||||
@@ -417,11 +439,9 @@ export default class IndexedDBCryptoStore {
|
||||
* to Base64 end-to-end session.
|
||||
*/
|
||||
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, txn, func,
|
||||
);
|
||||
});
|
||||
this._backend.getEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, txn, func,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -432,9 +452,7 @@ export default class IndexedDBCryptoStore {
|
||||
* sessionData}, then once with null to indicate the end of the list.
|
||||
*/
|
||||
getAllEndToEndInboundGroupSessions(txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getAllEndToEndInboundGroupSessions(txn, func);
|
||||
});
|
||||
this._backend.getAllEndToEndInboundGroupSessions(txn, func);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -447,11 +465,9 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
addEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
});
|
||||
this._backend.addEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -464,11 +480,17 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndInboundGroupSession(senderCurve25519Key, sessionId, sessionData, txn) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
});
|
||||
this._backend.storeEndToEndInboundGroupSession(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
}
|
||||
|
||||
storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
) {
|
||||
this._backend.storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
);
|
||||
}
|
||||
|
||||
// End-to-end device tracking
|
||||
@@ -484,9 +506,7 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndDeviceData(deviceData, txn) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndDeviceData(deviceData, txn);
|
||||
});
|
||||
this._backend.storeEndToEndDeviceData(deviceData, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -497,9 +517,7 @@ export default class IndexedDBCryptoStore {
|
||||
* device data
|
||||
*/
|
||||
getEndToEndDeviceData(txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndDeviceData(txn, func);
|
||||
});
|
||||
this._backend.getEndToEndDeviceData(txn, func);
|
||||
}
|
||||
|
||||
// End to End Rooms
|
||||
@@ -511,9 +529,7 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {*} txn An active transaction. See doTxn().
|
||||
*/
|
||||
storeEndToEndRoom(roomId, roomInfo, txn) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.storeEndToEndRoom(roomId, roomInfo, txn);
|
||||
});
|
||||
this._backend.storeEndToEndRoom(roomId, roomInfo, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -522,9 +538,7 @@ export default class IndexedDBCryptoStore {
|
||||
* @param {function(Object)} func Function called with the end to end encrypted rooms
|
||||
*/
|
||||
getEndToEndRooms(txn, func) {
|
||||
this._backendPromise.then(backend => {
|
||||
backend.getEndToEndRooms(txn, func);
|
||||
});
|
||||
this._backend.getEndToEndRooms(txn, func);
|
||||
}
|
||||
|
||||
// session backups
|
||||
@@ -536,9 +550,7 @@ export default class IndexedDBCryptoStore {
|
||||
* @returns {Promise} resolves to an array of inbound group sessions
|
||||
*/
|
||||
getSessionsNeedingBackup(limit) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.getSessionsNeedingBackup(limit);
|
||||
});
|
||||
return this._backend.getSessionsNeedingBackup(limit);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -547,9 +559,7 @@ export default class IndexedDBCryptoStore {
|
||||
* @returns {Promise} resolves to the number of sessions
|
||||
*/
|
||||
countSessionsNeedingBackup(txn) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.countSessionsNeedingBackup(txn);
|
||||
});
|
||||
return this._backend.countSessionsNeedingBackup(txn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -559,9 +569,7 @@ export default class IndexedDBCryptoStore {
|
||||
* @returns {Promise} resolves when the sessions are unmarked
|
||||
*/
|
||||
unmarkSessionsNeedingBackup(sessions, txn) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.unmarkSessionsNeedingBackup(sessions, txn);
|
||||
});
|
||||
return this._backend.unmarkSessionsNeedingBackup(sessions, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -571,9 +579,7 @@ export default class IndexedDBCryptoStore {
|
||||
* @returns {Promise} resolves when the sessions are marked
|
||||
*/
|
||||
markSessionsNeedingBackup(sessions, txn) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.markSessionsNeedingBackup(sessions, txn);
|
||||
});
|
||||
return this._backend.markSessionsNeedingBackup(sessions, txn);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -598,15 +604,15 @@ export default class IndexedDBCryptoStore {
|
||||
* exception will propagate to the caller of the getFoo method.
|
||||
*/
|
||||
doTxn(mode, stores, func) {
|
||||
return this._connect().then((backend) => {
|
||||
return backend.doTxn(mode, stores, func);
|
||||
});
|
||||
return this._backend.doTxn(mode, stores, func);
|
||||
}
|
||||
}
|
||||
|
||||
IndexedDBCryptoStore.STORE_ACCOUNT = 'account';
|
||||
IndexedDBCryptoStore.STORE_SESSIONS = 'sessions';
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS = 'inbound_group_sessions';
|
||||
IndexedDBCryptoStore.STORE_INBOUND_GROUP_SESSIONS_WITHHELD
|
||||
= 'inbound_group_sessions_withheld';
|
||||
IndexedDBCryptoStore.STORE_DEVICE_DATA = 'device_data';
|
||||
IndexedDBCryptoStore.STORE_ROOMS = 'rooms';
|
||||
IndexedDBCryptoStore.STORE_BACKUP = 'sessions_needing_backup';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2017, 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.
|
||||
@@ -14,8 +15,8 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import logger from '../../logger';
|
||||
import MemoryCryptoStore from './memory-crypto-store';
|
||||
import {logger} from '../../logger';
|
||||
import {MemoryCryptoStore} from './memory-crypto-store';
|
||||
|
||||
/**
|
||||
* Internal module. Partial localStorage backed storage for e2e.
|
||||
@@ -30,8 +31,10 @@ import MemoryCryptoStore from './memory-crypto-store';
|
||||
const E2E_PREFIX = "crypto.";
|
||||
const KEY_END_TO_END_ACCOUNT = E2E_PREFIX + "account";
|
||||
const KEY_CROSS_SIGNING_KEYS = E2E_PREFIX + "cross_signing_keys";
|
||||
const KEY_NOTIFIED_ERROR_DEVICES = E2E_PREFIX + "notified_error_devices";
|
||||
const KEY_DEVICE_DATA = E2E_PREFIX + "device_data";
|
||||
const KEY_INBOUND_SESSION_PREFIX = E2E_PREFIX + "inboundgroupsessions/";
|
||||
const KEY_INBOUND_SESSION_WITHHELD_PREFIX = E2E_PREFIX + "inboundgroupsessions.withheld/";
|
||||
const KEY_ROOMS_PREFIX = E2E_PREFIX + "rooms/";
|
||||
const KEY_SESSIONS_NEEDING_BACKUP = E2E_PREFIX + "sessionsneedingbackup";
|
||||
|
||||
@@ -39,10 +42,18 @@ function keyEndToEndSessions(deviceKey) {
|
||||
return E2E_PREFIX + "sessions/" + deviceKey;
|
||||
}
|
||||
|
||||
function keyEndToEndSessionProblems(deviceKey) {
|
||||
return E2E_PREFIX + "session.problems/" + deviceKey;
|
||||
}
|
||||
|
||||
function keyEndToEndInboundGroupSession(senderKey, sessionId) {
|
||||
return KEY_INBOUND_SESSION_PREFIX + senderKey + "/" + sessionId;
|
||||
}
|
||||
|
||||
function keyEndToEndInboundGroupSessionWithheld(senderKey, sessionId) {
|
||||
return KEY_INBOUND_SESSION_WITHHELD_PREFIX + senderKey + "/" + sessionId;
|
||||
}
|
||||
|
||||
function keyEndToEndRoomsPrefix(roomId) {
|
||||
return KEY_ROOMS_PREFIX + roomId;
|
||||
}
|
||||
@@ -50,7 +61,7 @@ function keyEndToEndRoomsPrefix(roomId) {
|
||||
/**
|
||||
* @implements {module:crypto/store/base~CryptoStore}
|
||||
*/
|
||||
export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
export class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
constructor(webStore) {
|
||||
super();
|
||||
this.store = webStore;
|
||||
@@ -122,13 +133,71 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
);
|
||||
}
|
||||
|
||||
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
|
||||
const key = keyEndToEndSessionProblems(deviceKey);
|
||||
const problems = getJsonItem(this.store, key) || [];
|
||||
problems.push({type, fixed, time: Date.now()});
|
||||
problems.sort((a, b) => {
|
||||
return a.time - b.time;
|
||||
});
|
||||
setJsonItem(this.store, key, problems);
|
||||
}
|
||||
|
||||
async getEndToEndSessionProblem(deviceKey, timestamp) {
|
||||
const key = keyEndToEndSessionProblems(deviceKey);
|
||||
const problems = getJsonItem(this.store, key) || [];
|
||||
if (!problems.length) {
|
||||
return null;
|
||||
}
|
||||
const lastProblem = problems[problems.length - 1];
|
||||
for (const problem of problems) {
|
||||
if (problem.time > timestamp) {
|
||||
return Object.assign({}, problem, {fixed: lastProblem.fixed});
|
||||
}
|
||||
}
|
||||
if (lastProblem.fixed) {
|
||||
return null;
|
||||
} else {
|
||||
return lastProblem;
|
||||
}
|
||||
}
|
||||
|
||||
async filterOutNotifiedErrorDevices(devices) {
|
||||
const notifiedErrorDevices =
|
||||
getJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES) || {};
|
||||
const ret = [];
|
||||
|
||||
for (const device of devices) {
|
||||
const {userId, deviceInfo} = device;
|
||||
if (userId in notifiedErrorDevices) {
|
||||
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
|
||||
ret.push(device);
|
||||
notifiedErrorDevices[userId][deviceInfo.deviceId] = true;
|
||||
}
|
||||
} else {
|
||||
ret.push(device);
|
||||
notifiedErrorDevices[userId] = {[deviceInfo.deviceId]: true };
|
||||
}
|
||||
}
|
||||
|
||||
setJsonItem(this.store, KEY_NOTIFIED_ERROR_DEVICES, notifiedErrorDevices);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Inbound Group Sessions
|
||||
|
||||
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
|
||||
func(getJsonItem(
|
||||
this.store,
|
||||
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
|
||||
));
|
||||
func(
|
||||
getJsonItem(
|
||||
this.store,
|
||||
keyEndToEndInboundGroupSession(senderCurve25519Key, sessionId),
|
||||
),
|
||||
getJsonItem(
|
||||
this.store,
|
||||
keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getAllEndToEndInboundGroupSessions(txn, func) {
|
||||
@@ -170,6 +239,16 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
);
|
||||
}
|
||||
|
||||
storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
) {
|
||||
setJsonItem(
|
||||
this.store,
|
||||
keyEndToEndInboundGroupSessionWithheld(senderCurve25519Key, sessionId),
|
||||
sessionData,
|
||||
);
|
||||
}
|
||||
|
||||
getEndToEndDeviceData(txn, func) {
|
||||
func(getJsonItem(
|
||||
this.store, KEY_DEVICE_DATA,
|
||||
@@ -288,12 +367,23 @@ export default class LocalStorageCryptoStore extends MemoryCryptoStore {
|
||||
func(keys);
|
||||
}
|
||||
|
||||
getSecretStorePrivateKey(txn, func, type) {
|
||||
const key = getJsonItem(this.store, E2E_PREFIX + `ssss_cache.${type}`);
|
||||
func(key ? Uint8Array.from(key) : key);
|
||||
}
|
||||
|
||||
storeCrossSigningKeys(txn, keys) {
|
||||
setJsonItem(
|
||||
this.store, KEY_CROSS_SIGNING_KEYS, keys,
|
||||
);
|
||||
}
|
||||
|
||||
storeSecretStorePrivateKey(txn, type, key) {
|
||||
setJsonItem(
|
||||
this.store, E2E_PREFIX + `ssss_cache.${type}`, Array.from(key),
|
||||
);
|
||||
}
|
||||
|
||||
doTxn(mode, stores, func) {
|
||||
return Promise.resolve(func(null));
|
||||
}
|
||||
|
||||
@@ -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,8 +16,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. in-memory storage for e2e.
|
||||
@@ -27,16 +28,23 @@ import utils from '../../utils';
|
||||
/**
|
||||
* @implements {module:crypto/store/base~CryptoStore}
|
||||
*/
|
||||
export default class MemoryCryptoStore {
|
||||
export class MemoryCryptoStore {
|
||||
constructor() {
|
||||
this._outgoingRoomKeyRequests = [];
|
||||
this._account = null;
|
||||
this._crossSigningKeys = null;
|
||||
this._privateKeys = {};
|
||||
this._backupKeys = {};
|
||||
|
||||
// Map of {devicekey -> {sessionId -> session pickle}}
|
||||
this._sessions = {};
|
||||
// Map of {devicekey -> array of problems}
|
||||
this._sessionProblems = {};
|
||||
// Map of {userId -> deviceId -> true}
|
||||
this._notifiedErrorDevices = {};
|
||||
// Map of {senderCurve25519Key+'/'+sessionId -> session data object}
|
||||
this._inboundGroupSessions = {};
|
||||
this._inboundGroupSessionsWithheld = {};
|
||||
// Opaque device data object
|
||||
this._deviceData = null;
|
||||
// roomId -> Opaque roomInfo object
|
||||
@@ -45,6 +53,18 @@ export default class MemoryCryptoStore {
|
||||
this._sessionsNeedingBackup = {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the database exists and is up-to-date.
|
||||
*
|
||||
* This must be called before the store can be used.
|
||||
*
|
||||
* @return {Promise} resolves to the store.
|
||||
*/
|
||||
async startup() {
|
||||
// No startup work to do for the memory store.
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all data from this store.
|
||||
*
|
||||
@@ -146,6 +166,19 @@ export default class MemoryCryptoStore {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Number} wantedState
|
||||
* @return {Promise<Array<*>>} All OutgoingRoomKeyRequests in state
|
||||
*/
|
||||
getAllOutgoingRoomKeyRequestsByState(wantedState) {
|
||||
return Promise.resolve(
|
||||
this._outgoingRoomKeyRequests.filter(
|
||||
(r) => r.state == wantedState,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
getOutgoingRoomKeyRequestsByTarget(userId, deviceId, wantedStates) {
|
||||
const results = [];
|
||||
|
||||
@@ -237,10 +270,19 @@ export default class MemoryCryptoStore {
|
||||
func(this._crossSigningKeys);
|
||||
}
|
||||
|
||||
getSecretStorePrivateKey(txn, func, type) {
|
||||
const result = this._privateKeys[type];
|
||||
return func(result || null);
|
||||
}
|
||||
|
||||
storeCrossSigningKeys(txn, keys) {
|
||||
this._crossSigningKeys = keys;
|
||||
}
|
||||
|
||||
storeSecretStorePrivateKey(txn, type, key) {
|
||||
this._privateKeys[type] = key;
|
||||
}
|
||||
|
||||
// Olm Sessions
|
||||
|
||||
countEndToEndSessions(txn, func) {
|
||||
@@ -257,11 +299,15 @@ export default class MemoryCryptoStore {
|
||||
}
|
||||
|
||||
getAllEndToEndSessions(txn, func) {
|
||||
for (const deviceSessions of Object.values(this._sessions)) {
|
||||
for (const sess of Object.values(deviceSessions)) {
|
||||
func(sess);
|
||||
}
|
||||
}
|
||||
Object.entries(this._sessions).forEach(([deviceKey, deviceSessions]) => {
|
||||
Object.entries(deviceSessions).forEach(([sessionId, session]) => {
|
||||
func({
|
||||
...session,
|
||||
deviceKey,
|
||||
sessionId,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
storeEndToEndSession(deviceKey, sessionId, sessionInfo, txn) {
|
||||
@@ -273,10 +319,61 @@ export default class MemoryCryptoStore {
|
||||
deviceSessions[sessionId] = sessionInfo;
|
||||
}
|
||||
|
||||
async storeEndToEndSessionProblem(deviceKey, type, fixed) {
|
||||
const problems = this._sessionProblems[deviceKey]
|
||||
= this._sessionProblems[deviceKey] || [];
|
||||
problems.push({type, fixed, time: Date.now()});
|
||||
problems.sort((a, b) => {
|
||||
return a.time - b.time;
|
||||
});
|
||||
}
|
||||
|
||||
async getEndToEndSessionProblem(deviceKey, timestamp) {
|
||||
const problems = this._sessionProblems[deviceKey] || [];
|
||||
if (!problems.length) {
|
||||
return null;
|
||||
}
|
||||
const lastProblem = problems[problems.length - 1];
|
||||
for (const problem of problems) {
|
||||
if (problem.time > timestamp) {
|
||||
return Object.assign({}, problem, {fixed: lastProblem.fixed});
|
||||
}
|
||||
}
|
||||
if (lastProblem.fixed) {
|
||||
return null;
|
||||
} else {
|
||||
return lastProblem;
|
||||
}
|
||||
}
|
||||
|
||||
async filterOutNotifiedErrorDevices(devices) {
|
||||
const notifiedErrorDevices = this._notifiedErrorDevices;
|
||||
const ret = [];
|
||||
|
||||
for (const device of devices) {
|
||||
const {userId, deviceInfo} = device;
|
||||
if (userId in notifiedErrorDevices) {
|
||||
if (!(deviceInfo.deviceId in notifiedErrorDevices[userId])) {
|
||||
ret.push(device);
|
||||
notifiedErrorDevices[userId][deviceInfo.deviceId] = true;
|
||||
}
|
||||
} else {
|
||||
ret.push(device);
|
||||
notifiedErrorDevices[userId] = {[deviceInfo.deviceId]: true };
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Inbound Group Sessions
|
||||
|
||||
getEndToEndInboundGroupSession(senderCurve25519Key, sessionId, txn, func) {
|
||||
func(this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] || null);
|
||||
const k = senderCurve25519Key+'/'+sessionId;
|
||||
func(
|
||||
this._inboundGroupSessions[k] || null,
|
||||
this._inboundGroupSessionsWithheld[k] || null,
|
||||
);
|
||||
}
|
||||
|
||||
getAllEndToEndInboundGroupSessions(txn, func) {
|
||||
@@ -306,6 +403,13 @@ export default class MemoryCryptoStore {
|
||||
this._inboundGroupSessions[senderCurve25519Key+'/'+sessionId] = sessionData;
|
||||
}
|
||||
|
||||
storeEndToEndInboundGroupSessionWithheld(
|
||||
senderCurve25519Key, sessionId, sessionData, txn,
|
||||
) {
|
||||
const k = senderCurve25519Key+'/'+sessionId;
|
||||
this._inboundGroupSessionsWithheld[k] = sessionData;
|
||||
}
|
||||
|
||||
// Device Data
|
||||
|
||||
getEndToEndDeviceData(txn, func) {
|
||||
|
||||
+153
-11
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -21,13 +22,22 @@ limitations under the License.
|
||||
|
||||
import {MatrixEvent} from '../../models/event';
|
||||
import {EventEmitter} from 'events';
|
||||
import logger from '../../logger';
|
||||
import DeviceInfo from '../deviceinfo';
|
||||
import {logger} from '../../logger';
|
||||
import {DeviceInfo} from '../deviceinfo';
|
||||
import {newTimeoutError} from "./Error";
|
||||
import {CrossSigningInfo} from "../CrossSigning";
|
||||
import {decodeBase64} from "../olmlib";
|
||||
|
||||
const timeoutException = new Error("Verification timed out");
|
||||
|
||||
export default class VerificationBase extends EventEmitter {
|
||||
export class SwitchStartEventError extends Error {
|
||||
constructor(startEvent) {
|
||||
super();
|
||||
this.startEvent = startEvent;
|
||||
}
|
||||
}
|
||||
|
||||
export class VerificationBase extends EventEmitter {
|
||||
/**
|
||||
* Base class for verification methods.
|
||||
*
|
||||
@@ -67,9 +77,21 @@ export default class VerificationBase extends EventEmitter {
|
||||
this._done = false;
|
||||
this._promise = null;
|
||||
this._transactionTimeoutTimer = null;
|
||||
}
|
||||
|
||||
// At this point, the verification request was received so start the timeout timer.
|
||||
this._resetTimer();
|
||||
static keyRequestTimeoutMs = 1000 * 60;
|
||||
|
||||
get initiatedByMe() {
|
||||
// if there is no start event yet,
|
||||
// we probably want to send it,
|
||||
// which happens if we initiate
|
||||
if (!this.startEvent) {
|
||||
return true;
|
||||
}
|
||||
const sender = this.startEvent.getSender();
|
||||
const content = this.startEvent.getContent();
|
||||
return sender === this._baseApis.getUserId() &&
|
||||
content.from_device === this._baseApis.getDeviceId();
|
||||
}
|
||||
|
||||
_resetTimer() {
|
||||
@@ -107,6 +129,24 @@ export default class VerificationBase extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
canSwitchStartEvent() {
|
||||
return false;
|
||||
}
|
||||
|
||||
switchStartEvent(event) {
|
||||
if (this.canSwitchStartEvent(event)) {
|
||||
logger.log("Verification Base: switching verification start event",
|
||||
{restartingFlow: !!this._rejectEvent});
|
||||
if (this._rejectEvent) {
|
||||
const reject = this._rejectEvent;
|
||||
this._rejectEvent = undefined;
|
||||
reject(new SwitchStartEventError(event));
|
||||
} else {
|
||||
this.startEvent = event;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleEvent(e) {
|
||||
if (this._done) {
|
||||
return;
|
||||
@@ -122,8 +162,18 @@ export default class VerificationBase extends EventEmitter {
|
||||
} else if (e.getType() === "m.key.verification.cancel") {
|
||||
const reject = this._reject;
|
||||
this._reject = undefined;
|
||||
reject(new Error("Other side cancelled verification"));
|
||||
} else {
|
||||
// there is only promise to reject if verify has been called
|
||||
if (reject) {
|
||||
const content = e.getContent();
|
||||
const {reason, code} = content;
|
||||
reject(new Error(`Other side cancelled verification ` +
|
||||
`because ${reason} (${code})`));
|
||||
}
|
||||
} else if (this._expectedEvent) {
|
||||
// only cancel if there is an event expected.
|
||||
// if there is no event expected, it means verify() wasn't called
|
||||
// and we're just replaying the timeline events when syncing
|
||||
// after a refresh when the events haven't been stored in the cache yet.
|
||||
const exception = new Error(
|
||||
"Unexpected message: expecting " + this._expectedEvent
|
||||
+ " but got " + e.getType(),
|
||||
@@ -141,11 +191,95 @@ export default class VerificationBase extends EventEmitter {
|
||||
done() {
|
||||
this._endTimer(); // always kill the activity timer
|
||||
if (!this._done) {
|
||||
if (this._channel.needsDoneMessage) {
|
||||
// verification in DM requires a done message
|
||||
this._send("m.key.verification.done", {});
|
||||
}
|
||||
this.request.onVerifierFinished();
|
||||
this._resolve();
|
||||
|
||||
//#region Cross-signing keys request
|
||||
// If this is a self-verification, ask the other party for keys
|
||||
if (this._baseApis.getUserId() !== this.userId) {
|
||||
return;
|
||||
}
|
||||
// FIXME: This is a lot of logic that isn't anything to do with verification
|
||||
// and probably ought to be somewhere else.
|
||||
console.log("VerificationBase.done: 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 = this._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) => {
|
||||
console.debug("VerificationBase.done: requesting secret",
|
||||
type, this.deviceId);
|
||||
const { promise } = client.requestSecret(
|
||||
`m.cross_signing.${type}`, [this.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/riot-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,
|
||||
VerificationBase.keyRequestTimeoutMs,
|
||||
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', [this.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("self_signing"),
|
||||
crossSigning.getCrossSigningKey("user_signing"),
|
||||
backupKeyPromise,
|
||||
]),
|
||||
timeout,
|
||||
]).then(resolve, reject);
|
||||
}).catch((e) => {
|
||||
console.warn("VerificationBase: failure while requesting keys:", e);
|
||||
});
|
||||
//#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +287,7 @@ export default class VerificationBase extends EventEmitter {
|
||||
this._endTimer(); // always kill the activity timer
|
||||
if (!this._done) {
|
||||
this.cancelled = true;
|
||||
this.request.onVerifierCancelled();
|
||||
if (this.userId && this.deviceId) {
|
||||
// send a cancellation to the other user (if it wasn't
|
||||
// cancelled by the other user)
|
||||
@@ -263,6 +398,13 @@ export default class VerificationBase extends EventEmitter {
|
||||
throw new Error("No devices could be verified");
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"Verification completed! Marking devices verified: ",
|
||||
verifiedDevices,
|
||||
);
|
||||
// TODO: There should probably be a batch version of this, otherwise it's going
|
||||
// to upload each signature in a separate API call which is silly because the
|
||||
// API supports as many signatures as you like.
|
||||
for (const deviceId of verifiedDevices) {
|
||||
await this._baseApis.setDeviceVerified(userId, deviceId);
|
||||
}
|
||||
|
||||
@@ -23,12 +23,10 @@ limitations under the License.
|
||||
import {MatrixEvent} from "../../models/event";
|
||||
|
||||
export function newVerificationError(code, reason, extradata) {
|
||||
extradata = extradata || {};
|
||||
extradata.code = code;
|
||||
extradata.reason = reason;
|
||||
const content = Object.assign({}, {code, reason}, extradata);
|
||||
return new MatrixEvent({
|
||||
type: "m.key.verification.cancel",
|
||||
content: extradata,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Verification method that is illegal to have (cannot possibly
|
||||
* do verification with this method).
|
||||
* @module crypto/verification/IllegalMethod
|
||||
*/
|
||||
|
||||
import {VerificationBase as Base} from "./Base";
|
||||
|
||||
/**
|
||||
* @class crypto/verification/IllegalMethod/IllegalMethod
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class IllegalMethod extends Base {
|
||||
static factory(...args) {
|
||||
return new IllegalMethod(...args);
|
||||
}
|
||||
|
||||
static get NAME() {
|
||||
// Typically the name will be something else, but to complete
|
||||
// the contract we offer a default one here.
|
||||
return "org.matrix.illegal_method";
|
||||
}
|
||||
|
||||
async _doVerification() {
|
||||
throw new Error("Verification is not possible with this method");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/*
|
||||
Copyright 2018 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -19,105 +20,251 @@ limitations under the License.
|
||||
* @module crypto/verification/QRCode
|
||||
*/
|
||||
|
||||
import Base from "./Base";
|
||||
import {VerificationBase as Base} from "./Base";
|
||||
import {
|
||||
errorFactory,
|
||||
newUserCancelledError,
|
||||
newKeyMismatchError,
|
||||
newUserMismatchError,
|
||||
newUserCancelledError,
|
||||
} from './Error';
|
||||
import {encodeUnpaddedBase64, decodeBase64} from "../olmlib";
|
||||
|
||||
const MATRIXTO_REGEXP = /^(?:https?:\/\/)?(?:www\.)?matrix\.to\/#\/([#@!+][^?]+)\?(.+)$/;
|
||||
const KEY_REGEXP = /^key_([^:]+:.+)$/;
|
||||
|
||||
const newQRCodeError = errorFactory("m.qr_code.invalid", "Invalid QR code");
|
||||
export const SHOW_QR_CODE_METHOD = "m.qr_code.show.v1";
|
||||
export const SCAN_QR_CODE_METHOD = "m.qr_code.scan.v1";
|
||||
|
||||
/**
|
||||
* @class crypto/verification/QRCode/ShowQRCode
|
||||
* @class crypto/verification/QRCode/ReciprocateQRCode
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class ShowQRCode extends Base {
|
||||
_doVerification() {
|
||||
if (!this._done) {
|
||||
const url = "https://matrix.to/#/" + this._baseApis.getUserId()
|
||||
+ "?device=" + encodeURIComponent(this._baseApis.deviceId)
|
||||
+ "&action=verify&key_ed25519%3A"
|
||||
+ encodeURIComponent(this._baseApis.deviceId) + "="
|
||||
+ encodeURIComponent(this._baseApis.getDeviceEd25519Key());
|
||||
this.emit("show_qr_code", {
|
||||
url: url,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ShowQRCode.NAME = "m.qr_code.show.v1";
|
||||
|
||||
/**
|
||||
* @class crypto/verification/QRCode/ScanQRCode
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export class ScanQRCode extends Base {
|
||||
export class ReciprocateQRCode extends Base {
|
||||
static factory(...args) {
|
||||
return new ScanQRCode(...args);
|
||||
return new ReciprocateQRCode(...args);
|
||||
}
|
||||
|
||||
static get NAME() {
|
||||
return "m.reciprocate.v1";
|
||||
}
|
||||
|
||||
async _doVerification() {
|
||||
const code = await new Promise((resolve, reject) => {
|
||||
this.emit("scan", {
|
||||
done: resolve,
|
||||
if (!this.startEvent) {
|
||||
// TODO: Support scanning QR codes
|
||||
throw new Error("It is not currently possible to start verification" +
|
||||
"with this method yet.");
|
||||
}
|
||||
|
||||
const {qrCodeData} = this.request;
|
||||
// 1. check the secret
|
||||
if (this.startEvent.getContent()['secret'] !== qrCodeData.encodedSharedSecret) {
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
|
||||
// 2. ask if other user shows shield as well
|
||||
await new Promise((resolve, reject) => {
|
||||
this.reciprocateQREvent = {
|
||||
confirm: resolve,
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
});
|
||||
};
|
||||
this.emit("show_reciprocate_qr", this.reciprocateQREvent);
|
||||
});
|
||||
|
||||
const match = code.match(MATRIXTO_REGEXP);
|
||||
let deviceId;
|
||||
// 3. determine key to sign
|
||||
const keys = {};
|
||||
if (!match) {
|
||||
throw newQRCodeError();
|
||||
}
|
||||
const userId = match[1];
|
||||
const params = match[2].split("&").map(
|
||||
(x) => x.split("=", 2).map(decodeURIComponent),
|
||||
);
|
||||
let action;
|
||||
for (const [name, value] of params) {
|
||||
if (name === "device") {
|
||||
deviceId = value;
|
||||
} else if (name === "action") {
|
||||
action = value;
|
||||
} else {
|
||||
const keyMatch = name.match(KEY_REGEXP);
|
||||
if (keyMatch) {
|
||||
keys[keyMatch[1]] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!deviceId || action !== "verify" || Object.keys(keys).length === 0) {
|
||||
throw newQRCodeError();
|
||||
if (qrCodeData.mode === MODE_VERIFY_OTHER_USER) {
|
||||
// add master key to keys to be signed, only if we're not doing self-verification
|
||||
const masterKey = qrCodeData.otherUserMasterKey;
|
||||
keys[`ed25519:${masterKey}`] = masterKey;
|
||||
} else if (qrCodeData.mode === MODE_VERIFY_SELF_TRUSTED) {
|
||||
const deviceId = this.request.targetDevice.deviceId;
|
||||
keys[`ed25519:${deviceId}`] = qrCodeData.otherDeviceKey;
|
||||
} else {
|
||||
// TODO: not sure if MODE_VERIFY_SELF_UNTRUSTED makes sense to sign anything here?
|
||||
}
|
||||
|
||||
if (!this.userId) {
|
||||
await new Promise((resolve, reject) => {
|
||||
this.emit("confirm_user_id", {
|
||||
userId: userId,
|
||||
confirm: resolve,
|
||||
cancel: () => reject(newUserMismatchError()),
|
||||
});
|
||||
});
|
||||
} else if (this.userId !== userId) {
|
||||
throw newUserMismatchError({
|
||||
expected: this.userId,
|
||||
actual: userId,
|
||||
});
|
||||
}
|
||||
// 4. sign the key
|
||||
await this._verifyKeys(this.userId, keys, (keyId, device, keyInfo) => {
|
||||
// make sure the device has the expected keys
|
||||
const targetKey = keys[keyId];
|
||||
if (!targetKey) throw newKeyMismatchError();
|
||||
|
||||
await this._verifyKeys(userId, keys, (keyId, device, key) => {
|
||||
if (device.keys[keyId] !== key) {
|
||||
if (keyInfo !== targetKey) {
|
||||
console.error("key ID from key info does not match");
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
for (const deviceKeyId in device.keys) {
|
||||
if (!deviceKeyId.startsWith("ed25519")) continue;
|
||||
const deviceTargetKey = keys[deviceKeyId];
|
||||
if (!deviceTargetKey) throw newKeyMismatchError();
|
||||
if (device.keys[deviceKeyId] !== deviceTargetKey) {
|
||||
console.error("master key does not match");
|
||||
throw newKeyMismatchError();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ScanQRCode.NAME = "m.qr_code.scan.v1";
|
||||
const CODE_VERSION = 0x02; // the version of binary QR codes we support
|
||||
const BINARY_PREFIX = "MATRIX"; // ASCII, used to prefix the binary format
|
||||
const MODE_VERIFY_OTHER_USER = 0x00; // Verifying someone who isn't us
|
||||
const MODE_VERIFY_SELF_TRUSTED = 0x01; // We trust the master key
|
||||
const MODE_VERIFY_SELF_UNTRUSTED = 0x02; // We do not trust the master key
|
||||
|
||||
export class QRCodeData {
|
||||
constructor(mode, sharedSecret, otherUserMasterKey, otherDeviceKey, buffer) {
|
||||
this._sharedSecret = sharedSecret;
|
||||
this._mode = mode;
|
||||
this._otherUserMasterKey = otherUserMasterKey;
|
||||
this._otherDeviceKey = otherDeviceKey;
|
||||
this._buffer = buffer;
|
||||
}
|
||||
|
||||
static async create(request, client) {
|
||||
const sharedSecret = QRCodeData._generateSharedSecret();
|
||||
const mode = QRCodeData._determineMode(request, client);
|
||||
let otherUserMasterKey = null;
|
||||
let otherDeviceKey = null;
|
||||
if (mode === MODE_VERIFY_OTHER_USER) {
|
||||
const otherUserCrossSigningInfo =
|
||||
client.getStoredCrossSigningForUser(request.otherUserId);
|
||||
otherUserMasterKey = otherUserCrossSigningInfo.getId("master");
|
||||
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
|
||||
otherDeviceKey = await QRCodeData._getOtherDeviceKey(request, client);
|
||||
}
|
||||
const qrData = QRCodeData._generateQrData(
|
||||
request, client, mode,
|
||||
sharedSecret,
|
||||
otherUserMasterKey,
|
||||
otherDeviceKey,
|
||||
);
|
||||
const buffer = QRCodeData._generateBuffer(qrData);
|
||||
return new QRCodeData(mode, sharedSecret,
|
||||
otherUserMasterKey, otherDeviceKey, buffer);
|
||||
}
|
||||
|
||||
get buffer() {
|
||||
return this._buffer;
|
||||
}
|
||||
|
||||
get mode() {
|
||||
return this._mode;
|
||||
}
|
||||
|
||||
get otherDeviceKey() {
|
||||
return this._otherDeviceKey;
|
||||
}
|
||||
|
||||
get otherUserMasterKey() {
|
||||
return this._otherUserMasterKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* The unpadded base64 encoded shared secret.
|
||||
*/
|
||||
get encodedSharedSecret() {
|
||||
return this._sharedSecret;
|
||||
}
|
||||
|
||||
static _generateSharedSecret() {
|
||||
const secretBytes = new Uint8Array(11);
|
||||
global.crypto.getRandomValues(secretBytes);
|
||||
return encodeUnpaddedBase64(secretBytes);
|
||||
}
|
||||
|
||||
static async _getOtherDeviceKey(request, client) {
|
||||
const myUserId = client.getUserId();
|
||||
const otherDevice = request.targetDevice;
|
||||
const otherDeviceId = otherDevice ? otherDevice.deviceId : null;
|
||||
const device = await client.getStoredDevice(myUserId, otherDeviceId);
|
||||
if (!device) {
|
||||
throw new Error("could not find device " + otherDeviceId);
|
||||
}
|
||||
const key = device.getFingerprint();
|
||||
return key;
|
||||
}
|
||||
|
||||
static _determineMode(request, client) {
|
||||
const myUserId = client.getUserId();
|
||||
const otherUserId = request.otherUserId;
|
||||
|
||||
let mode = MODE_VERIFY_OTHER_USER;
|
||||
if (myUserId === otherUserId) {
|
||||
// Mode changes depending on whether or not we trust the master cross signing key
|
||||
const myTrust = client.checkUserTrust(myUserId);
|
||||
if (myTrust.isCrossSigningVerified()) {
|
||||
mode = MODE_VERIFY_SELF_TRUSTED;
|
||||
} else {
|
||||
mode = MODE_VERIFY_SELF_UNTRUSTED;
|
||||
}
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
|
||||
static _generateQrData(request, client, mode,
|
||||
encodedSharedSecret, otherUserMasterKey, otherDeviceKey,
|
||||
) {
|
||||
const myUserId = client.getUserId();
|
||||
const transactionId = request.channel.transactionId;
|
||||
const qrData = {
|
||||
prefix: BINARY_PREFIX,
|
||||
version: CODE_VERSION,
|
||||
mode,
|
||||
transactionId,
|
||||
firstKeyB64: '', // worked out shortly
|
||||
secondKeyB64: '', // worked out shortly
|
||||
secretB64: encodedSharedSecret,
|
||||
};
|
||||
|
||||
const myCrossSigningInfo = client.getStoredCrossSigningForUser(myUserId);
|
||||
const myMasterKey = myCrossSigningInfo.getId("master");
|
||||
|
||||
if (mode === MODE_VERIFY_OTHER_USER) {
|
||||
// First key is our master cross signing key
|
||||
qrData.firstKeyB64 = myMasterKey;
|
||||
// Second key is the other user's master cross signing key
|
||||
qrData.secondKeyB64 = otherUserMasterKey;
|
||||
} else if (mode === MODE_VERIFY_SELF_TRUSTED) {
|
||||
// First key is our master cross signing key
|
||||
qrData.firstKeyB64 = myMasterKey;
|
||||
qrData.secondKeyB64 = otherDeviceKey;
|
||||
} else if (mode === MODE_VERIFY_SELF_UNTRUSTED) {
|
||||
// First key is our device's key
|
||||
qrData.firstKeyB64 = client.getDeviceEd25519Key();
|
||||
// Second key is what we think our master cross signing key is
|
||||
qrData.secondKeyB64 = myMasterKey;
|
||||
}
|
||||
return qrData;
|
||||
}
|
||||
|
||||
static _generateBuffer(qrData) {
|
||||
let buf = Buffer.alloc(0); // we'll concat our way through life
|
||||
|
||||
const appendByte = (b) => {
|
||||
const tmpBuf = Buffer.from([b]);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendInt = (i) => {
|
||||
const tmpBuf = Buffer.alloc(2);
|
||||
tmpBuf.writeInt16BE(i, 0);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendStr = (s, enc, withLengthPrefix = true) => {
|
||||
const tmpBuf = Buffer.from(s, enc);
|
||||
if (withLengthPrefix) appendInt(tmpBuf.byteLength);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
const appendEncBase64 = (b64) => {
|
||||
const b = decodeBase64(b64);
|
||||
const tmpBuf = Buffer.from(b);
|
||||
buf = Buffer.concat([buf, tmpBuf]);
|
||||
};
|
||||
|
||||
// Actually build the buffer for the QR code
|
||||
appendStr(qrData.prefix, "ascii", false);
|
||||
appendByte(qrData.version);
|
||||
appendByte(qrData.mode);
|
||||
appendStr(qrData.transactionId, "utf-8");
|
||||
appendEncBase64(qrData.firstKeyB64);
|
||||
appendEncBase64(qrData.secondKeyB64);
|
||||
appendEncBase64(qrData.secretB64);
|
||||
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
+107
-40
@@ -19,15 +19,18 @@ limitations under the License.
|
||||
* @module crypto/verification/SAS
|
||||
*/
|
||||
|
||||
import Base from "./Base";
|
||||
import {VerificationBase as Base, SwitchStartEventError} from "./Base";
|
||||
import anotherjson from 'another-json';
|
||||
import {
|
||||
errorFactory,
|
||||
newUserCancelledError,
|
||||
newUnknownMethodError,
|
||||
newKeyMismatchError,
|
||||
newInvalidMessageError,
|
||||
newKeyMismatchError,
|
||||
newUnknownMethodError,
|
||||
newUserCancelledError,
|
||||
} from './Error';
|
||||
import {logger} from '../../logger';
|
||||
|
||||
const START_TYPE = "m.key.verification.start";
|
||||
|
||||
const EVENTS = [
|
||||
"m.key.verification.accept",
|
||||
@@ -108,7 +111,7 @@ const emojiMapping = [
|
||||
["✏️", "pencil"], // 43
|
||||
["📎", "paperclip"], // 44
|
||||
["✂️", "scissors"], // 45
|
||||
["🔒", "padlock"], // 46
|
||||
["🔒", "lock"], // 46
|
||||
["🔑", "key"], // 47
|
||||
["🔨", "hammer"], // 48
|
||||
["☎️", "telephone"], // 49
|
||||
@@ -163,6 +166,15 @@ const macMethods = {
|
||||
"hmac-sha256": "calculate_mac_long_kdf",
|
||||
};
|
||||
|
||||
function calculateMAC(olmSAS, method) {
|
||||
return function(...args) {
|
||||
const macFunction = olmSAS[macMethods[method]];
|
||||
const mac = macFunction.apply(olmSAS, args);
|
||||
logger.log("SAS calculateMAC:", method, args, mac);
|
||||
return mac;
|
||||
};
|
||||
}
|
||||
|
||||
/* lists of algorithms/methods that are supported. The key agreement, hashes,
|
||||
* and MAC lists should be sorted in order of preference (most preferred
|
||||
* first).
|
||||
@@ -185,7 +197,11 @@ function intersection(anArray, aSet) {
|
||||
* @alias module:crypto/verification/SAS
|
||||
* @extends {module:crypto/verification/Base}
|
||||
*/
|
||||
export default class SAS extends Base {
|
||||
export class SAS extends Base {
|
||||
static get NAME() {
|
||||
return "m.sas.v1";
|
||||
}
|
||||
|
||||
get events() {
|
||||
return EVENTS;
|
||||
}
|
||||
@@ -197,16 +213,37 @@ export default class SAS extends Base {
|
||||
// make sure user's keys are downloaded
|
||||
await this._baseApis.downloadKeys([this.userId]);
|
||||
|
||||
if (this.startEvent) {
|
||||
return await this._doRespondVerification();
|
||||
} else {
|
||||
return await this._doSendVerification();
|
||||
}
|
||||
let retry = false;
|
||||
do {
|
||||
try {
|
||||
if (this.initiatedByMe) {
|
||||
return await this._doSendVerification();
|
||||
} else {
|
||||
return await this._doRespondVerification();
|
||||
}
|
||||
} catch (err) {
|
||||
if (err instanceof SwitchStartEventError) {
|
||||
// this changes what initiatedByMe returns
|
||||
this.startEvent = err.startEvent;
|
||||
retry = true;
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
} while (retry);
|
||||
}
|
||||
|
||||
async _doSendVerification() {
|
||||
const type = "m.key.verification.start";
|
||||
const initialMessage = this._channel.completeContent(type, {
|
||||
canSwitchStartEvent(event) {
|
||||
if (event.getType() !== START_TYPE) {
|
||||
return false;
|
||||
}
|
||||
const content = event.getContent();
|
||||
return content && content.method === SAS.NAME &&
|
||||
this._waitingForAccept;
|
||||
}
|
||||
|
||||
async _sendStart() {
|
||||
const startContent = this._channel.completeContent(START_TYPE, {
|
||||
method: SAS.NAME,
|
||||
from_device: this._baseApis.deviceId,
|
||||
key_agreement_protocols: KEY_AGREEMENT_LIST,
|
||||
@@ -215,11 +252,33 @@ export default class SAS extends Base {
|
||||
// FIXME: allow app to specify what SAS methods can be used
|
||||
short_authentication_string: SAS_LIST,
|
||||
});
|
||||
// add the transaction id to the message beforehand because
|
||||
// it needs to be included in the commitment hash later on
|
||||
this._channel.sendCompleted(type, initialMessage);
|
||||
await this._channel.sendCompleted(START_TYPE, startContent);
|
||||
return startContent;
|
||||
}
|
||||
|
||||
let e = await this._waitForEvent("m.key.verification.accept");
|
||||
async _doSendVerification() {
|
||||
this._waitingForAccept = true;
|
||||
let startContent;
|
||||
if (this.startEvent) {
|
||||
startContent = this._channel.completedContentFromEvent(this.startEvent);
|
||||
} else {
|
||||
startContent = await this._sendStart();
|
||||
}
|
||||
|
||||
// we might have switched to a different start event,
|
||||
// but was we didn't call _waitForEvent there was no
|
||||
// call that could throw yet. So check manually that
|
||||
// we're still on the initiator side
|
||||
if (!this.initiatedByMe) {
|
||||
throw new SwitchStartEventError(this.startEvent);
|
||||
}
|
||||
|
||||
let e;
|
||||
try {
|
||||
e = await this._waitForEvent("m.key.verification.accept");
|
||||
} finally {
|
||||
this._waitingForAccept = false;
|
||||
}
|
||||
let content = e.getContent();
|
||||
const sasMethods
|
||||
= intersection(content.short_authentication_string, SAS_SET);
|
||||
@@ -236,7 +295,7 @@ export default class SAS extends Base {
|
||||
const hashCommitment = content.commitment;
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
try {
|
||||
this._send("m.key.verification.key", {
|
||||
await this._send("m.key.verification.key", {
|
||||
key: olmSAS.get_pubkey(),
|
||||
});
|
||||
|
||||
@@ -244,7 +303,7 @@ export default class SAS extends Base {
|
||||
e = await this._waitForEvent("m.key.verification.key");
|
||||
// FIXME: make sure event is properly formed
|
||||
content = e.getContent();
|
||||
const commitmentStr = content.key + anotherjson.stringify(initialMessage);
|
||||
const commitmentStr = content.key + anotherjson.stringify(startContent);
|
||||
// TODO: use selected hash function (when we support multiple)
|
||||
if (olmutil.sha256(commitmentStr) !== hashCommitment) {
|
||||
throw newMismatchedCommitmentError();
|
||||
@@ -257,15 +316,20 @@ export default class SAS extends Base {
|
||||
+ this._channel.transactionId;
|
||||
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
|
||||
const verifySAS = new Promise((resolve, reject) => {
|
||||
this.emit("show_sas", {
|
||||
this.sasEvent = {
|
||||
sas: generateSas(sasBytes, sasMethods),
|
||||
confirm: () => {
|
||||
this._sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
confirm: async () => {
|
||||
try {
|
||||
await this._sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
mismatch: () => reject(newMismatchedSASError()),
|
||||
});
|
||||
};
|
||||
this.emit("show_sas", this.sasEvent);
|
||||
});
|
||||
|
||||
|
||||
@@ -317,7 +381,7 @@ export default class SAS extends Base {
|
||||
const olmSAS = new global.Olm.SAS();
|
||||
try {
|
||||
const commitmentStr = olmSAS.get_pubkey() + anotherjson.stringify(content);
|
||||
this._send("m.key.verification.accept", {
|
||||
await this._send("m.key.verification.accept", {
|
||||
key_agreement_protocol: keyAgreement,
|
||||
hash: hashMethod,
|
||||
message_authentication_code: macMethod,
|
||||
@@ -331,7 +395,7 @@ export default class SAS extends Base {
|
||||
// FIXME: make sure event is properly formed
|
||||
content = e.getContent();
|
||||
olmSAS.set_their_key(content.key);
|
||||
this._send("m.key.verification.key", {
|
||||
await this._send("m.key.verification.key", {
|
||||
key: olmSAS.get_pubkey(),
|
||||
});
|
||||
|
||||
@@ -341,15 +405,20 @@ export default class SAS extends Base {
|
||||
+ this._channel.transactionId;
|
||||
const sasBytes = olmSAS.generate_bytes(sasInfo, 6);
|
||||
const verifySAS = new Promise((resolve, reject) => {
|
||||
this.emit("show_sas", {
|
||||
this.sasEvent = {
|
||||
sas: generateSas(sasBytes, sasMethods),
|
||||
confirm: () => {
|
||||
this._sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
confirm: async () => {
|
||||
try {
|
||||
await this._sendMAC(olmSAS, macMethod);
|
||||
resolve();
|
||||
} catch(err) {
|
||||
reject(err);
|
||||
}
|
||||
},
|
||||
cancel: () => reject(newUserCancelledError()),
|
||||
mismatch: () => reject(newMismatchedSASError()),
|
||||
});
|
||||
};
|
||||
this.emit("show_sas", this.sasEvent);
|
||||
});
|
||||
|
||||
|
||||
@@ -380,7 +449,7 @@ export default class SAS extends Base {
|
||||
+ this._channel.transactionId;
|
||||
|
||||
const deviceKeyId = `ed25519:${this._baseApis.deviceId}`;
|
||||
mac[deviceKeyId] = olmSAS[macMethods[method]](
|
||||
mac[deviceKeyId] = calculateMAC(olmSAS, method)(
|
||||
this._baseApis.getDeviceEd25519Key(),
|
||||
baseInfo + deviceKeyId,
|
||||
);
|
||||
@@ -389,18 +458,18 @@ export default class SAS extends Base {
|
||||
const crossSigningId = this._baseApis.getCrossSigningId();
|
||||
if (crossSigningId) {
|
||||
const crossSigningKeyId = `ed25519:${crossSigningId}`;
|
||||
mac[crossSigningKeyId] = olmSAS[macMethods[method]](
|
||||
mac[crossSigningKeyId] = calculateMAC(olmSAS, method)(
|
||||
crossSigningId,
|
||||
baseInfo + crossSigningKeyId,
|
||||
);
|
||||
keyList.push(crossSigningKeyId);
|
||||
}
|
||||
|
||||
const keys = olmSAS[macMethods[method]](
|
||||
const keys = calculateMAC(olmSAS, method)(
|
||||
keyList.sort().join(","),
|
||||
baseInfo + "KEY_IDS",
|
||||
);
|
||||
this._send("m.key.verification.mac", { mac, keys });
|
||||
return this._send("m.key.verification.mac", { mac, keys });
|
||||
}
|
||||
|
||||
async _checkMAC(olmSAS, content, method) {
|
||||
@@ -409,7 +478,7 @@ export default class SAS extends Base {
|
||||
+ this._baseApis.getUserId() + this._baseApis.deviceId
|
||||
+ this._channel.transactionId;
|
||||
|
||||
if (content.keys !== olmSAS[macMethods[method]](
|
||||
if (content.keys !== calculateMAC(olmSAS, method)(
|
||||
Object.keys(content.mac).sort().join(","),
|
||||
baseInfo + "KEY_IDS",
|
||||
)) {
|
||||
@@ -417,7 +486,7 @@ export default class SAS extends Base {
|
||||
}
|
||||
|
||||
await this._verifyKeys(this.userId, content.mac, (keyId, device, keyInfo) => {
|
||||
if (keyInfo !== olmSAS[macMethods[method]](
|
||||
if (keyInfo !== calculateMAC(olmSAS, method)(
|
||||
device.keys[keyId],
|
||||
baseInfo + keyId,
|
||||
)) {
|
||||
@@ -426,5 +495,3 @@ export default class SAS extends Base {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
SAS.NAME = "m.sas.v1";
|
||||
|
||||
@@ -15,7 +15,14 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import VerificationRequest, {REQUEST_TYPE, START_TYPE} from "./VerificationRequest";
|
||||
import {
|
||||
VerificationRequest,
|
||||
REQUEST_TYPE,
|
||||
READY_TYPE,
|
||||
START_TYPE,
|
||||
} from "./VerificationRequest";
|
||||
import {logger} from '../../../logger';
|
||||
|
||||
const MESSAGE_TYPE = "m.room.message";
|
||||
const M_REFERENCE = "m.reference";
|
||||
const M_RELATES_TO = "m.relates_to";
|
||||
@@ -24,34 +31,54 @@ const M_RELATES_TO = "m.relates_to";
|
||||
* A key verification channel that sends verification events in the timeline of a room.
|
||||
* Uses the event id of the initial m.key.verification.request event as a transaction id.
|
||||
*/
|
||||
export default class InRoomChannel {
|
||||
export class InRoomChannel {
|
||||
/**
|
||||
* @param {MatrixClient} client the matrix client, to send messages with and get current user & device from.
|
||||
* @param {string} roomId id of the room where verification events should be posted in, should be a DM with the given user.
|
||||
* @param {string} userId id of user that the verification request is directed at, should be present in the room.
|
||||
*/
|
||||
constructor(client, roomId, userId) {
|
||||
constructor(client, roomId, userId = null) {
|
||||
this._client = client;
|
||||
this._roomId = roomId;
|
||||
this._userId = userId;
|
||||
this.userId = userId;
|
||||
this._requestEventId = null;
|
||||
}
|
||||
|
||||
/** Whether this channel needs m.key.verification.done messages to be sent after a successful verification */
|
||||
get needsDoneMessage() {
|
||||
get receiveStartFromOtherDevices() {
|
||||
return true;
|
||||
}
|
||||
|
||||
get roomId() {
|
||||
return this._roomId;
|
||||
}
|
||||
|
||||
/** The transaction id generated/used by this verification channel */
|
||||
get transactionId() {
|
||||
return this._requestEventId;
|
||||
}
|
||||
|
||||
static getOtherPartyUserId(event, client) {
|
||||
const type = InRoomChannel.getEventType(event);
|
||||
if (type !== REQUEST_TYPE) {
|
||||
return;
|
||||
}
|
||||
const ownUserId = client.getUserId();
|
||||
const sender = event.getSender();
|
||||
const content = event.getContent();
|
||||
const receiver = content.to;
|
||||
|
||||
if (sender === ownUserId) {
|
||||
return receiver;
|
||||
} else if (receiver === ownUserId) {
|
||||
return sender;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MatrixEvent} event the event to get the timestamp of
|
||||
* @return {number} the timestamp when the event was sent
|
||||
*/
|
||||
static getTimestamp(event) {
|
||||
getTimestamp(event) {
|
||||
return event.getTs();
|
||||
}
|
||||
|
||||
@@ -96,19 +123,26 @@ export default class InRoomChannel {
|
||||
}
|
||||
const type = InRoomChannel.getEventType(event);
|
||||
const content = event.getContent();
|
||||
|
||||
// from here on we're fairly sure that this is supposed to be
|
||||
// part of a verification request, so be noisy when rejecting something
|
||||
if (type === REQUEST_TYPE) {
|
||||
if (typeof content.to !== "string" || !content.to.length) {
|
||||
if (!content || typeof content.to !== "string" || !content.to.length) {
|
||||
logger.log("InRoomChannel: validateEvent: " +
|
||||
"no valid to " + (content && content.to));
|
||||
return false;
|
||||
}
|
||||
const ownUserId = client.getUserId();
|
||||
|
||||
// ignore requests that are not direct to or sent by the syncing user
|
||||
if (event.getSender() !== ownUserId && content.to !== ownUserId) {
|
||||
if (!InRoomChannel.getOtherPartyUserId(event, client)) {
|
||||
logger.log("InRoomChannel: validateEvent: " +
|
||||
`not directed to or sent by me: ${event.getSender()}` +
|
||||
`, ${content && content.to}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return VerificationRequest.validateEvent(
|
||||
type, event, InRoomChannel.getTimestamp(event), client);
|
||||
return VerificationRequest.validateEvent(type, event, client);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,28 +163,59 @@ export default class InRoomChannel {
|
||||
}
|
||||
}
|
||||
}
|
||||
return type;
|
||||
if (type && type !== REQUEST_TYPE) {
|
||||
return type;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the state of the channel, request, and verifier in response to a key verification event.
|
||||
* @param {MatrixEvent} event to handle
|
||||
* @param {VerificationRequest} request the request to forward handling to
|
||||
* @param {bool} isLiveEvent whether this is an even received through sync or not
|
||||
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
||||
*/
|
||||
async handleEvent(event, request) {
|
||||
async handleEvent(event, request, isLiveEvent) {
|
||||
// prevent processing the same event multiple times, as under
|
||||
// some circumstances Room.timeline can get emitted twice for the same event
|
||||
if (request.hasEventId(event.getId())) {
|
||||
return;
|
||||
}
|
||||
const type = InRoomChannel.getEventType(event);
|
||||
// do validations that need state (roomId, userId),
|
||||
// ignore if invalid
|
||||
if (event.getRoomId() !== this._roomId || event.getSender() !== this._userId) {
|
||||
|
||||
if (event.getRoomId() !== this._roomId) {
|
||||
return;
|
||||
}
|
||||
// set transactionId when receiving a .request
|
||||
if (!this._requestEventId && type === REQUEST_TYPE) {
|
||||
this._requestEventId = event.getId();
|
||||
// set userId if not set already
|
||||
if (this.userId === null) {
|
||||
const userId = InRoomChannel.getOtherPartyUserId(event, this._client);
|
||||
if (userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
}
|
||||
// ignore events not sent by us or the other party
|
||||
const ownUserId = this._client.getUserId();
|
||||
const sender = event.getSender();
|
||||
if (this.userId !== null) {
|
||||
if (sender !== ownUserId && sender !== this.userId) {
|
||||
logger.log(`InRoomChannel: ignoring verification event from ` +
|
||||
`non-participating sender ${sender}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (this._requestEventId === null) {
|
||||
this._requestEventId = InRoomChannel.getTransactionId(event);
|
||||
}
|
||||
|
||||
return await request.handleEvent(type, event, InRoomChannel.getTimestamp(event));
|
||||
const isRemoteEcho = !!event.getUnsigned().transaction_id;
|
||||
const isSentByUs = event.getSender() === this._client.getUserId();
|
||||
|
||||
return await request.handleEvent(
|
||||
type, event, isLiveEvent, isRemoteEcho, isSentByUs);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,7 +244,7 @@ export default class InRoomChannel {
|
||||
*/
|
||||
completeContent(type, content) {
|
||||
content = Object.assign({}, content);
|
||||
if (type === REQUEST_TYPE || type === START_TYPE) {
|
||||
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
|
||||
content.from_device = this._client.getDeviceId();
|
||||
}
|
||||
if (type === REQUEST_TYPE) {
|
||||
@@ -190,7 +255,7 @@ export default class InRoomChannel {
|
||||
"verification. You will need to use legacy key " +
|
||||
"verification to verify keys.",
|
||||
msgtype: REQUEST_TYPE,
|
||||
to: this._userId,
|
||||
to: this.userId,
|
||||
from_device: content.from_device,
|
||||
methods: content.methods,
|
||||
};
|
||||
@@ -231,3 +296,69 @@ export default class InRoomChannel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class InRoomRequests {
|
||||
constructor() {
|
||||
this._requestsByRoomId = new Map();
|
||||
}
|
||||
|
||||
getRequest(event) {
|
||||
const roomId = event.getRoomId();
|
||||
const txnId = InRoomChannel.getTransactionId(event);
|
||||
return this._getRequestByTxnId(roomId, txnId);
|
||||
}
|
||||
|
||||
getRequestByChannel(channel) {
|
||||
return this._getRequestByTxnId(channel.roomId, channel.transactionId);
|
||||
}
|
||||
|
||||
_getRequestByTxnId(roomId, txnId) {
|
||||
const requestsByTxnId = this._requestsByRoomId.get(roomId);
|
||||
if (requestsByTxnId) {
|
||||
return requestsByTxnId.get(txnId);
|
||||
}
|
||||
}
|
||||
|
||||
setRequest(event, request) {
|
||||
this._setRequest(
|
||||
event.getRoomId(),
|
||||
InRoomChannel.getTransactionId(event),
|
||||
request,
|
||||
);
|
||||
}
|
||||
|
||||
setRequestByChannel(channel, request) {
|
||||
this._setRequest(channel.roomId, channel.transactionId, request);
|
||||
}
|
||||
|
||||
_setRequest(roomId, txnId, request) {
|
||||
let requestsByTxnId = this._requestsByRoomId.get(roomId);
|
||||
if (!requestsByTxnId) {
|
||||
requestsByTxnId = new Map();
|
||||
this._requestsByRoomId.set(roomId, requestsByTxnId);
|
||||
}
|
||||
requestsByTxnId.set(txnId, request);
|
||||
}
|
||||
|
||||
removeRequest(event) {
|
||||
const roomId = event.getRoomId();
|
||||
const requestsByTxnId = this._requestsByRoomId.get(roomId);
|
||||
if (requestsByTxnId) {
|
||||
requestsByTxnId.delete(InRoomChannel.getTransactionId(event));
|
||||
if (requestsByTxnId.size === 0) {
|
||||
this._requestsByRoomId.delete(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findRequestInProgress(roomId) {
|
||||
const requestsByTxnId = this._requestsByRoomId.get(roomId);
|
||||
if (requestsByTxnId) {
|
||||
for (const request of requestsByTxnId.values()) {
|
||||
if (request.pending) {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
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.
|
||||
*/
|
||||
|
||||
/** a key verification channel that wraps over an actual channel to pass it to a verifier,
|
||||
* to notify the VerificationRequest when the verifier tries to send anything over the channel.
|
||||
* This way, the VerificationRequest can update its state based on events sent by the verifier.
|
||||
* Anything that is not sending is just routing through to the wrapped channel.
|
||||
*/
|
||||
export default class RequestCallbackChannel {
|
||||
constructor(request, channel) {
|
||||
this._request = request;
|
||||
this._channel = channel;
|
||||
}
|
||||
|
||||
get transactionId() {
|
||||
return this._channel.transactionId;
|
||||
}
|
||||
|
||||
get needsDoneMessage() {
|
||||
return this._channel.needsDoneMessage;
|
||||
}
|
||||
|
||||
handleEvent(event, request) {
|
||||
return this._channel.handleEvent(event, request);
|
||||
}
|
||||
|
||||
completedContentFromEvent(event) {
|
||||
return this._channel.completedContentFromEvent(event);
|
||||
}
|
||||
|
||||
completeContent(type, content) {
|
||||
return this._channel.completeContent(type, content);
|
||||
}
|
||||
|
||||
async send(type, uncompletedContent) {
|
||||
this._request.handleVerifierSend(type, uncompletedContent);
|
||||
const result = await this._channel.send(type, uncompletedContent);
|
||||
return result;
|
||||
}
|
||||
|
||||
async sendCompleted(type, content) {
|
||||
this._request.handleVerifierSend(type, content);
|
||||
const result = await this._channel.sendCompleted(type, content);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -15,33 +15,52 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { randomString } from '../../../randomstring';
|
||||
import logger from '../../../logger';
|
||||
import VerificationRequest, {
|
||||
PHASE_STARTED,
|
||||
REQUEST_TYPE,
|
||||
START_TYPE,
|
||||
CANCEL_TYPE,
|
||||
} from "./VerificationRequest";
|
||||
|
||||
import {randomString} from '../../../randomstring';
|
||||
import {logger} from '../../../logger';
|
||||
import {
|
||||
newUnexpectedMessageError,
|
||||
errorFromEvent,
|
||||
} from "../Error";
|
||||
CANCEL_TYPE,
|
||||
PHASE_STARTED,
|
||||
PHASE_READY,
|
||||
REQUEST_TYPE,
|
||||
READY_TYPE,
|
||||
START_TYPE,
|
||||
VerificationRequest,
|
||||
} from "./VerificationRequest";
|
||||
import {errorFromEvent, newUnexpectedMessageError} from "../Error";
|
||||
import {MatrixEvent} from "../../../models/event";
|
||||
|
||||
/**
|
||||
* A key verification channel that sends verification events over to_device messages.
|
||||
* Generates its own transaction ids.
|
||||
*/
|
||||
export default class ToDeviceChannel {
|
||||
export class ToDeviceChannel {
|
||||
// userId and devices of user we're about to verify
|
||||
constructor(client, userId, devices, transactionId = null, deviceId = null) {
|
||||
this._client = client;
|
||||
this._userId = userId;
|
||||
this.userId = userId;
|
||||
this._devices = devices;
|
||||
this.transactionId = transactionId;
|
||||
this._deviceId = deviceId;
|
||||
}
|
||||
|
||||
isToDevices(devices) {
|
||||
if (devices.length === this._devices.length) {
|
||||
for (const device of devices) {
|
||||
const d = this._devices.find(d => d.deviceId === device.deviceId);
|
||||
if (!d) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
get deviceId() {
|
||||
return this._deviceId;
|
||||
}
|
||||
|
||||
static getEventType(event) {
|
||||
return event.getType();
|
||||
}
|
||||
@@ -82,10 +101,12 @@ export default class ToDeviceChannel {
|
||||
}
|
||||
const content = event.getContent();
|
||||
if (!content) {
|
||||
logger.warn("ToDeviceChannel.validateEvent: invalid: no content");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!content.transaction_id) {
|
||||
logger.warn("ToDeviceChannel.validateEvent: invalid: no transaction_id");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -93,6 +114,7 @@ export default class ToDeviceChannel {
|
||||
|
||||
if (type === REQUEST_TYPE) {
|
||||
if (!Number.isFinite(content.timestamp)) {
|
||||
logger.warn("ToDeviceChannel.validateEvent: invalid: no timestamp");
|
||||
return false;
|
||||
}
|
||||
if (event.getSender() === client.getUserId() &&
|
||||
@@ -100,19 +122,19 @@ export default class ToDeviceChannel {
|
||||
) {
|
||||
// ignore requests from ourselves, because it doesn't make sense for a
|
||||
// device to verify itself
|
||||
logger.warn("ToDeviceChannel.validateEvent: invalid: from own device");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return VerificationRequest.validateEvent(
|
||||
type, event, ToDeviceChannel.getTimestamp(event), client);
|
||||
return VerificationRequest.validateEvent(type, event, client);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MatrixEvent} event the event to get the timestamp of
|
||||
* @return {number} the timestamp when the event was sent
|
||||
*/
|
||||
static getTimestamp(event) {
|
||||
getTimestamp(event) {
|
||||
const content = event.getContent();
|
||||
return content && content.timestamp;
|
||||
}
|
||||
@@ -121,12 +143,13 @@ export default class ToDeviceChannel {
|
||||
* Changes the state of the channel, request, and verifier in response to a key verification event.
|
||||
* @param {MatrixEvent} event to handle
|
||||
* @param {VerificationRequest} request the request to forward handling to
|
||||
* @param {bool} isLiveEvent whether this is an even received through sync or not
|
||||
* @returns {Promise} a promise that resolves when any requests as an anwser to the passed-in event are sent.
|
||||
*/
|
||||
async handleEvent(event, request) {
|
||||
async handleEvent(event, request, isLiveEvent) {
|
||||
const type = event.getType();
|
||||
const content = event.getContent();
|
||||
if (type === REQUEST_TYPE || type === START_TYPE) {
|
||||
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
|
||||
if (!this.transactionId) {
|
||||
this.transactionId = content.transaction_id;
|
||||
}
|
||||
@@ -145,14 +168,17 @@ export default class ToDeviceChannel {
|
||||
return this._sendToDevices(CANCEL_TYPE, cancelContent, [deviceId]);
|
||||
}
|
||||
}
|
||||
const wasStarted = request.phase === PHASE_STARTED ||
|
||||
request.phase === PHASE_READY;
|
||||
|
||||
const wasStarted = request.phase === PHASE_STARTED;
|
||||
await request.handleEvent(
|
||||
event.getType(), event, ToDeviceChannel.getTimestamp(event));
|
||||
const isStarted = request.phase === PHASE_STARTED;
|
||||
await request.handleEvent(event.getType(), event, isLiveEvent, false, false);
|
||||
|
||||
// the request has picked a start event, tell the other devices about it
|
||||
if (type === START_TYPE && !wasStarted && isStarted && this._deviceId) {
|
||||
const isStarted = request.phase === PHASE_STARTED ||
|
||||
request.phase === PHASE_READY;
|
||||
|
||||
const isAcceptingEvent = type === START_TYPE || type === READY_TYPE;
|
||||
// the request has picked a ready or start event, tell the other devices about it
|
||||
if (isAcceptingEvent && !wasStarted && isStarted && this._deviceId) {
|
||||
const nonChosenDevices = this._devices.filter(d => d !== this._deviceId);
|
||||
if (nonChosenDevices.length) {
|
||||
const message = this.completeContent({
|
||||
@@ -188,7 +214,7 @@ export default class ToDeviceChannel {
|
||||
if (this.transactionId) {
|
||||
content.transaction_id = this.transactionId;
|
||||
}
|
||||
if (type === REQUEST_TYPE || type === START_TYPE) {
|
||||
if (type === REQUEST_TYPE || type === READY_TYPE || type === START_TYPE) {
|
||||
content.from_device = this._client.getDeviceId();
|
||||
}
|
||||
if (type === REQUEST_TYPE) {
|
||||
@@ -218,12 +244,28 @@ export default class ToDeviceChannel {
|
||||
* @param {object} content
|
||||
* @returns {Promise} the promise of the request
|
||||
*/
|
||||
sendCompleted(type, content) {
|
||||
async sendCompleted(type, content) {
|
||||
let result;
|
||||
if (type === REQUEST_TYPE) {
|
||||
return this._sendToDevices(type, content, this._devices);
|
||||
result = await this._sendToDevices(type, content, this._devices);
|
||||
} else {
|
||||
return this._sendToDevices(type, content, [this._deviceId]);
|
||||
result = await this._sendToDevices(type, content, [this._deviceId]);
|
||||
}
|
||||
// the VerificationRequest state machine requires remote echos of the event
|
||||
// the client sends itself, so we fake this for to_device messages
|
||||
const remoteEchoEvent = new MatrixEvent({
|
||||
sender: this._client.getUserId(),
|
||||
content,
|
||||
type,
|
||||
});
|
||||
await this._request.handleEvent(
|
||||
type,
|
||||
remoteEchoEvent,
|
||||
/*isLiveEvent=*/true,
|
||||
/*isRemoteEcho=*/true,
|
||||
/*isSentByUs=*/true,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
_sendToDevices(type, content, devices) {
|
||||
@@ -233,7 +275,7 @@ export default class ToDeviceChannel {
|
||||
msgMap[deviceId] = content;
|
||||
}
|
||||
|
||||
return this._client.sendToDevice(type, {[this._userId]: msgMap});
|
||||
return this._client.sendToDevice(type, {[this.userId]: msgMap});
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -247,3 +289,71 @@ export default class ToDeviceChannel {
|
||||
return randomString(32);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class ToDeviceRequests {
|
||||
constructor() {
|
||||
this._requestsByUserId = new Map();
|
||||
}
|
||||
|
||||
getRequest(event) {
|
||||
return this.getRequestBySenderAndTxnId(
|
||||
event.getSender(),
|
||||
ToDeviceChannel.getTransactionId(event),
|
||||
);
|
||||
}
|
||||
|
||||
getRequestByChannel(channel) {
|
||||
return this.getRequestBySenderAndTxnId(channel.userId, channel.transactionId);
|
||||
}
|
||||
|
||||
getRequestBySenderAndTxnId(sender, txnId) {
|
||||
const requestsByTxnId = this._requestsByUserId.get(sender);
|
||||
if (requestsByTxnId) {
|
||||
return requestsByTxnId.get(txnId);
|
||||
}
|
||||
}
|
||||
|
||||
setRequest(event, request) {
|
||||
this.setRequestBySenderAndTxnId(
|
||||
event.getSender(),
|
||||
ToDeviceChannel.getTransactionId(event),
|
||||
request,
|
||||
);
|
||||
}
|
||||
|
||||
setRequestByChannel(channel, request) {
|
||||
this.setRequestBySenderAndTxnId(channel.userId, channel.transactionId, request);
|
||||
}
|
||||
|
||||
setRequestBySenderAndTxnId(sender, txnId, request) {
|
||||
let requestsByTxnId = this._requestsByUserId.get(sender);
|
||||
if (!requestsByTxnId) {
|
||||
requestsByTxnId = new Map();
|
||||
this._requestsByUserId.set(sender, requestsByTxnId);
|
||||
}
|
||||
requestsByTxnId.set(txnId, request);
|
||||
}
|
||||
|
||||
removeRequest(event) {
|
||||
const userId = event.getSender();
|
||||
const requestsByTxnId = this._requestsByUserId.get(userId);
|
||||
if (requestsByTxnId) {
|
||||
requestsByTxnId.delete(ToDeviceChannel.getTransactionId(event));
|
||||
if (requestsByTxnId.size === 0) {
|
||||
this._requestsByUserId.delete(userId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
findRequestInProgress(userId, devices) {
|
||||
const requestsByTxnId = this._requestsByUserId.get(userId);
|
||||
if (requestsByTxnId) {
|
||||
for (const request of requestsByTxnId.values()) {
|
||||
if (request.pending && request.channel.isToDevices(devices)) {
|
||||
return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user