Compare commits
801 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f2cad8f62 | |||
| 8924865c9c | |||
| 60950044f2 | |||
| 4c6c1d2107 | |||
| 2e3b6fba7d | |||
| de51291166 | |||
| d84a852ae9 | |||
| 9245b2a89a | |||
| d39e3141fc | |||
| d4327d4cfc | |||
| d9e5a17ab0 | |||
| b5c61af472 | |||
| fd705b7d5e | |||
| 8e53982bcd | |||
| ca4e738fff | |||
| 594e9b9e2d | |||
| 15b87e9dc1 | |||
| fa3583234c | |||
| 9679db6ddc | |||
| fdf48d1f30 | |||
| 40d13d9b59 | |||
| 4ab6ae7f30 | |||
| c8dd6bfd26 | |||
| 0a76f75c22 | |||
| 795c1225dd | |||
| b982d36303 | |||
| a80aa4c2ad | |||
| 27d9cf04de | |||
| e4779163b8 | |||
| 2cc899338a | |||
| 8dc56ec332 | |||
| 1d5ee22dd2 | |||
| 59917f45e3 | |||
| 35247fac2a | |||
| e24fb8b471 | |||
| 795900cf39 | |||
| bca7f41ca9 | |||
| 7f503eb71c | |||
| a26dc3179a | |||
| aa1a64628f | |||
| 0e66640b9f | |||
| 3f41e5071b | |||
| 9eb17e757c | |||
| 804bd221b2 | |||
| e20b1efae9 | |||
| e65915e159 | |||
| 4800e80492 | |||
| 5d0ff961b2 | |||
| 270350cd34 | |||
| ae2391791d | |||
| 24592adbba | |||
| efe659910f | |||
| 08babb6d6c | |||
| 50bd408d48 | |||
| ce4d53a88c | |||
| 7e9baf2707 | |||
| 3073883076 | |||
| 7ec5a5ad1a | |||
| 0422bae924 | |||
| 27ecab8574 | |||
| 5ca66a6985 | |||
| 2e387436cf | |||
| 591f031246 | |||
| dedb1eb745 | |||
| c40edcf2fc | |||
| 6509e72a74 | |||
| 38fec7f2b3 | |||
| 95243003c4 | |||
| 11fcf5c42f | |||
| b27f1b0e34 | |||
| b67cd4ddd2 | |||
| 44cc1cef71 | |||
| 34bec59389 | |||
| cb95f576a5 | |||
| 5c530cf9ee | |||
| 30a78bb1d6 | |||
| 2077ea0ddf | |||
| e757d605f5 | |||
| 61a5293af5 | |||
| 6e83a4bbca | |||
| 5c14910126 | |||
| 8ed1e37cef | |||
| 5fd004bae5 | |||
| 7de002b128 | |||
| f60dc7ed78 | |||
| 78d7f6c10b | |||
| fa25ca4475 | |||
| c9db63509f | |||
| ac0df5dea9 | |||
| d175c47a05 | |||
| 959e8450af | |||
| dd0642cd59 | |||
| 6a7da5a8b6 | |||
| 7cab7cadc9 | |||
| 728d80ed06 | |||
| 0c1d33d43f | |||
| 8f99180c99 | |||
| 0682292b91 | |||
| 404cc410cc | |||
| 17cc4fcb81 | |||
| 93f49265a6 | |||
| b1c8c64205 | |||
| 425a07d670 | |||
| 4262f1d3b0 | |||
| b5560d3cb6 | |||
| fc54c63a4c | |||
| e7a24d5e68 | |||
| b5c9473424 | |||
| 59d7b53242 | |||
| 59a7199202 | |||
| d1313b8614 | |||
| 4e8ce4cb5d | |||
| c85fe6bc21 | |||
| 3338ecf62a | |||
| 1c6a67d864 | |||
| e737200fbe | |||
| 41b3b0651f | |||
| 1cabc0cac9 | |||
| e0ee03fa6f | |||
| 4ec7aff301 | |||
| f5ff91935f | |||
| 7519bec9a3 | |||
| bf7070b8f2 | |||
| 39fc33d37a | |||
| d81a6e6872 | |||
| 2afc0c7661 | |||
| f349811020 | |||
| 0ab12fe969 | |||
| c4ac0d0570 | |||
| d039a39d84 | |||
| 1c008f549c | |||
| bd0ac703a0 | |||
| 6d2e9cfc02 | |||
| 661f182382 | |||
| 9ea4835e3e | |||
| 2602c36ad0 | |||
| d858940342 | |||
| 883183324f | |||
| 7d023ebdb9 | |||
| 473e49252e | |||
| d96c9f85a1 | |||
| 279ce0bba0 | |||
| da5ef42719 | |||
| a4eae1053c | |||
| f7039d9a8d | |||
| 723fdeaa06 | |||
| 19d513e3c0 | |||
| 23ac00c8ec | |||
| 4019ebf121 | |||
| 220ccfb52b | |||
| 9a838abd67 | |||
| 17d23eb9e5 | |||
| 8ea0035cd0 | |||
| 06b9c71dbc | |||
| 6a8ac62a51 | |||
| 1e894269c8 | |||
| 27c6f30e0f | |||
| bc48674f9f | |||
| e5f0f64405 | |||
| 09f4b07fb7 | |||
| 2ffac286ed | |||
| 83b48fb53c | |||
| f4137c6bba | |||
| e16b7f9c44 | |||
| 45953a268c | |||
| 84039ad7aa | |||
| 137fa9619f | |||
| e3d24f5c31 | |||
| 02c765f903 | |||
| fc6ff2c78a | |||
| bcdcdeb259 | |||
| 1d8f01ef11 | |||
| b58d88e0c3 | |||
| c8ca93c924 | |||
| d644af7be9 | |||
| ff2079da91 | |||
| 646f18ae18 | |||
| 2b8d4a21a4 | |||
| 78badd9af8 | |||
| 58aef51770 | |||
| 8fe1eda169 | |||
| 84066d4a76 | |||
| e1c220e2f7 | |||
| 798656dac5 | |||
| 721c459577 | |||
| 23173c4a1e | |||
| 4a8c5ebab0 | |||
| e29508938b | |||
| a357536ade | |||
| f3be27921c | |||
| 42c4cf2a30 | |||
| c5bece2d58 | |||
| 4662ca2e32 | |||
| 5a86b067e4 | |||
| 9a5345ec77 | |||
| 7c3e751d6e | |||
| 3070c98d26 | |||
| 95e906e0dc | |||
| 2e3d30d7b4 | |||
| 5b0457dad0 | |||
| a183584541 | |||
| 562bb5aee3 | |||
| dea3e4adf4 | |||
| 5d5d5bb141 | |||
| c58cf71be1 | |||
| af4b00195b | |||
| 41529a6bff | |||
| 300b03bd9e | |||
| a5b195efc7 | |||
| 692f9baa0e | |||
| 6b24d91ed9 | |||
| 24ce4881c7 | |||
| 849934b180 | |||
| 428b28a985 | |||
| 95145fae8f | |||
| ae894e0ff6 | |||
| 86d95518be | |||
| c8e459bc55 | |||
| 4d431b7c9e | |||
| 890e6cbc73 | |||
| e98960f30b | |||
| c500c06e4b | |||
| 3ac3be501f | |||
| 3573614640 | |||
| a60f60bd7d | |||
| a4980e8a04 | |||
| b628e6286a | |||
| fb47abcc17 | |||
| c2756a9a92 | |||
| 2d6882c495 | |||
| 51f3d90224 | |||
| 1a140ecc2f | |||
| f603696ff4 | |||
| ffd2843b0a | |||
| 618a58ba34 | |||
| 126ac3059b | |||
| 8af18a4df7 | |||
| 7790c3db8f | |||
| e3f4c1849c | |||
| 848156213b | |||
| 23e953d9cf | |||
| 464e181f66 | |||
| 7bd0e4975b | |||
| 127d4c225b | |||
| 9617d9aac9 | |||
| e828828ace | |||
| 3e9b0a8e7f | |||
| aff1e1d0a8 | |||
| 98f69aed41 | |||
| acfd0cdb07 | |||
| fc60593801 | |||
| 14226c0778 | |||
| 70ffc43ce0 | |||
| 9810a2f630 | |||
| 35f5117800 | |||
| d35cf56dc8 | |||
| 083cebe735 | |||
| faaf3f7a29 | |||
| 34cdf31cc5 | |||
| 6c7dbb814b | |||
| 217543ef38 | |||
| f57447527d | |||
| 8dbc7c38e5 | |||
| 5a069a8721 | |||
| 89efcee337 | |||
| 22daf0d81e | |||
| 53fec7a87e | |||
| adf8905d9f | |||
| 7b3dfe2f27 | |||
| 73c104cac1 | |||
| d86c05efb3 | |||
| cc236a8765 | |||
| 8b5bb7d8c5 | |||
| 2195da1cd8 | |||
| 65843f89dc | |||
| 8b56546565 | |||
| 8c4acf54e0 | |||
| c652762255 | |||
| 4bab678e46 | |||
| 81b127b6e7 | |||
| 269cfc3d34 | |||
| 987d87cd5d | |||
| 0de4a21320 | |||
| 6872cc717b | |||
| 5c4e46e908 | |||
| a2bfa08e09 | |||
| 977e29c3af | |||
| a2f7297941 | |||
| 6fa365935f | |||
| 39628a308b | |||
| 54391040a4 | |||
| 7a418ae09e | |||
| deff66ac42 | |||
| 2995cebd57 | |||
| ea4befabd9 | |||
| 6760f81498 | |||
| b3d1e8687e | |||
| 95c8708995 | |||
| 8d39821a1f | |||
| 2bcbf1eca4 | |||
| a5f06f772f | |||
| 2b389b920d | |||
| 298c260c5f | |||
| 72614e4252 | |||
| 8a71cec81a | |||
| a57c6159bd | |||
| 5f10f4301c | |||
| 176181bdcf | |||
| edea5e1c51 | |||
| b3941ca254 | |||
| c3c6428717 | |||
| de90da4adc | |||
| 002531349e | |||
| e38bfc64f4 | |||
| 93e1967119 | |||
| aee40977a3 | |||
| 9fe23227af | |||
| ce93869915 | |||
| 202c20feda | |||
| c307690c2e | |||
| 552a12eeed | |||
| c2ad298963 | |||
| d908d0f817 | |||
| 9edc876160 | |||
| 398edbbe0c | |||
| 89b56b5af8 | |||
| a57f63d614 | |||
| 74dd0a00d3 | |||
| b97e3d7bae | |||
| c3eb4d8106 | |||
| ea49a35b43 | |||
| a99e47c310 | |||
| 69fbe65ac4 | |||
| aaa15c768c | |||
| 58185e08e8 | |||
| 89c9e31140 | |||
| 1bd15b9fdd | |||
| 23126c4e48 | |||
| 6f5352b9a9 | |||
| eb16737d3b | |||
| 56309ae12c | |||
| 9fe0717cee | |||
| 7f23cbbeb5 | |||
| 3153a81cd2 | |||
| c3e593d998 | |||
| c2a386b889 | |||
| 317a141e07 | |||
| 3990e50ca6 | |||
| 90ea0229f2 | |||
| a42af5da69 | |||
| f63a01a85b | |||
| 27e1fb9a35 | |||
| c21517c61e | |||
| f626f2b24e | |||
| 37a7f69e03 | |||
| 38cf771f1f | |||
| 6d0b73cb3d | |||
| f96437a242 | |||
| 150862ec0c | |||
| 6db7eb0694 | |||
| 84c0311d80 | |||
| de097d3ca0 | |||
| 8aedc3077d | |||
| 0f26e7e3bc | |||
| 91db502cfe | |||
| 43aea6e482 | |||
| e778f7d72d | |||
| 94248523b3 | |||
| fd8377bce2 | |||
| 9e609a0fdf | |||
| 16a115d27e | |||
| 8167f5e9de | |||
| 5876c89858 | |||
| 5040be042f | |||
| ad2d3d2037 | |||
| 09f009ebd7 | |||
| 664d8c239c | |||
| 97ad060d4b | |||
| f4de3580b6 | |||
| 0fc5134563 | |||
| b0de9d1809 | |||
| 75fa7e97f9 | |||
| d21e8213b5 | |||
| 181c2a92de | |||
| 08d76f2ff4 | |||
| 5b758b8344 | |||
| 499f2796ba | |||
| df0444faa5 | |||
| b4c1b26f96 | |||
| 0245782cf4 | |||
| 87d0102663 | |||
| 6ee8b07cfe | |||
| 344631b4ee | |||
| f3e03c66a5 | |||
| d4e31f07a1 | |||
| d4de877e09 | |||
| 9b8e11aab9 | |||
| a0abffd026 | |||
| 4e99278eac | |||
| cdb8b5c1e9 | |||
| bf42e1a39f | |||
| 5883396106 | |||
| c6b0a19171 | |||
| 7ee0430054 | |||
| 36ca784690 | |||
| 2449bd27c1 | |||
| 29bd38734f | |||
| 6c07620a26 | |||
| 3e3894b573 | |||
| 0a26195472 | |||
| 0dc232b268 | |||
| c4465e7979 | |||
| 41f04d4f5d | |||
| 15d7deddb8 | |||
| 18e597aa79 | |||
| 407f9a3da8 | |||
| 82c3a795ff | |||
| ccda5c7260 | |||
| d706140a8f | |||
| 8351858be7 | |||
| 7cb25361b2 | |||
| 42a4ad60e8 | |||
| 9a325a4505 | |||
| fe572017b1 | |||
| c4ed5b6cda | |||
| 0d2f8c6d0f | |||
| fa1a40543c | |||
| 7637e79f2c | |||
| d0a5b86ff3 | |||
| 707b4c1185 | |||
| 9234ac96e1 | |||
| 2437a92998 | |||
| 947fa08dae | |||
| 8f4ac3da7f | |||
| 01bcbaf063 | |||
| 4770dc636a | |||
| 9294280dc1 | |||
| fba3298162 | |||
| ac2469d270 | |||
| db553b2040 | |||
| eeb6a811c0 | |||
| 528483ef0e | |||
| 72168ce084 | |||
| 6c85d3e28f | |||
| d5a853f3da | |||
| 8a2d6a4450 | |||
| c15ffb989a | |||
| 2b78f05aad | |||
| 1f0a96e31d | |||
| 6593cce778 | |||
| d7bcf42a2b | |||
| 18b655f829 | |||
| e2e70d6583 | |||
| c305b5052b | |||
| 6f4d2022fd | |||
| ef5201cf35 | |||
| 7bcdc2a3b6 | |||
| 7eeff64059 | |||
| 9c4229dc57 | |||
| 6c4e2fa508 | |||
| d5cd608045 | |||
| d83fc971ce | |||
| d96142b8cb | |||
| cd5d5da06a | |||
| 87bcba3561 | |||
| 81e9a7cefc | |||
| 3ddb2199d2 | |||
| 4abab73462 | |||
| 17fd85d687 | |||
| 279e88d9f9 | |||
| d016ce1848 | |||
| 591388d13e | |||
| a3b4cab22e | |||
| ffdb9c4a79 | |||
| cb8d5ce8fb | |||
| c10120602a | |||
| 47690bd268 | |||
| 807432b31f | |||
| 69d2a00759 | |||
| be01ee2de0 | |||
| 408fe5da4b | |||
| 28a7831ffd | |||
| 2bf8c99dfe | |||
| 77f0676a58 | |||
| e7b2a54e46 | |||
| 33e1601004 | |||
| 26ec0c6368 | |||
| 9f0fbcccf6 | |||
| 01ba94c670 | |||
| e431ba0bf5 | |||
| a3bb8a0d74 | |||
| 3245fbb1c9 | |||
| f4517c150c | |||
| e37229554b | |||
| df9da7539a | |||
| 1787d2ebe6 | |||
| faadb4953b | |||
| 021193087d | |||
| 0ac2b84c02 | |||
| 230b2a229f | |||
| ed1f12ce37 | |||
| 3f83941d57 | |||
| 91d7a8329e | |||
| 1a40491c0b | |||
| 79e661d1d9 | |||
| dce06d31aa | |||
| 3472614649 | |||
| 7ecd4a035f | |||
| 2ce0765206 | |||
| 108f6d90c9 | |||
| 7ceda2f39c | |||
| e00e94c6c3 | |||
| a71c7b2964 | |||
| 30c07b4e08 | |||
| d9fbc18777 | |||
| a58ace70a7 | |||
| 359c5280d7 | |||
| a07767d417 | |||
| 5a58fdff98 | |||
| 5058f09111 | |||
| 21b0afe72c | |||
| a726ebab39 | |||
| 2b124d98bc | |||
| 4634efc092 | |||
| 117ebeaf4b | |||
| 27f918e52d | |||
| 9facd86d81 | |||
| 7f2df68d62 | |||
| a6fa9f99fd | |||
| 7e95d85f17 | |||
| 57b65ec8c4 | |||
| 2d6fff7927 | |||
| 792623f53d | |||
| 6e67585bf6 | |||
| 5471c07244 | |||
| 0b04f7960b | |||
| 623f91733e | |||
| da3734ffc7 | |||
| 7128505768 | |||
| 6a96368048 | |||
| 8c9c843bfc | |||
| 094b2f90d6 | |||
| 2cbdca1f58 | |||
| d4fe2fe0a2 | |||
| 14db34beee | |||
| 7aea6160c3 | |||
| ca88539ec4 | |||
| 670755bfce | |||
| 46c1657643 | |||
| 9ac1417292 | |||
| de94b903d6 | |||
| 2f28976694 | |||
| 8ff8ea1342 | |||
| a1edef0ed5 | |||
| ee51ed78be | |||
| 2729f01e0f | |||
| e6730a7007 | |||
| 6fd852d573 | |||
| 7f2b268a59 | |||
| bb9adea5de | |||
| b1ae5534a1 | |||
| 9214f01185 | |||
| c35f73473e | |||
| bf54b17a2f | |||
| 4ce26f4fa0 | |||
| cdcbcdfab3 | |||
| 7a2d5c30db | |||
| a7bc1a95d3 | |||
| 4fa58bfaac | |||
| 7c92d91c04 | |||
| e612326714 | |||
| 9ef784d665 | |||
| 2481fbbd27 | |||
| a9d645cbcd | |||
| 578c927e58 | |||
| 24baf1fe0f | |||
| 861c07d5ce | |||
| 451d902604 | |||
| c3f00c96f8 | |||
| e50cf39a17 | |||
| 3f1439fe28 | |||
| 3d6872607e | |||
| fe33430e9b | |||
| b22324b305 | |||
| 037d62b165 | |||
| 8c39db002b | |||
| e27b6fb51e | |||
| e4f94cbfec | |||
| 807435c043 | |||
| 71f2a042c2 | |||
| 2e8fc3e232 | |||
| d273786d83 | |||
| 7ddc785a9a | |||
| 3e23affc9e | |||
| f2163164bf | |||
| 44dfbd2fa6 | |||
| 2f99d0de59 | |||
| 7a72949613 | |||
| b0241e51a3 | |||
| c5ea4fde35 | |||
| cc4ae3db1e | |||
| a5c5f5a7b1 | |||
| 7c46953805 | |||
| 2d83c40626 | |||
| d75101d042 | |||
| 04bb65f43e | |||
| c1ffed4fc9 | |||
| 0542e3d83d | |||
| 2d955027e1 | |||
| 38166135dc | |||
| 5bebe1d434 | |||
| a2a87b9fff | |||
| 497b973eb5 | |||
| de1988265d | |||
| 6cced25ae1 | |||
| 4e40c13b81 | |||
| bf152df322 | |||
| 3315cf5bc6 | |||
| 204279c575 | |||
| 83806b42e9 | |||
| fa0a22b090 | |||
| bce7fe0217 | |||
| 1fd21ee206 | |||
| 8a4a4140b3 | |||
| 62943f055d | |||
| ea149ebd8e | |||
| 32737a5517 | |||
| 1691a26163 | |||
| 51012e632e | |||
| 5d76fd9aac | |||
| c25f4c0642 | |||
| 9e48b7172b | |||
| 68125f5de6 | |||
| b602d3007d | |||
| 41cfbaf520 | |||
| 8206394918 | |||
| ca85564a9f | |||
| 7d9a699d62 | |||
| a38efc0f29 | |||
| 048a2000e7 | |||
| 18b444aac5 | |||
| a7a9ac24ed | |||
| b2ccb61864 | |||
| ac264918b8 | |||
| 8e19c583c6 | |||
| 740a5af068 | |||
| 8c3855221c | |||
| c2f1e4de64 | |||
| 8a7c53c00d | |||
| c1ae183795 | |||
| eea00301ff | |||
| 85522ac35a | |||
| 9b5f95672b | |||
| ffc5204109 | |||
| a607d70371 | |||
| 1fcb68c59f | |||
| 9bceb2f539 | |||
| 7003ea2d23 | |||
| 18ccd30c8c | |||
| eb19c19e36 | |||
| df2bcf6f1f | |||
| 3ee06be87b | |||
| 3a07a17e9d | |||
| 27eeeb8db6 | |||
| 6ded76a5a7 | |||
| 05a41d3b4d | |||
| 58d79ca9c6 | |||
| 4ee245dcce | |||
| 8daa12ac56 | |||
| 4134ba969a | |||
| a8f24da3ba | |||
| 390a1aa12c | |||
| b16724841d | |||
| ec81a5e539 | |||
| 949305da72 | |||
| 559306a33c | |||
| 24d2aa8078 | |||
| e70929317a | |||
| 62eeb3707f | |||
| 3fa06eeb99 | |||
| c0e6279837 | |||
| e7c70854ab | |||
| dcc3d6e755 | |||
| 2338d3e8fd | |||
| b83b9dc59d | |||
| 68822861d5 | |||
| b1e7bc77a4 | |||
| eb5949dbc2 | |||
| 7943baee49 | |||
| 7abdeed449 | |||
| eeebb43e32 | |||
| 5f49dab1fa | |||
| 6cacf83661 | |||
| 599c1ba98f | |||
| c2ec69cf44 | |||
| 32bdcede0c | |||
| 9af48920f6 | |||
| a3441429da | |||
| 9f957655d0 | |||
| 2663a17065 | |||
| 7124235d1b | |||
| babbdb4b90 | |||
| 18f6cbc23a | |||
| 583dbb07a5 | |||
| 25207a1586 | |||
| 283cf0d782 | |||
| 98d36d0ef0 | |||
| f33298b1a6 | |||
| 11aa306de2 | |||
| 669a3f22d2 | |||
| ff5f638b60 | |||
| 2a0c6c6474 | |||
| f447c55fcb | |||
| 84fc662614 | |||
| c57f076375 | |||
| 4561b94f33 | |||
| 9bd8699e18 | |||
| 5ef9a7b924 | |||
| bd56c52b37 | |||
| 1f25c4cf4b | |||
| 3f1a40a7d1 | |||
| b092ed0a82 | |||
| cd9252cc3d | |||
| 8b13602b3b | |||
| 92a43e7685 | |||
| 262a61afc9 | |||
| 1016519bb6 | |||
| 4dbe785bd7 | |||
| 676d547161 | |||
| 6a670163d3 | |||
| b8c4d1d5fa | |||
| 9e738f45ef | |||
| 8e8ac8c5ac | |||
| 4a7b3a103c | |||
| fc077bcd6b | |||
| c0c02baffc | |||
| 1174ccfc89 | |||
| 733689870e | |||
| 255451b8c7 | |||
| d4087a1aae | |||
| f07ac5d679 | |||
| 8b77b4171a | |||
| ea427cf366 | |||
| 15191d0230 | |||
| 5bd3c49afc | |||
| 765487dd9f | |||
| 03e53e991b | |||
| c3373f796b | |||
| 311e41ee0d | |||
| f8b5fceeb1 | |||
| 97b1bb6004 | |||
| 331cb02266 | |||
| 7751605e37 | |||
| a0eaa9c364 | |||
| 241d456a81 | |||
| 3e5b6bb460 | |||
| 5868c72662 | |||
| 4c184a30a2 | |||
| e4977d1d2a | |||
| ac069152b9 | |||
| 82827542b7 | |||
| 20a8e8e49b | |||
| 098cc1f9f8 | |||
| a3c46c6144 | |||
| f35fbdf8b0 | |||
| 442464add6 | |||
| abe40dff11 | |||
| b93eb0e318 | |||
| e6b67e5fa7 | |||
| 22ba253103 | |||
| a9fd63fd4b | |||
| 60a43439e5 | |||
| b6d7939685 | |||
| 4df0a839aa | |||
| e3cb3566bf | |||
| 9f34615869 | |||
| 05503b28b7 | |||
| 49e913865d | |||
| 4675a72e6b | |||
| d5f66631c1 | |||
| 81baca2f92 | |||
| 6e5870bd2b | |||
| 6df1f12b45 | |||
| 5abac19b72 | |||
| 62e959a94d | |||
| 54871f2af9 | |||
| 6a323525b5 | |||
| 1d00f79675 | |||
| 7201749280 | |||
| 5175cd8ddb | |||
| 21b33f4e61 | |||
| 9f34b371be | |||
| 587614cdd7 | |||
| db38bf1276 | |||
| 761071dac5 | |||
| 8f017e7b27 | |||
| b1864887aa | |||
| 9cb86596d8 | |||
| 8ee6c3bdc8 | |||
| 16f4021800 | |||
| 53876ea6e8 |
@@ -0,0 +1,103 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
style:
|
||||
name: Check style
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Cargo fmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
clippy:
|
||||
name: Run clippy
|
||||
needs: [style]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout the repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: clippy
|
||||
args: --all-targets -- -D warnings
|
||||
|
||||
test:
|
||||
name: ${{ matrix.name }}
|
||||
needs: [clippy]
|
||||
|
||||
runs-on: ${{ matrix.os || 'ubuntu-latest' }}
|
||||
strategy:
|
||||
matrix:
|
||||
name:
|
||||
- linux / stable
|
||||
- linux / beta
|
||||
- macOS / stable
|
||||
- windows / stable-x86_64-msvc
|
||||
|
||||
include:
|
||||
- name: linux / stable
|
||||
|
||||
- name: linux / beta
|
||||
rust: beta
|
||||
|
||||
- name: macOS / stable
|
||||
os: macOS-latest
|
||||
|
||||
- name: windows / stable-x86_64-msvc
|
||||
os: windows-latest
|
||||
target: x86_64-pc-windows-msvc
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Install rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.rust || 'stable' }}
|
||||
target: ${{ matrix.target }}
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: build
|
||||
|
||||
- name: Test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Code coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
jobs:
|
||||
code_coverage:
|
||||
name: Code Coverage
|
||||
runs-on: "ubuntu-latest"
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: Install tarpaulin
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-tarpaulin -f
|
||||
|
||||
- name: Run tarpaulin
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: tarpaulin
|
||||
args: --ignore-config --exclude-files "matrix_sdk/examples/*,matrix_sdk_common,matrix_sdk_test" --out Xml
|
||||
|
||||
- name: Upload to codecov.io
|
||||
uses: codecov/codecov-action@v1
|
||||
@@ -1,2 +1,4 @@
|
||||
Cargo.lock
|
||||
target
|
||||
master.zip
|
||||
emsdk-*
|
||||
+45
-9
@@ -6,17 +6,59 @@ addons:
|
||||
- libssl-dev
|
||||
|
||||
jobs:
|
||||
allow_failures:
|
||||
- os: osx
|
||||
name: macOS 10.15
|
||||
|
||||
include:
|
||||
- os: linux
|
||||
dist: bionic
|
||||
- stage: Format
|
||||
os: linux
|
||||
before_script:
|
||||
- rustup component add rustfmt
|
||||
script:
|
||||
- cargo fmt --all -- --check
|
||||
|
||||
- stage: Clippy
|
||||
os: linux
|
||||
before_script:
|
||||
- rustup component add clippy
|
||||
script:
|
||||
- cargo clippy --all-targets -- -D warnings
|
||||
|
||||
- stage: Test
|
||||
os: linux
|
||||
|
||||
- os: windows
|
||||
script:
|
||||
- cd matrix_sdk
|
||||
- cargo test --no-default-features --features "messages, native-tls"
|
||||
- cd ../matrix_sdk_base
|
||||
- cargo test --no-default-features --features "messages"
|
||||
|
||||
- os: osx
|
||||
|
||||
- os: linux
|
||||
name: native-tls build
|
||||
script:
|
||||
- cd matrix_sdk
|
||||
- cargo build --no-default-features --features "native-tls"
|
||||
|
||||
- os: linux
|
||||
name: rustls-tls build
|
||||
script:
|
||||
- cd matrix_sdk
|
||||
- cargo build --no-default-features --features "rustls-tls"
|
||||
|
||||
- os: osx
|
||||
name: macOS 10.15
|
||||
osx_image: xcode12
|
||||
|
||||
- os: linux
|
||||
name: Coverage
|
||||
before_script:
|
||||
- cargo install cargo-tarpaulin
|
||||
script:
|
||||
- cargo tarpaulin --out Xml
|
||||
- cargo tarpaulin --ignore-config --exclude-files "matrix_sdk/examples/*,matrix_sdk_common,matrix_sdk_test" --out Xml
|
||||
after_success:
|
||||
- bash <(curl -s https://codecov.io/bash)
|
||||
|
||||
@@ -42,12 +84,6 @@ jobs:
|
||||
cd matrix_sdk_base
|
||||
cargo test --target wasm32-unknown-unknown --no-default-features
|
||||
|
||||
|
||||
|
||||
before_script:
|
||||
- rustup component add rustfmt
|
||||
|
||||
script:
|
||||
- cargo fmt --all -- --check
|
||||
- cargo build
|
||||
- cargo test
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
[](https://travis-ci.org/matrix-org/matrix-rust-sdk)
|
||||

|
||||
[](https://codecov.io/gh/matrix-org/matrix-rust-sdk)
|
||||
[](https://opensource.org/licenses/Apache-2.0)
|
||||
[](https://matrix.to/#/#matrix-rust-sdk:matrix.org)
|
||||
|
||||
+54
-24
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk"]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
description = "A high level Matrix client-server library."
|
||||
edition = "2018"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
@@ -8,45 +8,75 @@ license = "Apache-2.0"
|
||||
name = "matrix-sdk"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["docs"]
|
||||
rustdoc-args = ["--cfg", "feature=\"docs\""]
|
||||
|
||||
[features]
|
||||
default = ["encryption", "sqlite-cryptostore"]
|
||||
default = ["encryption", "sqlite_cryptostore", "messages", "native-tls"]
|
||||
|
||||
messages = ["matrix-sdk-base/messages"]
|
||||
encryption = ["matrix-sdk-base/encryption"]
|
||||
sqlite-cryptostore = ["matrix-sdk-base/sqlite-cryptostore"]
|
||||
encryption = ["matrix-sdk-base/encryption", "dashmap"]
|
||||
sqlite_cryptostore = ["matrix-sdk-base/sqlite_cryptostore"]
|
||||
unstable-synapse-quirks = ["matrix-sdk-base/unstable-synapse-quirks"]
|
||||
native-tls = ["reqwest/native-tls"]
|
||||
rustls-tls = ["reqwest/rustls-tls"]
|
||||
socks = ["reqwest/socks"]
|
||||
|
||||
docs = ["encryption", "sqlite_cryptostore", "messages"]
|
||||
|
||||
[dependencies]
|
||||
http = "0.2.1"
|
||||
reqwest = "0.10.4"
|
||||
serde_json = "1.0.53"
|
||||
thiserror = "1.0.19"
|
||||
tracing = "0.1.14"
|
||||
url = "2.1.1"
|
||||
futures-timer = { version = "3.0.2", features = ["wasm-bindgen"] }
|
||||
dashmap = { version = "4.0.1", optional = true }
|
||||
http = "0.2.2"
|
||||
serde_json = "1.0.61"
|
||||
thiserror = "1.0.23"
|
||||
tracing = "0.1.22"
|
||||
url = "2.2.0"
|
||||
zeroize = "1.2.0"
|
||||
mime = "0.3.16"
|
||||
|
||||
|
||||
matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" }
|
||||
matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" }
|
||||
|
||||
[dependencies.matrix-sdk-base]
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
path = "../matrix_sdk_base"
|
||||
default_features = false
|
||||
|
||||
[dependencies.reqwest]
|
||||
version = "0.10.10"
|
||||
default_features = false
|
||||
|
||||
[dependencies.tracing-futures]
|
||||
version = "0.2.4"
|
||||
default-features = false
|
||||
features = ["std", "std-future"]
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
futures-timer = "3.0.2"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
|
||||
version = "0.2.24"
|
||||
default-features = false
|
||||
features = ["fs", "blocking"]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies.futures-timer]
|
||||
version = "3.0.2"
|
||||
features = ["wasm-bindgen"]
|
||||
|
||||
[dev-dependencies]
|
||||
async-trait = "0.1.31"
|
||||
dirs = "2.0.2"
|
||||
matrix-sdk-test = { version = "0.1.0", path = "../matrix_sdk_test" }
|
||||
tokio = { version = "0.2.21", features = ["rt-threaded", "macros"] }
|
||||
ruma-identifiers = { version = "0.16.1", features = ["rand"] }
|
||||
serde_json = "1.0.53"
|
||||
tracing-subscriber = "0.2.5"
|
||||
async-std = { version = "1.8.0", features = ["unstable"] }
|
||||
dirs = "3.0.1"
|
||||
matrix-sdk-test = { version = "0.2.0", path = "../matrix_sdk_test" }
|
||||
tokio = { version = "0.2.24", default-features = false, features = ["rt-threaded", "macros"] }
|
||||
serde_json = "1.0.61"
|
||||
tracing-subscriber = "0.2.15"
|
||||
tempfile = "3.1.0"
|
||||
mockito = "0.25.1"
|
||||
mockito = "0.28.0"
|
||||
lazy_static = "1.4.0"
|
||||
futures = "0.3.5"
|
||||
futures = "0.3.8"
|
||||
|
||||
[[example]]
|
||||
name = "emoji_verification"
|
||||
required-features = ["encryption"]
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
use std::{env, process::exit};
|
||||
use tokio::time::{delay_for, Duration};
|
||||
|
||||
use matrix_sdk::{
|
||||
self, async_trait,
|
||||
events::{room::member::MemberEventContent, StrippedStateEvent},
|
||||
Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
struct AutoJoinBot {
|
||||
client: Client,
|
||||
}
|
||||
|
||||
impl AutoJoinBot {
|
||||
pub fn new(client: Client) -> Self {
|
||||
Self { client }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventEmitter for AutoJoinBot {
|
||||
async fn on_stripped_state_member(
|
||||
&self,
|
||||
room: SyncRoom,
|
||||
room_member: &StrippedStateEvent<MemberEventContent>,
|
||||
_: Option<MemberEventContent>,
|
||||
) {
|
||||
if room_member.state_key != self.client.user_id().await.unwrap() {
|
||||
return;
|
||||
}
|
||||
|
||||
if let SyncRoom::Invited(room) = room {
|
||||
let room = room.read().await;
|
||||
println!("Autojoining room {}", room.room_id);
|
||||
let mut delay = 2;
|
||||
|
||||
while let Err(err) = self.client.join_room_by_id(&room.room_id).await {
|
||||
// retry autojoin due to synapse sending invites, before the
|
||||
// invited user can join for more information see
|
||||
// https://github.com/matrix-org/synapse/issues/4345
|
||||
eprintln!(
|
||||
"Failed to join room {} ({:?}), retrying in {}s",
|
||||
room.room_id, err, delay
|
||||
);
|
||||
|
||||
delay_for(Duration::from_secs(delay)).await;
|
||||
delay *= 2;
|
||||
|
||||
if delay > 3600 {
|
||||
eprintln!("Can't join room {} ({:?})", room.room_id, err);
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!("Successfully joined room {}", room.room_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn login_and_sync(
|
||||
homeserver_url: String,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(), matrix_sdk::Error> {
|
||||
let mut home = dirs::home_dir().expect("no home directory found");
|
||||
home.push("autojoin_bot");
|
||||
|
||||
let client_config = ClientConfig::new().store_path(home);
|
||||
|
||||
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
|
||||
let mut client = Client::new_with_config(homeserver_url, client_config).unwrap();
|
||||
|
||||
client
|
||||
.login(username, password, None, Some("autojoin bot"))
|
||||
.await?;
|
||||
|
||||
println!("logged in as {}", username);
|
||||
|
||||
client
|
||||
.add_event_emitter(Box::new(AutoJoinBot::new(client.clone())))
|
||||
.await;
|
||||
|
||||
client.sync(SyncSettings::default()).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), matrix_sdk::Error> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let (homeserver_url, username, password) =
|
||||
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
|
||||
(Some(a), Some(b), Some(c)) => (a, b, c),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Usage: {} <homeserver_url> <username> <password>",
|
||||
env::args().next().unwrap()
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
login_and_sync(homeserver_url, &username, &password).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
use std::{env, process::exit};
|
||||
|
||||
use matrix_sdk::{
|
||||
self,
|
||||
events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
|
||||
self, async_trait,
|
||||
events::{
|
||||
room::message::{MessageEventContent, TextMessageEventContent},
|
||||
AnyMessageEventContent, SyncMessageEvent,
|
||||
},
|
||||
Client, ClientConfig, EventEmitter, JsonStore, SyncRoom, SyncSettings,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
struct CommandBot {
|
||||
/// This clone of the `Client` will send requests to the server,
|
||||
/// while the other keeps us in sync with the server using `sync_forever`.
|
||||
/// while the other keeps us in sync with the server using `sync`.
|
||||
client: Client,
|
||||
}
|
||||
|
||||
@@ -19,11 +22,11 @@ impl CommandBot {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl EventEmitter for CommandBot {
|
||||
async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) {
|
||||
async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
|
||||
if let SyncRoom::Joined(room) = room {
|
||||
let msg_body = if let MessageEvent {
|
||||
let msg_body = if let SyncMessageEvent {
|
||||
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
|
||||
..
|
||||
} = event
|
||||
@@ -34,12 +37,9 @@ impl EventEmitter for CommandBot {
|
||||
};
|
||||
|
||||
if msg_body.contains("!party") {
|
||||
let content = MessageEventContent::Text(TextMessageEventContent {
|
||||
body: "🎉🎊🥳 let's PARTY!! 🥳🎊🎉".to_string(),
|
||||
format: None,
|
||||
formatted_body: None,
|
||||
relates_to: None,
|
||||
});
|
||||
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::text_plain(
|
||||
"🎉🎊🥳 let's PARTY!! 🥳🎊🎉",
|
||||
));
|
||||
// we clone here to hold the lock for as little time as possible.
|
||||
let room_id = room.read().await.room_id.clone();
|
||||
|
||||
@@ -78,12 +78,7 @@ async fn login_and_sync(
|
||||
let mut client = Client::new_with_config(homeserver_url, client_config).unwrap();
|
||||
|
||||
client
|
||||
.login(
|
||||
username.clone(),
|
||||
password,
|
||||
None,
|
||||
Some("command bot".to_string()),
|
||||
)
|
||||
.login(&username, &password, None, Some("command bot"))
|
||||
.await?;
|
||||
|
||||
println!("logged in as {}", username);
|
||||
@@ -91,18 +86,18 @@ async fn login_and_sync(
|
||||
// An initial sync to set up state and so our bot doesn't respond to old messages.
|
||||
// If the `StateStore` finds saved state in the location given the initial sync will
|
||||
// be skipped in favor of loading state from the store
|
||||
client.sync(SyncSettings::default()).await.unwrap();
|
||||
client.sync_once(SyncSettings::default()).await.unwrap();
|
||||
// add our CommandBot to be notified of incoming messages, we do this after the initial
|
||||
// sync to avoid responding to messages before the bot was running.
|
||||
client
|
||||
.add_event_emitter(Box::new(CommandBot::new(client.clone())))
|
||||
.await;
|
||||
|
||||
// since we called sync before we `sync_forever` we must pass that sync token to
|
||||
// `sync_forever`
|
||||
// since we called `sync_once` before we entered our sync loop we must pass
|
||||
// that sync token to `sync`
|
||||
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
|
||||
// this keeps state from the server streaming in to CommandBot via the EventEmitter trait
|
||||
client.sync_forever(settings, |_| async {}).await;
|
||||
client.sync(settings).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
env, io,
|
||||
process::exit,
|
||||
sync::atomic::{AtomicBool, Ordering},
|
||||
};
|
||||
|
||||
use serde_json::json;
|
||||
use url::Url;
|
||||
|
||||
use matrix_sdk::{
|
||||
self, api::r0::uiaa::AuthData, identifiers::UserId, Client, ClientConfig, LoopCtrl,
|
||||
SyncSettings,
|
||||
};
|
||||
|
||||
fn auth_data<'a>(user: &UserId, password: &str, session: Option<&'a str>) -> AuthData<'a> {
|
||||
let mut auth_parameters = BTreeMap::new();
|
||||
let identifier = json!({
|
||||
"type": "m.id.user",
|
||||
"user": user,
|
||||
});
|
||||
|
||||
auth_parameters.insert("identifier".to_owned(), identifier);
|
||||
auth_parameters.insert("password".to_owned(), password.to_owned().into());
|
||||
|
||||
// This is needed because of https://github.com/matrix-org/synapse/issues/5665
|
||||
auth_parameters.insert("user".to_owned(), user.as_str().into());
|
||||
|
||||
AuthData::DirectRequest {
|
||||
kind: "m.login.password",
|
||||
auth_parameters,
|
||||
session,
|
||||
}
|
||||
}
|
||||
|
||||
async fn bootstrap(client: Client, user_id: UserId, password: String) {
|
||||
println!("Bootstrapping a new cross signing identity, press enter to continue.");
|
||||
|
||||
let mut input = String::new();
|
||||
|
||||
io::stdin()
|
||||
.read_line(&mut input)
|
||||
.expect("error: unable to read user input");
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
if let Err(e) = client.bootstrap_cross_signing(None).await {
|
||||
if let Some(response) = e.uiaa_response() {
|
||||
let auth_data = auth_data(&user_id, &password, response.session.as_deref());
|
||||
client
|
||||
.bootstrap_cross_signing(Some(auth_data))
|
||||
.await
|
||||
.expect("Couldn't bootstrap cross signing")
|
||||
} else {
|
||||
panic!("Error durign cross signing bootstrap {:#?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "encryption"))]
|
||||
panic!("Cross signing requires the encryption feature to be enabled");
|
||||
}
|
||||
|
||||
async fn login(
|
||||
homeserver_url: String,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(), matrix_sdk::Error> {
|
||||
let client_config = ClientConfig::new()
|
||||
.disable_ssl_verification()
|
||||
.proxy("http://localhost:8080")
|
||||
.unwrap();
|
||||
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
|
||||
let client = Client::new_with_config(homeserver_url, client_config).unwrap();
|
||||
|
||||
let response = client
|
||||
.login(username, password, None, Some("rust-sdk"))
|
||||
.await?;
|
||||
|
||||
let user_id = &response.user_id;
|
||||
let client_ref = &client;
|
||||
let asked = AtomicBool::new(false);
|
||||
let asked_ref = &asked;
|
||||
|
||||
client
|
||||
.sync_with_callback(SyncSettings::new(), |_| async move {
|
||||
let asked = asked_ref;
|
||||
let client = &client_ref;
|
||||
let user_id = &user_id;
|
||||
let password = &password;
|
||||
|
||||
// Wait for sync to be done then ask the user to bootstrap.
|
||||
if !asked.load(Ordering::SeqCst) {
|
||||
tokio::spawn(bootstrap(
|
||||
(*client).clone(),
|
||||
(*user_id).clone(),
|
||||
password.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
asked.store(true, Ordering::SeqCst);
|
||||
LoopCtrl::Continue
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), matrix_sdk::Error> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let (homeserver_url, username, password) =
|
||||
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
|
||||
(Some(a), Some(b), Some(c)) => (a, b, c),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Usage: {} <homeserver_url> <username> <password>",
|
||||
env::args().next().unwrap()
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
login(homeserver_url, &username, &password).await
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
use std::{env, io, process::exit};
|
||||
use url::Url;
|
||||
|
||||
use matrix_sdk::{
|
||||
self, events::AnyToDeviceEvent, identifiers::UserId, Client, ClientConfig, LoopCtrl, Sas,
|
||||
SyncSettings,
|
||||
};
|
||||
|
||||
async fn wait_for_confirmation(client: Client, sas: Sas) {
|
||||
println!("Does the emoji match: {:?}", sas.emoji());
|
||||
|
||||
let mut input = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut input)
|
||||
.expect("error: unable to read user input");
|
||||
|
||||
match input.trim().to_lowercase().as_ref() {
|
||||
"yes" | "true" | "ok" => {
|
||||
sas.confirm().await.unwrap();
|
||||
|
||||
if sas.is_done() {
|
||||
print_result(&sas);
|
||||
print_devices(sas.other_device().user_id(), &client).await;
|
||||
}
|
||||
}
|
||||
_ => sas.cancel().await.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
fn print_result(sas: &Sas) {
|
||||
let device = sas.other_device();
|
||||
|
||||
println!(
|
||||
"Successfully verified device {} {} {:?}",
|
||||
device.user_id(),
|
||||
device.device_id(),
|
||||
device.local_trust_state()
|
||||
);
|
||||
}
|
||||
|
||||
async fn print_devices(user_id: &UserId, client: &Client) {
|
||||
println!("Devices of user {}", user_id);
|
||||
|
||||
for device in client.get_user_devices(user_id).await.unwrap().devices() {
|
||||
println!(
|
||||
" {:<10} {:<30} {:<}",
|
||||
device.device_id(),
|
||||
device.display_name().as_deref().unwrap_or_default(),
|
||||
device.is_trusted()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn login(
|
||||
homeserver_url: String,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(), matrix_sdk::Error> {
|
||||
let client_config = ClientConfig::new()
|
||||
.disable_ssl_verification()
|
||||
.proxy("http://localhost:8080")
|
||||
.unwrap();
|
||||
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
|
||||
let client = Client::new_with_config(homeserver_url, client_config).unwrap();
|
||||
|
||||
client
|
||||
.login(username, password, None, Some("rust-sdk"))
|
||||
.await?;
|
||||
|
||||
let client_ref = &client;
|
||||
|
||||
client
|
||||
.sync_with_callback(SyncSettings::new(), |response| async move {
|
||||
let client = &client_ref;
|
||||
|
||||
for event in &response.to_device.events {
|
||||
let e = event
|
||||
.deserialize()
|
||||
.expect("Can't deserialize to-device event");
|
||||
|
||||
match e {
|
||||
AnyToDeviceEvent::KeyVerificationStart(e) => {
|
||||
let sas = client
|
||||
.get_verification(&e.content.transaction_id)
|
||||
.await
|
||||
.expect("Sas object wasn't created");
|
||||
println!(
|
||||
"Starting verification with {} {}",
|
||||
&sas.other_device().user_id(),
|
||||
&sas.other_device().device_id()
|
||||
);
|
||||
print_devices(&e.sender, &client).await;
|
||||
sas.accept().await.unwrap();
|
||||
}
|
||||
|
||||
AnyToDeviceEvent::KeyVerificationKey(e) => {
|
||||
let sas = client
|
||||
.get_verification(&e.content.transaction_id)
|
||||
.await
|
||||
.expect("Sas object wasn't created");
|
||||
|
||||
tokio::spawn(wait_for_confirmation((*client).clone(), sas));
|
||||
}
|
||||
|
||||
AnyToDeviceEvent::KeyVerificationMac(e) => {
|
||||
let sas = client
|
||||
.get_verification(&e.content.transaction_id)
|
||||
.await
|
||||
.expect("Sas object wasn't created");
|
||||
|
||||
if sas.is_done() {
|
||||
print_result(&sas);
|
||||
print_devices(&e.sender, &client).await;
|
||||
}
|
||||
}
|
||||
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
LoopCtrl::Continue
|
||||
})
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), matrix_sdk::Error> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let (homeserver_url, username, password) =
|
||||
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
|
||||
(Some(a), Some(b), Some(c)) => (a, b, c),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Usage: {} <homeserver_url> <username> <password>",
|
||||
env::args().next().unwrap()
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
login(homeserver_url, &username, &password).await
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
use std::{convert::TryFrom, env, process::exit};
|
||||
|
||||
use url::Url;
|
||||
|
||||
use matrix_sdk::{
|
||||
self, api::r0::profile, identifiers::UserId, Client, ClientConfig, Result as MatrixResult,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct UserProfile {
|
||||
avatar_url: Option<String>,
|
||||
displayname: Option<String>,
|
||||
}
|
||||
|
||||
/// This function calls the GET profile endpoint
|
||||
/// Spec: https://matrix.org/docs/spec/client_server/r0.6.1#get-matrix-client-r0-profile-userid
|
||||
/// Ruma: https://docs.rs/ruma-client-api/0.9.0/ruma_client_api/r0/profile/get_profile/index.html
|
||||
async fn get_profile(client: Client, mxid: &UserId) -> MatrixResult<UserProfile> {
|
||||
// First construct the request you want to make
|
||||
// See https://docs.rs/ruma-client-api/0.9.0/ruma_client_api/index.html for all available Endpoints
|
||||
let request = profile::get_profile::Request::new(mxid);
|
||||
|
||||
// Start the request using matrix_sdk::Client::send
|
||||
let resp = client.send(request).await?;
|
||||
|
||||
// Use the response and construct a UserProfile struct.
|
||||
// See https://docs.rs/ruma-client-api/0.9.0/ruma_client_api/r0/profile/get_profile/struct.Response.html
|
||||
// for details on the Response for this Request
|
||||
let user_profile = UserProfile {
|
||||
avatar_url: resp.avatar_url,
|
||||
displayname: resp.displayname,
|
||||
};
|
||||
Ok(user_profile)
|
||||
}
|
||||
|
||||
async fn login(
|
||||
homeserver_url: String,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<Client, matrix_sdk::Error> {
|
||||
let client_config = ClientConfig::new()
|
||||
.proxy("http://localhost:8080")?
|
||||
.disable_ssl_verification();
|
||||
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
|
||||
let client = Client::new_with_config(homeserver_url, client_config).unwrap();
|
||||
|
||||
client
|
||||
.login(username, password, None, Some("rust-sdk"))
|
||||
.await?;
|
||||
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), matrix_sdk::Error> {
|
||||
tracing_subscriber::fmt::init();
|
||||
|
||||
let (homeserver_url, username, password) =
|
||||
match (env::args().nth(1), env::args().nth(2), env::args().nth(3)) {
|
||||
(Some(a), Some(b), Some(c)) => (a, b, c),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Usage: {} <homeserver_url> <mxid> <password>",
|
||||
env::args().next().unwrap()
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
let client = login(homeserver_url, &username, &password).await?;
|
||||
|
||||
let user_id = UserId::try_from(username).expect("Couldn't parse the MXID");
|
||||
let profile = get_profile(client, &user_id).await?;
|
||||
println!("{:#?}", profile);
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
use std::{
|
||||
env,
|
||||
fs::File,
|
||||
io::{Seek, SeekFrom},
|
||||
path::PathBuf,
|
||||
process::exit,
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use matrix_sdk::{
|
||||
self, async_trait,
|
||||
events::{
|
||||
room::message::{MessageEventContent, TextMessageEventContent},
|
||||
SyncMessageEvent,
|
||||
},
|
||||
Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
struct ImageBot {
|
||||
client: Client,
|
||||
image: Arc<Mutex<File>>,
|
||||
}
|
||||
|
||||
impl ImageBot {
|
||||
pub fn new(client: Client, image: File) -> Self {
|
||||
let image = Arc::new(Mutex::new(image));
|
||||
Self { client, image }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl EventEmitter for ImageBot {
|
||||
async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
|
||||
if let SyncRoom::Joined(room) = room {
|
||||
let msg_body = if let SyncMessageEvent {
|
||||
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
|
||||
..
|
||||
} = event
|
||||
{
|
||||
msg_body
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
if msg_body.contains("!image") {
|
||||
let room_id = room.read().await.room_id.clone();
|
||||
|
||||
println!("sending image");
|
||||
let mut image = self.image.lock().await;
|
||||
|
||||
self.client
|
||||
.room_send_attachment(&room_id, "cat", &mime::IMAGE_JPEG, &mut *image, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
image.seek(SeekFrom::Start(0)).unwrap();
|
||||
|
||||
println!("message sent");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn login_and_sync(
|
||||
homeserver_url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
image: File,
|
||||
) -> Result<(), matrix_sdk::Error> {
|
||||
let client_config = ClientConfig::new()
|
||||
.proxy("http://localhost:8080")?
|
||||
.disable_ssl_verification();
|
||||
|
||||
let homeserver_url = Url::parse(&homeserver_url).expect("Couldn't parse the homeserver URL");
|
||||
let mut client = Client::new_with_config(homeserver_url, client_config).unwrap();
|
||||
|
||||
client
|
||||
.login(&username, &password, None, Some("command bot"))
|
||||
.await?;
|
||||
|
||||
client.sync_once(SyncSettings::default()).await.unwrap();
|
||||
client
|
||||
.add_event_emitter(Box::new(ImageBot::new(client.clone(), image)))
|
||||
.await;
|
||||
|
||||
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
|
||||
client.sync(settings).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), matrix_sdk::Error> {
|
||||
tracing_subscriber::fmt::init();
|
||||
let (homeserver_url, username, password, image_path) = match (
|
||||
env::args().nth(1),
|
||||
env::args().nth(2),
|
||||
env::args().nth(3),
|
||||
env::args().nth(4),
|
||||
) {
|
||||
(Some(a), Some(b), Some(c), Some(d)) => (a, b, c, d),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Usage: {} <homeserver_url> <username> <password> <image>",
|
||||
env::args().next().unwrap()
|
||||
);
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
println!(
|
||||
"helloooo {} {} {} {:#?}",
|
||||
homeserver_url, username, password, image_path
|
||||
);
|
||||
let path = PathBuf::from(image_path);
|
||||
let image = File::open(path).expect("Can't open image file.");
|
||||
|
||||
login_and_sync(homeserver_url, username, password, image).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -2,18 +2,21 @@ use std::{env, process::exit};
|
||||
use url::Url;
|
||||
|
||||
use matrix_sdk::{
|
||||
self,
|
||||
events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
|
||||
self, async_trait,
|
||||
events::{
|
||||
room::message::{MessageEventContent, TextMessageEventContent},
|
||||
SyncMessageEvent,
|
||||
},
|
||||
Client, ClientConfig, EventEmitter, SyncRoom, SyncSettings,
|
||||
};
|
||||
|
||||
struct EventCallback;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl EventEmitter for EventCallback {
|
||||
async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) {
|
||||
async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
|
||||
if let SyncRoom::Joined(room) = room {
|
||||
if let MessageEvent {
|
||||
if let SyncMessageEvent {
|
||||
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
|
||||
sender,
|
||||
..
|
||||
@@ -23,12 +26,8 @@ impl EventEmitter for EventCallback {
|
||||
// any reads should be held for the shortest time possible to
|
||||
// avoid dead locks
|
||||
let room = room.read().await;
|
||||
let member = room.members.get(&sender).unwrap();
|
||||
member
|
||||
.display_name
|
||||
.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or(sender.to_string())
|
||||
let member = room.joined_members.get(&sender).unwrap();
|
||||
member.name()
|
||||
};
|
||||
println!("{}: {}", name, msg_body);
|
||||
}
|
||||
@@ -38,8 +37,8 @@ impl EventEmitter for EventCallback {
|
||||
|
||||
async fn login(
|
||||
homeserver_url: String,
|
||||
username: String,
|
||||
password: String,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(), matrix_sdk::Error> {
|
||||
let client_config = ClientConfig::new()
|
||||
.proxy("http://localhost:8080")?
|
||||
@@ -50,9 +49,9 @@ async fn login(
|
||||
client.add_event_emitter(Box::new(EventCallback)).await;
|
||||
|
||||
client
|
||||
.login(username, password, None, Some("rust-sdk".to_string()))
|
||||
.login(username, password, None, Some("rust-sdk"))
|
||||
.await?;
|
||||
client.sync_forever(SyncSettings::new(), |_| async {}).await;
|
||||
client.sync(SyncSettings::new()).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -73,5 +72,5 @@ async fn main() -> Result<(), matrix_sdk::Error> {
|
||||
}
|
||||
};
|
||||
|
||||
login(homeserver_url, username, password).await
|
||||
login(homeserver_url, &username, &password).await
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ edition = "2018"
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
matrix-sdk = { path = "../..", default-features = false }
|
||||
matrix-sdk = { path = "../..", default-features = false, features = ["native-tls"] }
|
||||
url = "2.1.1"
|
||||
wasm-bindgen = { version = "0.2.62", features = ["serde-serialize"] }
|
||||
wasm-bindgen-futures = "0.4.12"
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
#![type_length_limit = "1702124"]
|
||||
|
||||
use matrix_sdk::{
|
||||
api::r0::sync::sync_events::Response as SyncResponse,
|
||||
events::collections::all::RoomEvent,
|
||||
events::room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
|
||||
events::{
|
||||
room::message::{MessageEventContent, TextMessageEventContent},
|
||||
AnyMessageEventContent, AnySyncMessageEvent, AnySyncRoomEvent, SyncMessageEvent,
|
||||
},
|
||||
identifiers::RoomId,
|
||||
Client, ClientConfig, SyncSettings,
|
||||
Client, ClientConfig, LoopCtrl, SyncSettings,
|
||||
};
|
||||
use url::Url;
|
||||
use wasm_bindgen::prelude::*;
|
||||
@@ -12,11 +16,15 @@ use web_sys::console;
|
||||
struct WasmBot(Client);
|
||||
|
||||
impl WasmBot {
|
||||
async fn on_room_message(&self, room_id: &RoomId, event: RoomEvent) {
|
||||
let msg_body = if let RoomEvent::RoomMessage(MessageEvent {
|
||||
async fn on_room_message(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event: SyncMessageEvent<MessageEventContent>,
|
||||
) {
|
||||
let msg_body = if let SyncMessageEvent {
|
||||
content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
|
||||
..
|
||||
}) = event
|
||||
} = event
|
||||
{
|
||||
msg_body.clone()
|
||||
} else {
|
||||
@@ -26,23 +34,27 @@ impl WasmBot {
|
||||
console::log_1(&format!("Received message event {:?}", &msg_body).into());
|
||||
|
||||
if msg_body.starts_with("!party") {
|
||||
let content = MessageEventContent::Text(TextMessageEventContent::new_plain(
|
||||
"🎉🎊🥳 let's PARTY with wasm!! 🥳🎊🎉".to_string(),
|
||||
let content = AnyMessageEventContent::RoomMessage(MessageEventContent::Text(
|
||||
TextMessageEventContent::plain("🎉🎊🥳 let's PARTY with wasm!! 🥳🎊🎉".to_string()),
|
||||
));
|
||||
|
||||
self.0.room_send(&room_id, content, None).await.unwrap();
|
||||
}
|
||||
}
|
||||
async fn on_sync_response(&self, response: SyncResponse) {
|
||||
console::log_1(&format!("Synced").into());
|
||||
async fn on_sync_response(&self, response: SyncResponse) -> LoopCtrl {
|
||||
console::log_1(&"Synced".to_string().into());
|
||||
|
||||
for (room_id, room) in response.rooms.join {
|
||||
for event in room.timeline.events {
|
||||
if let Ok(event) = event.deserialize() {
|
||||
self.on_room_message(&room_id, event).await
|
||||
if let AnySyncRoomEvent::Message(AnySyncMessageEvent::RoomMessage(ev)) = event {
|
||||
self.on_room_message(&room_id, ev).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LoopCtrl::Continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,11 +75,11 @@ pub async fn run() -> Result<JsValue, JsValue> {
|
||||
|
||||
let bot = WasmBot(client.clone());
|
||||
|
||||
client.sync(SyncSettings::default()).await.unwrap();
|
||||
client.sync_once(SyncSettings::default()).await.unwrap();
|
||||
|
||||
let settings = SyncSettings::default().token(client.sync_token().await.unwrap());
|
||||
client
|
||||
.sync_forever(settings, |response| bot.on_sync_response(response))
|
||||
.sync_with_callback(settings, |response| bot.on_sync_response(response))
|
||||
.await;
|
||||
|
||||
Ok(JsValue::NULL)
|
||||
|
||||
+1831
-883
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,131 @@
|
||||
// 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.
|
||||
|
||||
use std::{ops::Deref, result::Result as StdResult};
|
||||
|
||||
use matrix_sdk_base::crypto::{
|
||||
store::CryptoStoreError, Device as BaseDevice, LocalTrust, ReadOnlyDevice,
|
||||
UserDevices as BaseUserDevices,
|
||||
};
|
||||
use matrix_sdk_common::{
|
||||
api::r0::to_device::send_event_to_device::Request as ToDeviceRequest,
|
||||
identifiers::{DeviceId, DeviceIdBox},
|
||||
};
|
||||
|
||||
use crate::{error::Result, http_client::HttpClient, Sas};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// A device represents a E2EE capable client of an user.
|
||||
pub struct Device {
|
||||
pub(crate) inner: BaseDevice,
|
||||
pub(crate) http_client: HttpClient,
|
||||
}
|
||||
|
||||
impl Deref for Device {
|
||||
type Target = ReadOnlyDevice;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Device {
|
||||
/// Start a interactive verification with this `Device`
|
||||
///
|
||||
/// Returns a `Sas` object that represents the interactive verification flow.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```no_run
|
||||
/// # use std::convert::TryFrom;
|
||||
/// # use matrix_sdk::{Client, identifiers::UserId};
|
||||
/// # use url::Url;
|
||||
/// # use futures::executor::block_on;
|
||||
/// # let alice = UserId::try_from("@alice:example.org").unwrap();
|
||||
/// # let homeserver = Url::parse("http://example.com").unwrap();
|
||||
/// # let client = Client::new(homeserver).unwrap();
|
||||
/// # block_on(async {
|
||||
/// let device = client.get_device(&alice, "DEVICEID".into())
|
||||
/// .await
|
||||
/// .unwrap()
|
||||
/// .unwrap();
|
||||
///
|
||||
/// let verification = device.start_verification().await.unwrap();
|
||||
/// # });
|
||||
/// ```
|
||||
pub async fn start_verification(&self) -> Result<Sas> {
|
||||
let (sas, request) = self.inner.start_verification().await?;
|
||||
let txn_id_string = request.txn_id_string();
|
||||
let request = ToDeviceRequest::new(request.event_type, &txn_id_string, request.messages);
|
||||
|
||||
self.http_client.send(request).await?;
|
||||
|
||||
Ok(Sas {
|
||||
inner: sas,
|
||||
http_client: self.http_client.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Is the device trusted.
|
||||
pub fn is_trusted(&self) -> bool {
|
||||
self.inner.trust_state()
|
||||
}
|
||||
|
||||
/// Set the local trust state of the device to the given state.
|
||||
///
|
||||
/// This won't affect any cross signing trust state, this only sets a flag
|
||||
/// marking to have the given trust state.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `trust_state` - The new trust state that should be set for the device.
|
||||
pub async fn set_local_trust(
|
||||
&self,
|
||||
trust_state: LocalTrust,
|
||||
) -> StdResult<(), CryptoStoreError> {
|
||||
self.inner.set_local_trust(trust_state).await
|
||||
}
|
||||
}
|
||||
|
||||
/// A read only view over all devices belonging to a user.
|
||||
#[derive(Debug)]
|
||||
pub struct UserDevices {
|
||||
pub(crate) inner: BaseUserDevices,
|
||||
pub(crate) http_client: HttpClient,
|
||||
}
|
||||
|
||||
impl UserDevices {
|
||||
/// Get the specific device with the given device id.
|
||||
pub fn get(&self, device_id: &DeviceId) -> Option<Device> {
|
||||
self.inner.get(device_id).map(|d| Device {
|
||||
inner: d,
|
||||
http_client: self.http_client.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterator over all the device ids of the user devices.
|
||||
pub fn keys(&self) -> impl Iterator<Item = &DeviceIdBox> {
|
||||
self.inner.keys()
|
||||
}
|
||||
|
||||
/// Iterator over all the devices of the user devices.
|
||||
pub fn devices(&self) -> impl Iterator<Item = Device> + '_ {
|
||||
let client = self.http_client.clone();
|
||||
|
||||
self.inner.devices().map(move |d| Device {
|
||||
inner: d,
|
||||
http_client: client.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
+63
-6
@@ -14,15 +14,21 @@
|
||||
|
||||
//! Error conditions.
|
||||
|
||||
use matrix_sdk_base::Error as MatrixError;
|
||||
use matrix_sdk_common::{
|
||||
api::{
|
||||
r0::uiaa::{UiaaInfo, UiaaResponse as UiaaError},
|
||||
Error as RumaClientError,
|
||||
},
|
||||
FromHttpResponseError as RumaResponseError, IntoHttpError as RumaIntoHttpError, ServerError,
|
||||
};
|
||||
use reqwest::Error as ReqwestError;
|
||||
use serde_json::Error as JsonError;
|
||||
use std::io::Error as IoError;
|
||||
use thiserror::Error;
|
||||
|
||||
use matrix_sdk_base::Error as MatrixError;
|
||||
|
||||
use crate::api::Error as RumaClientError;
|
||||
use crate::FromHttpResponseError as RumaResponseError;
|
||||
use crate::IntoHttpError as RumaIntoHttpError;
|
||||
#[cfg(feature = "encryption")]
|
||||
use matrix_sdk_base::crypto::store::CryptoStoreError;
|
||||
|
||||
/// Result type of the rust-sdk.
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@@ -34,6 +40,10 @@ pub enum Error {
|
||||
#[error("the queried endpoint requires authentication but was called before logging in")]
|
||||
AuthenticationRequired,
|
||||
|
||||
/// Queried endpoint is not meant for clients.
|
||||
#[error("the queried endpoint is not meant for clients")]
|
||||
NotClientRequest,
|
||||
|
||||
/// An error at the HTTP layer.
|
||||
#[error(transparent)]
|
||||
Reqwest(#[from] ReqwestError),
|
||||
@@ -42,6 +52,10 @@ pub enum Error {
|
||||
#[error(transparent)]
|
||||
SerdeJson(#[from] JsonError),
|
||||
|
||||
/// An IO error happened.
|
||||
#[error(transparent)]
|
||||
IO(#[from] IoError),
|
||||
|
||||
/// An error converting between ruma_client_api types and Hyper types.
|
||||
#[error("can't parse the JSON response as a Matrix response")]
|
||||
RumaResponse(RumaResponseError<RumaClientError>),
|
||||
@@ -50,9 +64,52 @@ pub enum Error {
|
||||
#[error("can't convert between ruma_client_api and hyper types.")]
|
||||
IntoHttp(RumaIntoHttpError),
|
||||
|
||||
/// An error occured in the Matrix client library.
|
||||
/// An error occurred in the Matrix client library.
|
||||
#[error(transparent)]
|
||||
MatrixError(#[from] MatrixError),
|
||||
|
||||
/// An error occurred in the crypto store.
|
||||
#[cfg(feature = "encryption")]
|
||||
#[error(transparent)]
|
||||
CryptoStoreError(#[from] CryptoStoreError),
|
||||
|
||||
/// An error occurred while authenticating.
|
||||
///
|
||||
/// When registering or authenticating the Matrix server can send a `UiaaResponse`
|
||||
/// as the error type, this is a User-Interactive Authentication API response. This
|
||||
/// represents an error with information about how to authenticate the user.
|
||||
#[error("User-Interactive Authentication required.")]
|
||||
UiaaError(RumaResponseError<UiaaError>),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Try to destructure the error into an universal interactive auth info.
|
||||
///
|
||||
/// Some requests require universal interactive auth, doing such a request
|
||||
/// will always fail the first time with a 401 status code, the response
|
||||
/// body will contain info how the client can authenticate.
|
||||
///
|
||||
/// The request will need to be retried, this time containing additional
|
||||
/// authentication data.
|
||||
///
|
||||
/// This method is an convenience method to get to the info the server
|
||||
/// returned on the first, failed request.
|
||||
pub fn uiaa_response(&self) -> Option<&UiaaInfo> {
|
||||
if let Error::UiaaError(RumaResponseError::Http(ServerError::Known(
|
||||
UiaaError::AuthResponse(i),
|
||||
))) = self
|
||||
{
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RumaResponseError<UiaaError>> for Error {
|
||||
fn from(error: RumaResponseError<UiaaError>) -> Self {
|
||||
Self::UiaaError(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RumaResponseError<RumaClientError>> for Error {
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
// 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.
|
||||
|
||||
use std::{convert::TryFrom, fmt::Debug, sync::Arc};
|
||||
|
||||
use http::{HeaderValue, Method as HttpMethod, Response as HttpResponse};
|
||||
use reqwest::{Client, Response};
|
||||
use tracing::trace;
|
||||
use url::Url;
|
||||
|
||||
use matrix_sdk_common::{
|
||||
api::r0::media::create_content, async_trait, locks::RwLock, AuthScheme, FromHttpResponseError,
|
||||
};
|
||||
|
||||
use crate::{ClientConfig, Error, OutgoingRequest, Result, Session};
|
||||
|
||||
/// Abstraction around the http layer. The allows implementors to use different
|
||||
/// http libraries.
|
||||
#[async_trait]
|
||||
pub trait HttpSend: Sync + Send + Debug {
|
||||
/// The method abstracting sending request types and receiving response types.
|
||||
///
|
||||
/// This is called by the client every time it wants to send anything to a homeserver.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `request` - The http request that has been converted from a ruma `Request`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::convert::TryFrom;
|
||||
/// use matrix_sdk::{HttpSend, Result, async_trait};
|
||||
///
|
||||
/// #[derive(Debug)]
|
||||
/// struct Client(reqwest::Client);
|
||||
///
|
||||
/// impl Client {
|
||||
/// async fn response_to_http_response(
|
||||
/// &self,
|
||||
/// mut response: reqwest::Response,
|
||||
/// ) -> Result<http::Response<Vec<u8>>> {
|
||||
/// // Convert the reqwest response to a http one.
|
||||
/// todo!()
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// #[async_trait]
|
||||
/// impl HttpSend for Client {
|
||||
/// async fn send_request(&self, request: http::Request<Vec<u8>>) -> Result<http::Response<Vec<u8>>> {
|
||||
/// Ok(self
|
||||
/// .response_to_http_response(
|
||||
/// self.0
|
||||
/// .execute(reqwest::Request::try_from(request)?)
|
||||
/// .await?,
|
||||
/// )
|
||||
/// .await?)
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
async fn send_request(
|
||||
&self,
|
||||
request: http::Request<Vec<u8>>,
|
||||
) -> Result<http::Response<Vec<u8>>>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct HttpClient {
|
||||
pub(crate) inner: Arc<dyn HttpSend>,
|
||||
pub(crate) homeserver: Arc<Url>,
|
||||
pub(crate) session: Arc<RwLock<Option<Session>>>,
|
||||
}
|
||||
|
||||
impl HttpClient {
|
||||
async fn send_request<Request: OutgoingRequest>(
|
||||
&self,
|
||||
request: Request,
|
||||
session: Arc<RwLock<Option<Session>>>,
|
||||
content_type: Option<HeaderValue>,
|
||||
) -> Result<http::Response<Vec<u8>>> {
|
||||
let mut request = {
|
||||
let read_guard;
|
||||
let access_token = match Request::METADATA.authentication {
|
||||
AuthScheme::AccessToken => {
|
||||
read_guard = session.read().await;
|
||||
|
||||
if let Some(session) = read_guard.as_ref() {
|
||||
Some(session.access_token.as_str())
|
||||
} else {
|
||||
return Err(Error::AuthenticationRequired);
|
||||
}
|
||||
}
|
||||
AuthScheme::None => None,
|
||||
_ => return Err(Error::NotClientRequest),
|
||||
};
|
||||
|
||||
request.try_into_http_request(&self.homeserver.to_string(), access_token)?
|
||||
};
|
||||
|
||||
if let HttpMethod::POST | HttpMethod::PUT | HttpMethod::DELETE = *request.method() {
|
||||
if let Some(content_type) = content_type {
|
||||
request
|
||||
.headers_mut()
|
||||
.append(http::header::CONTENT_TYPE, content_type);
|
||||
}
|
||||
}
|
||||
|
||||
self.inner.send_request(request).await
|
||||
}
|
||||
|
||||
pub async fn upload(
|
||||
&self,
|
||||
request: create_content::Request<'_>,
|
||||
) -> Result<create_content::Response> {
|
||||
let response = self
|
||||
.send_request(request, self.session.clone(), None)
|
||||
.await?;
|
||||
Ok(create_content::Response::try_from(response)?)
|
||||
}
|
||||
|
||||
pub async fn send<Request>(&self, request: Request) -> Result<Request::IncomingResponse>
|
||||
where
|
||||
Request: OutgoingRequest,
|
||||
Error: From<FromHttpResponseError<Request::EndpointError>>,
|
||||
{
|
||||
let content_type = HeaderValue::from_static("application/json");
|
||||
let response = self
|
||||
.send_request(request, self.session.clone(), Some(content_type))
|
||||
.await?;
|
||||
|
||||
trace!("Got response: {:?}", response);
|
||||
|
||||
Ok(Request::IncomingResponse::try_from(response)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a client with the specified configuration.
|
||||
pub(crate) fn client_with_config(config: &ClientConfig) -> Result<Client> {
|
||||
let http_client = reqwest::Client::builder();
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let http_client = {
|
||||
let http_client = match config.timeout {
|
||||
Some(x) => http_client.timeout(x),
|
||||
None => http_client,
|
||||
};
|
||||
|
||||
let http_client = if config.disable_ssl_verification {
|
||||
http_client.danger_accept_invalid_certs(true)
|
||||
} else {
|
||||
http_client
|
||||
};
|
||||
|
||||
let http_client = match &config.proxy {
|
||||
Some(p) => http_client.proxy(p.clone()),
|
||||
None => http_client,
|
||||
};
|
||||
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
|
||||
let user_agent = match &config.user_agent {
|
||||
Some(a) => a.clone(),
|
||||
None => HeaderValue::from_str(&format!("matrix-rust-sdk {}", crate::VERSION)).unwrap(),
|
||||
};
|
||||
|
||||
headers.insert(reqwest::header::USER_AGENT, user_agent);
|
||||
|
||||
http_client.default_headers(headers)
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[allow(unused)]
|
||||
let _ = config;
|
||||
|
||||
Ok(http_client.build()?)
|
||||
}
|
||||
|
||||
async fn response_to_http_response(mut response: Response) -> Result<http::Response<Vec<u8>>> {
|
||||
let status = response.status();
|
||||
|
||||
let mut http_builder = HttpResponse::builder().status(status);
|
||||
let headers = http_builder.headers_mut().unwrap();
|
||||
|
||||
for (k, v) in response.headers_mut().drain() {
|
||||
if let Some(key) = k {
|
||||
headers.insert(key, v);
|
||||
}
|
||||
}
|
||||
|
||||
let body = response.bytes().await?.as_ref().to_owned();
|
||||
|
||||
Ok(http_builder.body(body).unwrap())
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl HttpSend for Client {
|
||||
async fn send_request(
|
||||
&self,
|
||||
request: http::Request<Vec<u8>>,
|
||||
) -> Result<http::Response<Vec<u8>>> {
|
||||
Ok(
|
||||
response_to_http_response(self.execute(reqwest::Request::try_from(request)?).await?)
|
||||
.await?,
|
||||
)
|
||||
}
|
||||
}
|
||||
+58
-12
@@ -15,15 +15,35 @@
|
||||
|
||||
//! This crate implements a [Matrix](https://matrix.org/) client library.
|
||||
//!
|
||||
//! ## Crate Feature Flags
|
||||
//! # Enabling logging
|
||||
//!
|
||||
//! Users of the matrix-sdk crate can enable log output by depending on the `tracing-subscriber`
|
||||
//! crate and including the following line in their application (e.g. at the start of `main`):
|
||||
//!
|
||||
//! ```rust
|
||||
//! tracing_subscriber::fmt::init();
|
||||
//! ```
|
||||
//!
|
||||
//! The log output is controlled via the `RUST_LOG` environment variable by setting it to one of
|
||||
//! the `error`, `warn`, `info`, `debug` or `trace` levels. The output is printed to stdout.
|
||||
//!
|
||||
//! The `RUST_LOG` variable also supports a more advanced syntax for filtering log output more
|
||||
//! precisely, for instance with crate-level granularity. For more information on this, check out
|
||||
//! the [tracing_subscriber
|
||||
//! documentation](https://tracing.rs/tracing_subscriber/filter/struct.envfilter).
|
||||
//!
|
||||
//! # Crate Feature Flags
|
||||
//!
|
||||
//! The following crate feature flags are available:
|
||||
//!
|
||||
//! * `encryption`: Enables end-to-end encryption support in the library.
|
||||
//! * `sqlite-cryptostore`: Enables a SQLite based store for the encryption
|
||||
//! * `sqlite_cryptostore`: Enables a SQLite based store for the encryption
|
||||
//! keys. If this is disabled and `encryption` support is enabled the keys will
|
||||
//! by default be stored only in memory and thus lost after the client is
|
||||
//! destroyed.
|
||||
//! * `unstable-synapse-quirks`: Enables support to deal with inconsistencies
|
||||
//! of Synapse in compliance with the Matrix API specification.
|
||||
//! * `socks`: Enables SOCKS support in reqwest, the default HTTP client.
|
||||
|
||||
#![deny(
|
||||
missing_debug_implementations,
|
||||
@@ -35,23 +55,49 @@
|
||||
unused_import_braces,
|
||||
unused_qualifications
|
||||
)]
|
||||
#![cfg_attr(feature = "docs", feature(doc_cfg))]
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use matrix_sdk_base::JsonStore;
|
||||
pub use matrix_sdk_base::{EventEmitter, Room, Session, SyncRoom};
|
||||
pub use matrix_sdk_base::{RoomState, StateStore};
|
||||
pub use matrix_sdk_common::*;
|
||||
pub use reqwest::header::InvalidHeaderValue;
|
||||
#[cfg(not(any(feature = "native-tls", feature = "rustls-tls",)))]
|
||||
compile_error!("one of 'native-tls' or 'rustls-tls' features must be enabled");
|
||||
|
||||
#[cfg(all(feature = "native-tls", feature = "rustls-tls",))]
|
||||
compile_error!("only one of 'native-tls' or 'rustls-tls' features can be enabled");
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
pub use matrix_sdk_base::{Device, TrustState};
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
pub use matrix_sdk_base::crypto::LocalTrust;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use matrix_sdk_base::JsonStore;
|
||||
pub use matrix_sdk_base::{
|
||||
CustomEvent, Error as BaseError, EventEmitter, Room, RoomMember, RoomState, Session,
|
||||
StateStore, SyncRoom,
|
||||
};
|
||||
|
||||
#[cfg(feature = "messages")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(messages)))]
|
||||
pub use matrix_sdk_base::{MessageQueue, PossiblyRedactedExt};
|
||||
|
||||
pub use matrix_sdk_common::*;
|
||||
pub use reqwest;
|
||||
|
||||
mod client;
|
||||
mod error;
|
||||
mod request_builder;
|
||||
pub use client::{Client, ClientConfig, SyncSettings};
|
||||
mod http_client;
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
mod device;
|
||||
#[cfg(feature = "encryption")]
|
||||
mod sas;
|
||||
|
||||
pub use client::{Client, ClientConfig, LoopCtrl, SyncSettings};
|
||||
#[cfg(feature = "encryption")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
pub use device::Device;
|
||||
pub use error::{Error, Result};
|
||||
pub use request_builder::{MessagesRequestBuilder, RoomBuilder};
|
||||
pub use http_client::HttpSend;
|
||||
#[cfg(feature = "encryption")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
pub use sas::Sas;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -1,386 +0,0 @@
|
||||
use crate::api;
|
||||
use crate::events::room::power_levels::PowerLevelsEventContent;
|
||||
use crate::events::EventJson;
|
||||
use crate::identifiers::{RoomId, UserId};
|
||||
use api::r0::filter::RoomEventFilter;
|
||||
use api::r0::membership::Invite3pid;
|
||||
use api::r0::message::get_message_events::{self, Direction};
|
||||
use api::r0::room::{
|
||||
create_room::{self, CreationContent, InitialStateEvent, RoomPreset},
|
||||
Visibility,
|
||||
};
|
||||
|
||||
use crate::js_int::UInt;
|
||||
|
||||
/// A builder used to create rooms.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::convert::TryFrom;
|
||||
/// # use matrix_sdk::{Client, RoomBuilder};
|
||||
/// # use matrix_sdk::api::r0::room::Visibility;
|
||||
/// # use matrix_sdk::identifiers::UserId;
|
||||
/// # use url::Url;
|
||||
/// # let homeserver = Url::parse("http://example.com").unwrap();
|
||||
/// # let mut rt = tokio::runtime::Runtime::new().unwrap();
|
||||
/// # rt.block_on(async {
|
||||
/// let mut builder = RoomBuilder::default();
|
||||
/// builder.creation_content(false)
|
||||
/// .initial_state(vec![])
|
||||
/// .visibility(Visibility::Public)
|
||||
/// .name("name")
|
||||
/// .room_version("v1.0");
|
||||
/// let mut client = Client::new(homeserver).unwrap();
|
||||
/// client.create_room(builder).await;
|
||||
/// # })
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct RoomBuilder {
|
||||
/// Extra keys to be added to the content of the `m.room.create`.
|
||||
creation_content: Option<CreationContent>,
|
||||
/// List of state events to send to the new room.
|
||||
///
|
||||
/// Takes precedence over events set by preset, but gets overriden by
|
||||
/// name and topic keys.
|
||||
initial_state: Vec<InitialStateEvent>,
|
||||
/// A list of user IDs to invite to the room.
|
||||
///
|
||||
/// This will tell the server to invite everyone in the list to the newly created room.
|
||||
invite: Vec<UserId>,
|
||||
/// List of third party IDs of users to invite.
|
||||
invite_3pid: Vec<Invite3pid>,
|
||||
/// If set, this sets the `is_direct` flag on room invites.
|
||||
is_direct: Option<bool>,
|
||||
/// If this is included, an `m.room.name` event will be sent into the room to indicate
|
||||
/// the name of the room.
|
||||
name: Option<String>,
|
||||
/// Power level content to override in the default power level event.
|
||||
power_level_content_override: Option<PowerLevelsEventContent>,
|
||||
/// Convenience parameter for setting various default state events based on a preset.
|
||||
preset: Option<RoomPreset>,
|
||||
/// The desired room alias local part.
|
||||
room_alias_name: Option<String>,
|
||||
/// Room version to set for the room. Defaults to homeserver's default if not specified.
|
||||
room_version: Option<String>,
|
||||
/// If this is included, an `m.room.topic` event will be sent into the room to indicate
|
||||
/// the topic for the room.
|
||||
topic: Option<String>,
|
||||
/// A public visibility indicates that the room will be shown in the published room
|
||||
/// list. A private visibility will hide the room from the published room list. Rooms
|
||||
/// default to private visibility if this key is not included.
|
||||
visibility: Option<Visibility>,
|
||||
}
|
||||
|
||||
impl RoomBuilder {
|
||||
/// Returns an empty `RoomBuilder` for creating rooms.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Set the `CreationContent`.
|
||||
///
|
||||
/// Weather users on other servers can join this room.
|
||||
pub fn creation_content(&mut self, federate: bool) -> &mut Self {
|
||||
let federate = Some(federate);
|
||||
self.creation_content = Some(CreationContent { federate });
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the `InitialStateEvent` vector.
|
||||
pub fn initial_state(&mut self, state: Vec<InitialStateEvent>) -> &mut Self {
|
||||
self.initial_state = state;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the vec of `UserId`s.
|
||||
pub fn invite(&mut self, invite: Vec<UserId>) -> &mut Self {
|
||||
self.invite = invite;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the vec of `Invite3pid`s.
|
||||
pub fn invite_3pid(&mut self, invite: Vec<Invite3pid>) -> &mut Self {
|
||||
self.invite_3pid = invite;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the vec of `Invite3pid`s.
|
||||
pub fn is_direct(&mut self, direct: bool) -> &mut Self {
|
||||
self.is_direct = Some(direct);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the room name. A `m.room.name` event will be sent to the room.
|
||||
pub fn name<S: Into<String>>(&mut self, name: S) -> &mut Self {
|
||||
self.name = Some(name.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the room's power levels.
|
||||
pub fn power_level_override(&mut self, power: PowerLevelsEventContent) -> &mut Self {
|
||||
self.power_level_content_override = Some(power);
|
||||
self
|
||||
}
|
||||
|
||||
/// Convenience for setting various default state events based on a preset.
|
||||
pub fn preset(&mut self, preset: RoomPreset) -> &mut Self {
|
||||
self.preset = Some(preset);
|
||||
self
|
||||
}
|
||||
|
||||
/// The local part of a room alias.
|
||||
pub fn room_alias_name<S: Into<String>>(&mut self, alias: S) -> &mut Self {
|
||||
self.room_alias_name = Some(alias.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Room version, defaults to homeserver's version if left unspecified.
|
||||
pub fn room_version<S: Into<String>>(&mut self, version: S) -> &mut Self {
|
||||
self.room_version = Some(version.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// If included, a `m.room.topic` event will be sent to the room.
|
||||
pub fn topic<S: Into<String>>(&mut self, topic: S) -> &mut Self {
|
||||
self.topic = Some(topic.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// A public visibility indicates that the room will be shown in the published
|
||||
/// room list. A private visibility will hide the room from the published room list.
|
||||
/// Rooms default to private visibility if this key is not included.
|
||||
pub fn visibility(&mut self, vis: Visibility) -> &mut Self {
|
||||
self.visibility = Some(vis);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<create_room::Request> for RoomBuilder {
|
||||
fn into(self) -> create_room::Request {
|
||||
create_room::Request {
|
||||
creation_content: self.creation_content,
|
||||
initial_state: self.initial_state,
|
||||
invite: self.invite,
|
||||
invite_3pid: self.invite_3pid,
|
||||
is_direct: self.is_direct,
|
||||
name: self.name,
|
||||
power_level_content_override: self.power_level_content_override.map(EventJson::from),
|
||||
preset: self.preset,
|
||||
room_alias_name: self.room_alias_name,
|
||||
room_version: self.room_version,
|
||||
topic: self.topic,
|
||||
visibility: self.visibility,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a builder for making get_message_event requests.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::convert::TryFrom;
|
||||
/// # use matrix_sdk::{Client, MessagesRequestBuilder};
|
||||
/// # use matrix_sdk::api::r0::message::get_message_events::{self, Direction};
|
||||
/// # use matrix_sdk::identifiers::RoomId;
|
||||
/// # use url::Url;
|
||||
/// # let homeserver = Url::parse("http://example.com").unwrap();
|
||||
/// # let mut rt = tokio::runtime::Runtime::new().unwrap();
|
||||
/// # rt.block_on(async {
|
||||
/// # let room_id = RoomId::try_from("!test:localhost").unwrap();
|
||||
/// # let last_sync_token = "".to_string();
|
||||
/// let mut client = Client::new(homeserver).unwrap();
|
||||
///
|
||||
/// let mut builder = MessagesRequestBuilder::new();
|
||||
/// builder.room_id(room_id)
|
||||
/// .from(last_sync_token)
|
||||
/// .direction(Direction::Forward);
|
||||
///
|
||||
/// client.room_messages(builder).await.is_err();
|
||||
/// # })
|
||||
/// ```
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MessagesRequestBuilder {
|
||||
/// The room to get events from.
|
||||
room_id: Option<RoomId>,
|
||||
/// The token to start returning events from.
|
||||
///
|
||||
/// This token can be obtained from a
|
||||
/// prev_batch token returned for each room by the sync API, or from a start or end token
|
||||
/// returned by a previous request to this endpoint.
|
||||
from: Option<String>,
|
||||
/// The token to stop returning events at.
|
||||
///
|
||||
/// This token can be obtained from a prev_batch
|
||||
/// token returned for each room by the sync endpoint, or from a start or end token returned
|
||||
/// by a previous request to this endpoint.
|
||||
to: Option<String>,
|
||||
/// The direction to return events from.
|
||||
direction: Option<Direction>,
|
||||
/// The maximum number of events to return.
|
||||
///
|
||||
/// Default: 10.
|
||||
limit: Option<UInt>,
|
||||
/// A filter of the returned events with.
|
||||
filter: Option<RoomEventFilter>,
|
||||
}
|
||||
|
||||
impl MessagesRequestBuilder {
|
||||
/// Create a `MessagesRequestBuilder` builder to make a `get_message_events::Request`.
|
||||
///
|
||||
/// The `room_id` and `from`` fields **need to be set** to create the request.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// RoomId is required to create a `get_message_events::Request`.
|
||||
pub fn room_id(&mut self, room_id: RoomId) -> &mut Self {
|
||||
self.room_id = Some(room_id);
|
||||
self
|
||||
}
|
||||
|
||||
/// A `next_batch` token or `start` or `end` from a previous `get_message_events` request.
|
||||
///
|
||||
/// This is required to create a `get_message_events::Request`.
|
||||
pub fn from(&mut self, from: String) -> &mut Self {
|
||||
self.from = Some(from);
|
||||
self
|
||||
}
|
||||
|
||||
/// A `next_batch` token or `start` or `end` from a previous `get_message_events` request.
|
||||
///
|
||||
/// This token signals when to stop receiving events.
|
||||
pub fn to(&mut self, to: String) -> &mut Self {
|
||||
self.to = Some(to);
|
||||
self
|
||||
}
|
||||
|
||||
/// The direction to return events from.
|
||||
///
|
||||
/// If not specified `Direction::Backward` is used.
|
||||
pub fn direction(&mut self, direction: Direction) -> &mut Self {
|
||||
self.direction = Some(direction);
|
||||
self
|
||||
}
|
||||
|
||||
/// The maximum number of events to return.
|
||||
pub fn limit(&mut self, limit: UInt) -> &mut Self {
|
||||
self.limit = Some(limit);
|
||||
self
|
||||
}
|
||||
|
||||
/// Filter events by the given `RoomEventFilter`.
|
||||
pub fn filter(&mut self, filter: RoomEventFilter) -> &mut Self {
|
||||
self.filter = Some(filter);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<get_message_events::Request> for MessagesRequestBuilder {
|
||||
fn into(self) -> get_message_events::Request {
|
||||
get_message_events::Request {
|
||||
room_id: self.room_id.expect("`room_id` and `from` need to be set"),
|
||||
from: self.from.expect("`room_id` and `from` need to be set"),
|
||||
to: self.to,
|
||||
dir: self.direction.unwrap_or(Direction::Backward),
|
||||
limit: self.limit,
|
||||
filter: self.filter,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::*;
|
||||
use crate::api::r0::filter::{LazyLoadOptions, RoomEventFilter};
|
||||
use crate::events::room::power_levels::NotificationPowerLevels;
|
||||
use crate::js_int::Int;
|
||||
use crate::{identifiers::RoomId, Client, Session};
|
||||
|
||||
use mockito::{mock, Matcher};
|
||||
use std::convert::TryFrom;
|
||||
use url::Url;
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_room_builder() {
|
||||
let homeserver = Url::parse(&mockito::server_url()).unwrap();
|
||||
|
||||
let _m = mock("POST", "/_matrix/client/r0/createRoom")
|
||||
.with_status(200)
|
||||
.with_body_from_file("../test_data/room_id.json")
|
||||
.create();
|
||||
|
||||
let session = Session {
|
||||
access_token: "1234".to_owned(),
|
||||
user_id: UserId::try_from("@example:localhost").unwrap(),
|
||||
device_id: "DEVICEID".to_owned(),
|
||||
};
|
||||
|
||||
let mut builder = RoomBuilder::new();
|
||||
builder
|
||||
.creation_content(false)
|
||||
.initial_state(vec![])
|
||||
.visibility(Visibility::Public)
|
||||
.name("room_name")
|
||||
.room_version("v1.0")
|
||||
.invite_3pid(vec![])
|
||||
.is_direct(true)
|
||||
.power_level_override(PowerLevelsEventContent {
|
||||
ban: Int::MAX,
|
||||
events: BTreeMap::default(),
|
||||
events_default: Int::MIN,
|
||||
invite: Int::MIN,
|
||||
kick: Int::MIN,
|
||||
redact: Int::MAX,
|
||||
state_default: Int::MIN,
|
||||
users_default: Int::MIN,
|
||||
notifications: NotificationPowerLevels { room: Int::MIN },
|
||||
users: BTreeMap::default(),
|
||||
})
|
||||
.preset(RoomPreset::PrivateChat)
|
||||
.room_alias_name("room_alias")
|
||||
.topic("room topic")
|
||||
.visibility(Visibility::Private);
|
||||
let cli = Client::new(homeserver).unwrap();
|
||||
cli.restore_login(session).await.unwrap();
|
||||
assert!(cli.create_room(builder).await.is_ok());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_message_events() {
|
||||
let homeserver = Url::parse(&mockito::server_url()).unwrap();
|
||||
|
||||
let _m = mock(
|
||||
"GET",
|
||||
Matcher::Regex(r"^/_matrix/client/r0/rooms/.*/messages".to_string()),
|
||||
)
|
||||
.with_status(200)
|
||||
.with_body_from_file("../test_data/room_messages.json")
|
||||
.create();
|
||||
|
||||
let session = Session {
|
||||
access_token: "1234".to_owned(),
|
||||
user_id: UserId::try_from("@example:localhost").unwrap(),
|
||||
device_id: "DEVICEID".to_owned(),
|
||||
};
|
||||
|
||||
let mut builder = MessagesRequestBuilder::new();
|
||||
builder
|
||||
.room_id(RoomId::try_from("!roomid:example.com").unwrap())
|
||||
.from("t47429-4392820_219380_26003_2265".to_string())
|
||||
.to("t4357353_219380_26003_2265".to_string())
|
||||
.direction(Direction::Backward)
|
||||
.limit(UInt::new(10).unwrap())
|
||||
.filter(RoomEventFilter {
|
||||
lazy_load_options: LazyLoadOptions::Enabled {
|
||||
include_redundant_members: false,
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let cli = Client::new(homeserver).unwrap();
|
||||
cli.restore_login(session).await.unwrap();
|
||||
assert!(cli.room_messages(builder).await.is_ok());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
// 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.
|
||||
|
||||
use matrix_sdk_base::crypto::{ReadOnlyDevice, Sas as BaseSas};
|
||||
use matrix_sdk_common::api::r0::to_device::send_event_to_device::Request as ToDeviceRequest;
|
||||
|
||||
use crate::{error::Result, http_client::HttpClient};
|
||||
|
||||
/// An object controling the interactive verification flow.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Sas {
|
||||
pub(crate) inner: BaseSas,
|
||||
pub(crate) http_client: HttpClient,
|
||||
}
|
||||
|
||||
impl Sas {
|
||||
/// Accept the interactive verification flow.
|
||||
pub async fn accept(&self) -> Result<()> {
|
||||
if let Some(req) = self.inner.accept() {
|
||||
let txn_id_string = req.txn_id_string();
|
||||
let request = ToDeviceRequest::new(req.event_type, &txn_id_string, req.messages);
|
||||
|
||||
self.http_client.send(request).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Confirm that the short auth strings match on both sides.
|
||||
pub async fn confirm(&self) -> Result<()> {
|
||||
let (to_device, signature) = self.inner.confirm().await?;
|
||||
|
||||
if let Some(req) = to_device {
|
||||
let txn_id_string = req.txn_id_string();
|
||||
let request = ToDeviceRequest::new(req.event_type, &txn_id_string, req.messages);
|
||||
|
||||
self.http_client.send(request).await?;
|
||||
}
|
||||
|
||||
if let Some(s) = signature {
|
||||
self.http_client.send(s).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Cancel the interactive verification flow.
|
||||
pub async fn cancel(&self) -> Result<()> {
|
||||
if let Some(req) = self.inner.cancel() {
|
||||
let txn_id_string = req.txn_id_string();
|
||||
let request = ToDeviceRequest::new(req.event_type, &txn_id_string, req.messages);
|
||||
|
||||
self.http_client.send(request).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the emoji version of the short auth string.
|
||||
pub fn emoji(&self) -> Option<Vec<(&'static str, &'static str)>> {
|
||||
self.inner.emoji()
|
||||
}
|
||||
|
||||
/// Get the decimal version of the short auth string.
|
||||
pub fn decimals(&self) -> Option<(u16, u16, u16)> {
|
||||
self.inner.decimals()
|
||||
}
|
||||
|
||||
/// Is the verification process done.
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.inner.is_done()
|
||||
}
|
||||
|
||||
/// Is the verification process canceled.
|
||||
pub fn is_canceled(&self) -> bool {
|
||||
self.inner.is_canceled()
|
||||
}
|
||||
|
||||
/// Get the other users device that we're veryfying.
|
||||
pub fn other_device(&self) -> ReadOnlyDevice {
|
||||
self.inner.other_device()
|
||||
}
|
||||
}
|
||||
+26
-17
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk"]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
description = "The base component to build a Matrix client library."
|
||||
edition = "2018"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
@@ -8,39 +8,48 @@ license = "Apache-2.0"
|
||||
name = "matrix-sdk-base"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["docs"]
|
||||
rustdoc-args = ["--cfg", "feature=\"docs\""]
|
||||
|
||||
[features]
|
||||
default = ["encryption", "sqlite-cryptostore", "messages"]
|
||||
default = ["encryption", "sqlite_cryptostore", "messages"]
|
||||
messages = []
|
||||
encryption = ["matrix-sdk-crypto"]
|
||||
sqlite-cryptostore = ["matrix-sdk-crypto/sqlite-cryptostore"]
|
||||
sqlite_cryptostore = ["matrix-sdk-crypto/sqlite_cryptostore"]
|
||||
unstable-synapse-quirks = ["matrix-sdk-common/unstable-synapse-quirks"]
|
||||
|
||||
docs = ["encryption", "sqlite_cryptostore", "messages"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.31"
|
||||
serde = "1.0.110"
|
||||
serde_json = "1.0.53"
|
||||
zeroize = "1.1.0"
|
||||
serde = "1.0.118"
|
||||
serde_json = "1.0.61"
|
||||
zeroize = "1.2.0"
|
||||
tracing = "0.1.22"
|
||||
|
||||
matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" }
|
||||
matrix-sdk-crypto = { version = "0.1.0", path = "../matrix_sdk_crypto", optional = true }
|
||||
matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" }
|
||||
matrix-sdk-crypto = { version = "0.2.0", path = "../matrix_sdk_crypto", optional = true }
|
||||
|
||||
# Misc dependencies
|
||||
thiserror = "1.0.19"
|
||||
thiserror = "1.0.23"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
|
||||
version = "0.2.21"
|
||||
version = "0.2.24"
|
||||
default-features = false
|
||||
features = ["sync", "fs"]
|
||||
|
||||
[dev-dependencies]
|
||||
matrix-sdk-test = { version = "0.1.0", path = "../matrix_sdk_test" }
|
||||
http = "0.2.1"
|
||||
tracing-subscriber = "0.2.5"
|
||||
futures = "0.3.8"
|
||||
matrix-sdk-test = { version = "0.2.0", path = "../matrix_sdk_test" }
|
||||
http = "0.2.2"
|
||||
tracing-subscriber = "0.2.15"
|
||||
tempfile = "3.1.0"
|
||||
mockito = "0.28.0"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
tokio = { version = "0.2.21", features = ["rt-threaded", "macros"] }
|
||||
tokio = { version = "0.2.24", default-features = false, features = ["rt-threaded", "macros"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
|
||||
wasm-bindgen-test = "0.3.12"
|
||||
wasm-bindgen-test = "0.3.19"
|
||||
|
||||
+909
-381
File diff suppressed because it is too large
Load Diff
@@ -47,13 +47,13 @@ pub enum Error {
|
||||
|
||||
/// An error occurred during a E2EE operation.
|
||||
#[cfg(feature = "encryption")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "encryption")))]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
#[error(transparent)]
|
||||
OlmError(#[from] OlmError),
|
||||
|
||||
/// An error occurred during a E2EE group operation.
|
||||
#[cfg(feature = "encryption")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "encryption")))]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
#[error(transparent)]
|
||||
MegolmError(#[from] MegolmError),
|
||||
}
|
||||
|
||||
@@ -15,36 +15,57 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk_common::locks::RwLock;
|
||||
use serde_json::value::RawValue as RawJsonValue;
|
||||
|
||||
use crate::events::{
|
||||
fully_read::FullyReadEvent,
|
||||
ignored_user_list::IgnoredUserListEvent,
|
||||
presence::PresenceEvent,
|
||||
push_rules::PushRulesEvent,
|
||||
receipt::ReceiptEvent,
|
||||
room::{
|
||||
aliases::AliasesEvent,
|
||||
avatar::AvatarEvent,
|
||||
canonical_alias::CanonicalAliasEvent,
|
||||
join_rules::JoinRulesEvent,
|
||||
member::{MemberEvent, MemberEventContent},
|
||||
message::{feedback::FeedbackEvent, MessageEvent},
|
||||
name::NameEvent,
|
||||
power_levels::PowerLevelsEvent,
|
||||
redaction::RedactionEvent,
|
||||
tombstone::TombstoneEvent,
|
||||
use crate::{
|
||||
events::{
|
||||
call::{
|
||||
answer::AnswerEventContent, candidates::CandidatesEventContent,
|
||||
hangup::HangupEventContent, invite::InviteEventContent,
|
||||
},
|
||||
custom::CustomEventContent,
|
||||
fully_read::FullyReadEventContent,
|
||||
ignored_user_list::IgnoredUserListEventContent,
|
||||
presence::PresenceEvent,
|
||||
push_rules::PushRulesEventContent,
|
||||
receipt::ReceiptEventContent,
|
||||
room::{
|
||||
aliases::AliasesEventContent,
|
||||
avatar::AvatarEventContent,
|
||||
canonical_alias::CanonicalAliasEventContent,
|
||||
join_rules::JoinRulesEventContent,
|
||||
member::MemberEventContent,
|
||||
message::{feedback::FeedbackEventContent, MessageEventContent as MsgEventContent},
|
||||
name::NameEventContent,
|
||||
power_levels::PowerLevelsEventContent,
|
||||
redaction::SyncRedactionEvent,
|
||||
tombstone::TombstoneEventContent,
|
||||
},
|
||||
typing::TypingEventContent,
|
||||
BasicEvent, StrippedStateEvent, SyncEphemeralRoomEvent, SyncMessageEvent, SyncStateEvent,
|
||||
},
|
||||
stripped::{
|
||||
StrippedRoomAliases, StrippedRoomAvatar, StrippedRoomCanonicalAlias, StrippedRoomJoinRules,
|
||||
StrippedRoomMember, StrippedRoomName, StrippedRoomPowerLevels,
|
||||
},
|
||||
typing::TypingEvent,
|
||||
Room, RoomState,
|
||||
};
|
||||
use crate::{Room, RoomState};
|
||||
use matrix_sdk_common::async_trait;
|
||||
|
||||
/// Type alias for `RoomState` enum when passed to `EventEmitter` methods.
|
||||
pub type SyncRoom = RoomState<Arc<RwLock<Room>>>;
|
||||
|
||||
/// This represents the various "unrecognized" events.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum CustomEvent<'c> {
|
||||
/// A custom basic event.
|
||||
Basic(&'c BasicEvent<CustomEventContent>),
|
||||
/// A custom basic event.
|
||||
EphemeralRoom(&'c SyncEphemeralRoomEvent<CustomEventContent>),
|
||||
/// A custom room event.
|
||||
Message(&'c SyncMessageEvent<CustomEventContent>),
|
||||
/// A custom state event.
|
||||
State(&'c SyncStateEvent<CustomEventContent>),
|
||||
/// A custom stripped state event.
|
||||
StrippedState(&'c StrippedStateEvent<CustomEventContent>),
|
||||
}
|
||||
|
||||
/// This trait allows any type implementing `EventEmitter` to specify event callbacks for each event.
|
||||
/// The `Client` calls each method when the corresponding event is received.
|
||||
///
|
||||
@@ -56,19 +77,20 @@ pub type SyncRoom = RoomState<Arc<RwLock<Room>>>;
|
||||
/// # use matrix_sdk_base::{
|
||||
/// # self,
|
||||
/// # events::{
|
||||
/// # room::message::{MessageEvent, MessageEventContent, TextMessageEventContent},
|
||||
/// # room::message::{MessageEventContent, TextMessageEventContent},
|
||||
/// # SyncMessageEvent
|
||||
/// # },
|
||||
/// # EventEmitter, SyncRoom
|
||||
/// # };
|
||||
/// # use matrix_sdk_common::locks::RwLock;
|
||||
/// # use matrix_sdk_common::{async_trait, locks::RwLock};
|
||||
///
|
||||
/// struct EventCallback;
|
||||
///
|
||||
/// #[async_trait::async_trait]
|
||||
/// #[async_trait]
|
||||
/// impl EventEmitter for EventCallback {
|
||||
/// async fn on_room_message(&self, room: SyncRoom, event: &MessageEvent) {
|
||||
/// async fn on_room_message(&self, room: SyncRoom, event: &SyncMessageEvent<MessageEventContent>) {
|
||||
/// if let SyncRoom::Joined(room) = room {
|
||||
/// if let MessageEvent {
|
||||
/// if let SyncMessageEvent {
|
||||
/// content: MessageEventContent::Text(TextMessageEventContent { body: msg_body, .. }),
|
||||
/// sender,
|
||||
/// ..
|
||||
@@ -76,7 +98,7 @@ pub type SyncRoom = RoomState<Arc<RwLock<Room>>>;
|
||||
/// {
|
||||
/// let name = {
|
||||
/// let room = room.read().await;
|
||||
/// let member = room.members.get(&sender).unwrap();
|
||||
/// let member = room.joined_members.get(&sender).unwrap();
|
||||
/// member
|
||||
/// .display_name
|
||||
/// .as_ref()
|
||||
@@ -89,94 +111,188 @@ pub type SyncRoom = RoomState<Arc<RwLock<Room>>>;
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
#[async_trait::async_trait]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait EventEmitter: Send + Sync {
|
||||
// ROOM EVENTS from `IncomingTimeline`
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomMember` event.
|
||||
async fn on_room_member(&self, _: SyncRoom, _: &MemberEvent) {}
|
||||
async fn on_room_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomName` event.
|
||||
async fn on_room_name(&self, _: SyncRoom, _: &NameEvent) {}
|
||||
async fn on_room_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomCanonicalAlias` event.
|
||||
async fn on_room_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {}
|
||||
async fn on_room_canonical_alias(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncStateEvent<CanonicalAliasEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomAliases` event.
|
||||
async fn on_room_aliases(&self, _: SyncRoom, _: &AliasesEvent) {}
|
||||
async fn on_room_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomAvatar` event.
|
||||
async fn on_room_avatar(&self, _: SyncRoom, _: &AvatarEvent) {}
|
||||
async fn on_room_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomMessage` event.
|
||||
async fn on_room_message(&self, _: SyncRoom, _: &MessageEvent) {}
|
||||
async fn on_room_message(&self, _: SyncRoom, _: &SyncMessageEvent<MsgEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomMessageFeedback` event.
|
||||
async fn on_room_message_feedback(&self, _: SyncRoom, _: &FeedbackEvent) {}
|
||||
async fn on_room_message_feedback(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncMessageEvent<FeedbackEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `RoomEvent::CallInvite` event
|
||||
async fn on_room_call_invite(&self, _: SyncRoom, _: &SyncMessageEvent<InviteEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::CallAnswer` event
|
||||
async fn on_room_call_answer(&self, _: SyncRoom, _: &SyncMessageEvent<AnswerEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::CallCandidates` event
|
||||
async fn on_room_call_candidates(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncMessageEvent<CandidatesEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `RoomEvent::CallHangup` event
|
||||
async fn on_room_call_hangup(&self, _: SyncRoom, _: &SyncMessageEvent<HangupEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomRedaction` event.
|
||||
async fn on_room_redaction(&self, _: SyncRoom, _: &RedactionEvent) {}
|
||||
async fn on_room_redaction(&self, _: SyncRoom, _: &SyncRedactionEvent) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::RoomPowerLevels` event.
|
||||
async fn on_room_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {}
|
||||
async fn on_room_power_levels(&self, _: SyncRoom, _: &SyncStateEvent<PowerLevelsEventContent>) {
|
||||
}
|
||||
/// Fires when `Client` receives a `RoomEvent::Tombstone` event.
|
||||
async fn on_room_tombstone(&self, _: SyncRoom, _: &TombstoneEvent) {}
|
||||
async fn on_room_join_rules(&self, _: SyncRoom, _: &SyncStateEvent<JoinRulesEventContent>) {}
|
||||
/// Fires when `Client` receives a `RoomEvent::Tombstone` event.
|
||||
async fn on_room_tombstone(&self, _: SyncRoom, _: &SyncStateEvent<TombstoneEventContent>) {}
|
||||
|
||||
// `RoomEvent`s from `IncomingState`
|
||||
/// Fires when `Client` receives a `StateEvent::RoomMember` event.
|
||||
async fn on_state_member(&self, _: SyncRoom, _: &MemberEvent) {}
|
||||
async fn on_state_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomName` event.
|
||||
async fn on_state_name(&self, _: SyncRoom, _: &NameEvent) {}
|
||||
async fn on_state_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomCanonicalAlias` event.
|
||||
async fn on_state_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {}
|
||||
async fn on_state_canonical_alias(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncStateEvent<CanonicalAliasEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomAliases` event.
|
||||
async fn on_state_aliases(&self, _: SyncRoom, _: &AliasesEvent) {}
|
||||
async fn on_state_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomAvatar` event.
|
||||
async fn on_state_avatar(&self, _: SyncRoom, _: &AvatarEvent) {}
|
||||
async fn on_state_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomPowerLevels` event.
|
||||
async fn on_state_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {}
|
||||
async fn on_state_power_levels(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncStateEvent<PowerLevelsEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `StateEvent::RoomJoinRules` event.
|
||||
async fn on_state_join_rules(&self, _: SyncRoom, _: &JoinRulesEvent) {}
|
||||
async fn on_state_join_rules(&self, _: SyncRoom, _: &SyncStateEvent<JoinRulesEventContent>) {}
|
||||
|
||||
// `AnyStrippedStateEvent`s
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomMember` event.
|
||||
async fn on_stripped_state_member(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedRoomMember,
|
||||
_: &StrippedStateEvent<MemberEventContent>,
|
||||
_: Option<MemberEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomName` event.
|
||||
async fn on_stripped_state_name(&self, _: SyncRoom, _: &StrippedRoomName) {}
|
||||
async fn on_stripped_state_name(&self, _: SyncRoom, _: &StrippedStateEvent<NameEventContent>) {}
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomCanonicalAlias` event.
|
||||
async fn on_stripped_state_canonical_alias(&self, _: SyncRoom, _: &StrippedRoomCanonicalAlias) {
|
||||
async fn on_stripped_state_canonical_alias(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedStateEvent<CanonicalAliasEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAliases` event.
|
||||
async fn on_stripped_state_aliases(&self, _: SyncRoom, _: &StrippedRoomAliases) {}
|
||||
async fn on_stripped_state_aliases(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedStateEvent<AliasesEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAvatar` event.
|
||||
async fn on_stripped_state_avatar(&self, _: SyncRoom, _: &StrippedRoomAvatar) {}
|
||||
async fn on_stripped_state_avatar(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedStateEvent<AvatarEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomPowerLevels` event.
|
||||
async fn on_stripped_state_power_levels(&self, _: SyncRoom, _: &StrippedRoomPowerLevels) {}
|
||||
async fn on_stripped_state_power_levels(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedStateEvent<PowerLevelsEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomJoinRules` event.
|
||||
async fn on_stripped_state_join_rules(&self, _: SyncRoom, _: &StrippedRoomJoinRules) {}
|
||||
async fn on_stripped_state_join_rules(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedStateEvent<JoinRulesEventContent>,
|
||||
) {
|
||||
}
|
||||
|
||||
// `NonRoomEvent` (this is a type alias from ruma_events)
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomMember` event.
|
||||
async fn on_account_presence(&self, _: SyncRoom, _: &PresenceEvent) {}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomPresence` event.
|
||||
async fn on_non_room_presence(&self, _: SyncRoom, _: &PresenceEvent) {}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomName` event.
|
||||
async fn on_account_ignored_users(&self, _: SyncRoom, _: &IgnoredUserListEvent) {}
|
||||
async fn on_non_room_ignored_users(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &BasicEvent<IgnoredUserListEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomCanonicalAlias` event.
|
||||
async fn on_account_push_rules(&self, _: SyncRoom, _: &PushRulesEvent) {}
|
||||
async fn on_non_room_push_rules(&self, _: SyncRoom, _: &BasicEvent<PushRulesEventContent>) {}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomAliases` event.
|
||||
async fn on_account_data_fully_read(&self, _: SyncRoom, _: &FullyReadEvent) {}
|
||||
async fn on_non_room_fully_read(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncEphemeralRoomEvent<FullyReadEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::Typing` event.
|
||||
async fn on_account_data_typing(&self, _: SyncRoom, _: &TypingEvent) {}
|
||||
async fn on_non_room_typing(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncEphemeralRoomEvent<TypingEventContent>,
|
||||
) {
|
||||
}
|
||||
/// Fires when `Client` receives a `NonRoomEvent::Receipt` event.
|
||||
///
|
||||
/// This is always a read receipt.
|
||||
async fn on_account_data_receipt(&self, _: SyncRoom, _: &ReceiptEvent) {}
|
||||
async fn on_non_room_receipt(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncEphemeralRoomEvent<ReceiptEventContent>,
|
||||
) {
|
||||
}
|
||||
|
||||
// `PresenceEvent` is a struct so there is only the one method
|
||||
/// Fires when `Client` receives a `NonRoomEvent::RoomAliases` event.
|
||||
async fn on_presence_event(&self, _: SyncRoom, _: &PresenceEvent) {}
|
||||
|
||||
/// Fires when `Client` receives a `Event::Custom` event or if deserialization fails
|
||||
/// because the event was unknown to ruma.
|
||||
///
|
||||
/// The only guarantee this method can give about the event is that it is valid JSON.
|
||||
async fn on_unrecognized_event(&self, _: SyncRoom, _: &RawJsonValue) {}
|
||||
|
||||
/// Fires when `Client` receives a `Event::Custom` event or if deserialization fails
|
||||
/// because the event was unknown to ruma.
|
||||
///
|
||||
/// The only guarantee this method can give about the event is that it is in the
|
||||
/// shape of a valid matrix event.
|
||||
async fn on_custom_event(&self, _: SyncRoom, _: &CustomEvent<'_>) {}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
use matrix_sdk_common::{async_trait, locks::Mutex};
|
||||
use matrix_sdk_test::{async_test, sync_response, SyncResponseFile};
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -186,65 +302,108 @@ mod test {
|
||||
#[derive(Clone)]
|
||||
pub struct EvEmitterTest(Arc<Mutex<Vec<String>>>);
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EventEmitter for EvEmitterTest {
|
||||
async fn on_room_member(&self, _: SyncRoom, _: &MemberEvent) {
|
||||
async fn on_room_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {
|
||||
self.0.lock().await.push("member".to_string())
|
||||
}
|
||||
async fn on_room_name(&self, _: SyncRoom, _: &NameEvent) {
|
||||
async fn on_room_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {
|
||||
self.0.lock().await.push("name".to_string())
|
||||
}
|
||||
async fn on_room_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {
|
||||
async fn on_room_canonical_alias(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncStateEvent<CanonicalAliasEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("canonical".to_string())
|
||||
}
|
||||
async fn on_room_aliases(&self, _: SyncRoom, _: &AliasesEvent) {
|
||||
async fn on_room_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {
|
||||
self.0.lock().await.push("aliases".to_string())
|
||||
}
|
||||
async fn on_room_avatar(&self, _: SyncRoom, _: &AvatarEvent) {
|
||||
async fn on_room_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {
|
||||
self.0.lock().await.push("avatar".to_string())
|
||||
}
|
||||
async fn on_room_message(&self, _: SyncRoom, _: &MessageEvent) {
|
||||
async fn on_room_message(&self, _: SyncRoom, _: &SyncMessageEvent<MsgEventContent>) {
|
||||
self.0.lock().await.push("message".to_string())
|
||||
}
|
||||
async fn on_room_message_feedback(&self, _: SyncRoom, _: &FeedbackEvent) {
|
||||
async fn on_room_message_feedback(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncMessageEvent<FeedbackEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("feedback".to_string())
|
||||
}
|
||||
async fn on_room_redaction(&self, _: SyncRoom, _: &RedactionEvent) {
|
||||
async fn on_room_call_invite(&self, _: SyncRoom, _: &SyncMessageEvent<InviteEventContent>) {
|
||||
self.0.lock().await.push("call invite".to_string())
|
||||
}
|
||||
async fn on_room_call_answer(&self, _: SyncRoom, _: &SyncMessageEvent<AnswerEventContent>) {
|
||||
self.0.lock().await.push("call answer".to_string())
|
||||
}
|
||||
async fn on_room_call_candidates(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncMessageEvent<CandidatesEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("call candidates".to_string())
|
||||
}
|
||||
async fn on_room_call_hangup(&self, _: SyncRoom, _: &SyncMessageEvent<HangupEventContent>) {
|
||||
self.0.lock().await.push("call hangup".to_string())
|
||||
}
|
||||
async fn on_room_redaction(&self, _: SyncRoom, _: &SyncRedactionEvent) {
|
||||
self.0.lock().await.push("redaction".to_string())
|
||||
}
|
||||
async fn on_room_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {
|
||||
async fn on_room_power_levels(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncStateEvent<PowerLevelsEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("power".to_string())
|
||||
}
|
||||
async fn on_room_tombstone(&self, _: SyncRoom, _: &TombstoneEvent) {
|
||||
async fn on_room_tombstone(&self, _: SyncRoom, _: &SyncStateEvent<TombstoneEventContent>) {
|
||||
self.0.lock().await.push("tombstone".to_string())
|
||||
}
|
||||
|
||||
async fn on_state_member(&self, _: SyncRoom, _: &MemberEvent) {
|
||||
async fn on_state_member(&self, _: SyncRoom, _: &SyncStateEvent<MemberEventContent>) {
|
||||
self.0.lock().await.push("state member".to_string())
|
||||
}
|
||||
async fn on_state_name(&self, _: SyncRoom, _: &NameEvent) {
|
||||
async fn on_state_name(&self, _: SyncRoom, _: &SyncStateEvent<NameEventContent>) {
|
||||
self.0.lock().await.push("state name".to_string())
|
||||
}
|
||||
async fn on_state_canonical_alias(&self, _: SyncRoom, _: &CanonicalAliasEvent) {
|
||||
async fn on_state_canonical_alias(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncStateEvent<CanonicalAliasEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("state canonical".to_string())
|
||||
}
|
||||
async fn on_state_aliases(&self, _: SyncRoom, _: &AliasesEvent) {
|
||||
async fn on_state_aliases(&self, _: SyncRoom, _: &SyncStateEvent<AliasesEventContent>) {
|
||||
self.0.lock().await.push("state aliases".to_string())
|
||||
}
|
||||
async fn on_state_avatar(&self, _: SyncRoom, _: &AvatarEvent) {
|
||||
async fn on_state_avatar(&self, _: SyncRoom, _: &SyncStateEvent<AvatarEventContent>) {
|
||||
self.0.lock().await.push("state avatar".to_string())
|
||||
}
|
||||
async fn on_state_power_levels(&self, _: SyncRoom, _: &PowerLevelsEvent) {
|
||||
async fn on_state_power_levels(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncStateEvent<PowerLevelsEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("state power".to_string())
|
||||
}
|
||||
async fn on_state_join_rules(&self, _: SyncRoom, _: &JoinRulesEvent) {
|
||||
async fn on_state_join_rules(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncStateEvent<JoinRulesEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("state rules".to_string())
|
||||
}
|
||||
|
||||
// `AnyStrippedStateEvent`s
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomMember` event.
|
||||
async fn on_stripped_state_member(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedRoomMember,
|
||||
_: &StrippedStateEvent<MemberEventContent>,
|
||||
_: Option<MemberEventContent>,
|
||||
) {
|
||||
self.0
|
||||
@@ -252,65 +411,116 @@ mod test {
|
||||
.await
|
||||
.push("stripped state member".to_string())
|
||||
}
|
||||
async fn on_stripped_state_name(&self, _: SyncRoom, _: &StrippedRoomName) {
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomName` event.
|
||||
async fn on_stripped_state_name(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedStateEvent<NameEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("stripped state name".to_string())
|
||||
}
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomCanonicalAlias` event.
|
||||
async fn on_stripped_state_canonical_alias(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedRoomCanonicalAlias,
|
||||
_: &StrippedStateEvent<CanonicalAliasEventContent>,
|
||||
) {
|
||||
self.0
|
||||
.lock()
|
||||
.await
|
||||
.push("stripped state canonical".to_string())
|
||||
}
|
||||
async fn on_stripped_state_aliases(&self, _: SyncRoom, _: &StrippedRoomAliases) {
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAliases` event.
|
||||
async fn on_stripped_state_aliases(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedStateEvent<AliasesEventContent>,
|
||||
) {
|
||||
self.0
|
||||
.lock()
|
||||
.await
|
||||
.push("stripped state aliases".to_string())
|
||||
}
|
||||
async fn on_stripped_state_avatar(&self, _: SyncRoom, _: &StrippedRoomAvatar) {
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomAvatar` event.
|
||||
async fn on_stripped_state_avatar(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedStateEvent<AvatarEventContent>,
|
||||
) {
|
||||
self.0
|
||||
.lock()
|
||||
.await
|
||||
.push("stripped state avatar".to_string())
|
||||
}
|
||||
async fn on_stripped_state_power_levels(&self, _: SyncRoom, _: &StrippedRoomPowerLevels) {
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomPowerLevels` event.
|
||||
async fn on_stripped_state_power_levels(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedStateEvent<PowerLevelsEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("stripped state power".to_string())
|
||||
}
|
||||
async fn on_stripped_state_join_rules(&self, _: SyncRoom, _: &StrippedRoomJoinRules) {
|
||||
/// Fires when `Client` receives a `AnyStrippedStateEvent::StrippedRoomJoinRules` event.
|
||||
async fn on_stripped_state_join_rules(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &StrippedStateEvent<JoinRulesEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("stripped state rules".to_string())
|
||||
}
|
||||
|
||||
async fn on_account_presence(&self, _: SyncRoom, _: &PresenceEvent) {
|
||||
self.0.lock().await.push("account presence".to_string())
|
||||
async fn on_non_room_presence(&self, _: SyncRoom, _: &PresenceEvent) {
|
||||
self.0.lock().await.push("presence".to_string())
|
||||
}
|
||||
async fn on_account_ignored_users(&self, _: SyncRoom, _: &IgnoredUserListEvent) {
|
||||
async fn on_non_room_ignored_users(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &BasicEvent<IgnoredUserListEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("account ignore".to_string())
|
||||
}
|
||||
async fn on_account_push_rules(&self, _: SyncRoom, _: &PushRulesEvent) {
|
||||
async fn on_non_room_push_rules(&self, _: SyncRoom, _: &BasicEvent<PushRulesEventContent>) {
|
||||
self.0.lock().await.push("account push rules".to_string())
|
||||
}
|
||||
async fn on_account_data_fully_read(&self, _: SyncRoom, _: &FullyReadEvent) {
|
||||
async fn on_non_room_fully_read(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncEphemeralRoomEvent<FullyReadEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("account read".to_string())
|
||||
}
|
||||
async fn on_non_room_typing(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncEphemeralRoomEvent<TypingEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("typing event".to_string())
|
||||
}
|
||||
async fn on_non_room_receipt(
|
||||
&self,
|
||||
_: SyncRoom,
|
||||
_: &SyncEphemeralRoomEvent<ReceiptEventContent>,
|
||||
) {
|
||||
self.0.lock().await.push("receipt event".to_string())
|
||||
}
|
||||
async fn on_presence_event(&self, _: SyncRoom, _: &PresenceEvent) {
|
||||
self.0.lock().await.push("presence event".to_string())
|
||||
}
|
||||
async fn on_unrecognized_event(&self, _: SyncRoom, _: &RawJsonValue) {
|
||||
self.0.lock().await.push("unrecognized event".to_string())
|
||||
}
|
||||
async fn on_custom_event(&self, _: SyncRoom, _: &CustomEvent<'_>) {
|
||||
self.0.lock().await.push("custom event".to_string())
|
||||
}
|
||||
}
|
||||
|
||||
use crate::identifiers::UserId;
|
||||
use crate::{BaseClient, Session};
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use crate::{identifiers::user_id, BaseClient, Session};
|
||||
|
||||
async fn get_client() -> BaseClient {
|
||||
let session = Session {
|
||||
access_token: "1234".to_owned(),
|
||||
user_id: UserId::try_from("@example:example.com").unwrap(),
|
||||
device_id: "DEVICEID".to_owned(),
|
||||
user_id: user_id!("@example:example.com"),
|
||||
device_id: "DEVICEID".into(),
|
||||
};
|
||||
let client = BaseClient::new().unwrap();
|
||||
client.restore_login(session).await.unwrap();
|
||||
@@ -341,9 +551,10 @@ mod test {
|
||||
"state member",
|
||||
"state member",
|
||||
"message",
|
||||
"account read",
|
||||
"account ignore",
|
||||
"presence event"
|
||||
"presence event",
|
||||
"receipt event",
|
||||
"account read",
|
||||
],
|
||||
)
|
||||
}
|
||||
@@ -394,4 +605,57 @@ mod test {
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn event_emitter_more_events() {
|
||||
let vec = Arc::new(Mutex::new(Vec::new()));
|
||||
let test_vec = Arc::clone(&vec);
|
||||
let emitter = Box::new(EvEmitterTest(vec));
|
||||
|
||||
let client = get_client().await;
|
||||
client.add_event_emitter(emitter).await;
|
||||
|
||||
let mut response = sync_response(SyncResponseFile::All);
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
let v = test_vec.lock().await;
|
||||
assert_eq!(
|
||||
v.as_slice(),
|
||||
[
|
||||
"message",
|
||||
"message", // this is a message edit event
|
||||
"redaction",
|
||||
"unrecognized event",
|
||||
// "unrecognized event", this is actually a redacted "m.room.messages" event
|
||||
|
||||
// the ephemeral room events are looped over after the room events
|
||||
"receipt event",
|
||||
"typing event"
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn event_emitter_voip() {
|
||||
let vec = Arc::new(Mutex::new(Vec::new()));
|
||||
let test_vec = Arc::clone(&vec);
|
||||
let emitter = Box::new(EvEmitterTest(vec));
|
||||
|
||||
let client = get_client().await;
|
||||
client.add_event_emitter(emitter).await;
|
||||
|
||||
let mut response = sync_response(SyncResponseFile::Voip);
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
let v = test_vec.lock().await;
|
||||
assert_eq!(
|
||||
v.as_slice(),
|
||||
[
|
||||
"call invite",
|
||||
"call answer",
|
||||
"call candidates",
|
||||
"call hangup",
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
//! The following crate feature flags are available:
|
||||
//!
|
||||
//! * `encryption`: Enables end-to-end encryption support in the library.
|
||||
//! * `sqlite-cryptostore`: Enables a SQLite based store for the encryption
|
||||
//! * `sqlite_cryptostore`: Enables a SQLite based store for the encryption
|
||||
//! keys. If this is disabled and `encryption` support is enabled the keys will
|
||||
//! by default be stored only in memory and thus lost after the client is
|
||||
//! destroyed.
|
||||
//! * `unstable-synapse-quirks`: Enables support to deal with inconsistencies
|
||||
//! of Synapse in compliance with the Matrix API specification.
|
||||
#![deny(
|
||||
missing_debug_implementations,
|
||||
dead_code,
|
||||
@@ -34,8 +36,12 @@
|
||||
unused_import_braces,
|
||||
unused_qualifications
|
||||
)]
|
||||
#![cfg_attr(feature = "docs", feature(doc_cfg))]
|
||||
|
||||
pub use crate::{error::Error, error::Result, session::Session};
|
||||
pub use crate::{
|
||||
error::{Error, Result},
|
||||
session::Session,
|
||||
};
|
||||
pub use matrix_sdk_common::*;
|
||||
|
||||
mod client;
|
||||
@@ -46,10 +52,18 @@ mod session;
|
||||
mod state;
|
||||
|
||||
pub use client::{BaseClient, BaseClientConfig, RoomState, RoomStateType};
|
||||
pub use event_emitter::{EventEmitter, SyncRoom};
|
||||
pub use event_emitter::{CustomEvent, EventEmitter, SyncRoom};
|
||||
pub use models::{Room, RoomMember};
|
||||
pub use state::{AllRooms, ClientState};
|
||||
|
||||
#[cfg(feature = "encryption")]
|
||||
pub use matrix_sdk_crypto::{Device, TrustState};
|
||||
pub use models::Room;
|
||||
#[cfg_attr(feature = "docs", doc(cfg(encryption)))]
|
||||
pub use matrix_sdk_crypto as crypto;
|
||||
|
||||
#[cfg(feature = "messages")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(messages)))]
|
||||
pub use models::{MessageQueue, PossiblyRedactedExt};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use state::JsonStore;
|
||||
pub use state::StateStore;
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
//! De-/serialization functions to and from json strings, allows the type to be used as a query string.
|
||||
|
||||
use serde::de::{Deserialize, Deserializer, Error as _};
|
||||
|
||||
use crate::events::collections::all::Event;
|
||||
use crate::events::presence::PresenceEvent;
|
||||
use crate::events::EventJson;
|
||||
|
||||
pub fn deserialize_events<'de, D>(deserializer: D) -> Result<Vec<Event>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let mut events = vec![];
|
||||
let ev = Vec::<EventJson<Event>>::deserialize(deserializer)?;
|
||||
for event in ev {
|
||||
events.push(event.deserialize().map_err(D::Error::custom)?);
|
||||
}
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
pub fn deserialize_presence<'de, D>(deserializer: D) -> Result<Vec<PresenceEvent>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let mut events = vec![];
|
||||
let ev = Vec::<EventJson<PresenceEvent>>::deserialize(deserializer)?;
|
||||
for event in ev {
|
||||
events.push(event.deserialize().map_err(D::Error::custom)?);
|
||||
}
|
||||
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::fs;
|
||||
|
||||
use crate::events::room::member::MemberEvent;
|
||||
use crate::events::EventJson;
|
||||
use crate::models::RoomMember;
|
||||
|
||||
#[test]
|
||||
fn events_and_presence_deserialization() {
|
||||
let ev_json = fs::read_to_string("../test_data/events/member.json").unwrap();
|
||||
let ev = serde_json::from_str::<EventJson<MemberEvent>>(&ev_json)
|
||||
.unwrap()
|
||||
.deserialize()
|
||||
.unwrap();
|
||||
let member = RoomMember::new(&ev);
|
||||
|
||||
let member_json = serde_json::to_string(&member).unwrap();
|
||||
let mem = serde_json::from_str::<RoomMember>(&member_json).unwrap();
|
||||
assert_eq!(member, mem);
|
||||
}
|
||||
}
|
||||
@@ -3,104 +3,112 @@
|
||||
//! The `Room` struct optionally holds a `MessageQueue` if the "messages"
|
||||
//! feature is enabled.
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::ops::Deref;
|
||||
use std::vec::IntoIter;
|
||||
|
||||
use crate::events::room::message::MessageEvent;
|
||||
use crate::events::EventJson;
|
||||
use std::{time::SystemTime, vec::IntoIter};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
events::AnyPossiblyRedactedSyncMessageEvent,
|
||||
identifiers::{EventId, UserId},
|
||||
};
|
||||
use serde::{de, ser, Serialize};
|
||||
|
||||
/// A queue that holds the 10 most recent messages received from the server.
|
||||
/// Exposes some of the field access methods found in the event held by
|
||||
/// `AnyPossiblyRedacted*` enums.
|
||||
///
|
||||
/// This is just an extension trait to ease the use of certain event enums.
|
||||
pub trait PossiblyRedactedExt {
|
||||
/// Access the redacted or full event's `event_id` field.
|
||||
fn event_id(&self) -> &EventId;
|
||||
/// Access the redacted or full event's `origin_server_ts` field.
|
||||
fn origin_server_ts(&self) -> &SystemTime;
|
||||
/// Access the redacted or full event's `sender` field.
|
||||
fn sender(&self) -> &UserId;
|
||||
}
|
||||
|
||||
impl PossiblyRedactedExt for AnyPossiblyRedactedSyncMessageEvent {
|
||||
/// Access the underlying event's `event_id`.
|
||||
fn event_id(&self) -> &EventId {
|
||||
match self {
|
||||
Self::Regular(e) => e.event_id(),
|
||||
Self::Redacted(e) => e.event_id(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Access the underlying event's `origin_server_ts`.
|
||||
fn origin_server_ts(&self) -> &SystemTime {
|
||||
match self {
|
||||
Self::Regular(e) => e.origin_server_ts(),
|
||||
Self::Redacted(e) => e.origin_server_ts(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Access the underlying event's `sender`.
|
||||
fn sender(&self) -> &UserId {
|
||||
match self {
|
||||
Self::Regular(e) => e.sender(),
|
||||
Self::Redacted(e) => e.sender(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MESSAGE_QUEUE_CAP: usize = 35;
|
||||
|
||||
/// A queue that holds the 35 most recent messages received from the server.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct MessageQueue {
|
||||
msgs: Vec<MessageWrapper>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct MessageWrapper(MessageEvent);
|
||||
|
||||
impl Deref for MessageWrapper {
|
||||
type Target = MessageEvent;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for MessageWrapper {
|
||||
fn eq(&self, other: &MessageWrapper) -> bool {
|
||||
self.0.event_id == other.0.event_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for MessageWrapper {}
|
||||
|
||||
impl PartialOrd for MessageWrapper {
|
||||
fn partial_cmp(&self, other: &MessageWrapper) -> Option<Ordering> {
|
||||
Some(self.0.origin_server_ts.cmp(&other.0.origin_server_ts))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for MessageWrapper {
|
||||
fn cmp(&self, other: &MessageWrapper) -> Ordering {
|
||||
self.partial_cmp(other).unwrap_or(Ordering::Equal)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for MessageQueue {
|
||||
fn eq(&self, other: &MessageQueue) -> bool {
|
||||
self.msgs.len() == other.msgs.len()
|
||||
&& self
|
||||
.msgs
|
||||
.iter()
|
||||
.zip(other.msgs.iter())
|
||||
.all(|(msg_a, msg_b)| msg_a.event_id == msg_b.event_id)
|
||||
}
|
||||
pub(crate) msgs: Vec<AnyPossiblyRedactedSyncMessageEvent>,
|
||||
}
|
||||
|
||||
impl MessageQueue {
|
||||
/// Create a new empty `MessageQueue`.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
msgs: Vec::with_capacity(20),
|
||||
msgs: Vec::with_capacity(45),
|
||||
}
|
||||
}
|
||||
|
||||
/// Inserts a `MessageEvent` into `MessageQueue`, sorted by by `origin_server_ts`.
|
||||
///
|
||||
/// Removes the oldest element in the queue if there are more than 10 elements.
|
||||
pub fn push(&mut self, msg: MessageEvent) -> bool {
|
||||
pub fn push(&mut self, msg: AnyPossiblyRedactedSyncMessageEvent) -> bool {
|
||||
// only push new messages into the queue
|
||||
if let Some(latest) = self.msgs.last() {
|
||||
if msg.origin_server_ts < latest.origin_server_ts && self.msgs.len() >= 10 {
|
||||
if msg.origin_server_ts() < latest.origin_server_ts() && self.msgs.len() >= 10 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let message = MessageWrapper(msg);
|
||||
match self.msgs.binary_search_by(|m| m.cmp(&message)) {
|
||||
Ok(pos) => {
|
||||
if self.msgs[pos] != message {
|
||||
self.msgs.insert(pos, message)
|
||||
}
|
||||
}
|
||||
Err(pos) => self.msgs.insert(pos, message),
|
||||
if self.msgs.iter().all(|old| old.event_id() != msg.event_id()) {
|
||||
self.msgs.push(msg)
|
||||
}
|
||||
if self.msgs.len() > 10 {
|
||||
self.msgs.remove(0);
|
||||
|
||||
if self.msgs.len() > MESSAGE_QUEUE_CAP {
|
||||
self.msgs.pop();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn iter(&self) -> impl Iterator<Item = &MessageWrapper> {
|
||||
/// Iterate over the messages in the queue.
|
||||
pub fn iter(&self) -> impl Iterator<Item = &AnyPossiblyRedactedSyncMessageEvent> {
|
||||
self.msgs.iter()
|
||||
}
|
||||
|
||||
/// Iterate over each message mutably.
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut AnyPossiblyRedactedSyncMessageEvent> {
|
||||
self.msgs.iter_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for MessageQueue {
|
||||
fn eq(&self, other: &MessageQueue) -> bool {
|
||||
self.msgs
|
||||
.iter()
|
||||
.zip(other.msgs.iter())
|
||||
.all(|(a, b)| a.event_id() == b.event_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for MessageQueue {
|
||||
type Item = MessageWrapper;
|
||||
type Item = AnyPossiblyRedactedSyncMessageEvent;
|
||||
type IntoIter = IntoIter<Self::Item>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
@@ -109,23 +117,38 @@ impl IntoIterator for MessageQueue {
|
||||
}
|
||||
|
||||
pub(crate) mod ser_deser {
|
||||
use std::fmt;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct MessageQueueDeserializer;
|
||||
|
||||
impl<'de> de::Visitor<'de> for MessageQueueDeserializer {
|
||||
type Value = MessageQueue;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("an array of message events")
|
||||
}
|
||||
|
||||
fn visit_seq<S>(self, mut access: S) -> Result<Self::Value, S::Error>
|
||||
where
|
||||
S: de::SeqAccess<'de>,
|
||||
{
|
||||
let mut msgs = Vec::with_capacity(access.size_hint().unwrap_or(0));
|
||||
|
||||
while let Some(msg) = access.next_element::<AnyPossiblyRedactedSyncMessageEvent>()? {
|
||||
msgs.push(msg);
|
||||
}
|
||||
|
||||
Ok(MessageQueue { msgs })
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<MessageQueue, D::Error>
|
||||
where
|
||||
D: de::Deserializer<'de>,
|
||||
{
|
||||
use serde::de::Error;
|
||||
|
||||
let messages: Vec<EventJson<MessageEvent>> = de::Deserialize::deserialize(deserializer)?;
|
||||
|
||||
let mut msgs = vec![];
|
||||
for json in messages {
|
||||
let msg = json.deserialize().map_err(D::Error::custom)?;
|
||||
msgs.push(MessageWrapper(msg));
|
||||
}
|
||||
|
||||
Ok(MessageQueue { msgs })
|
||||
deserializer.deserialize_seq(MessageQueueDeserializer)
|
||||
}
|
||||
|
||||
pub fn serialize<S>(msgs: &MessageQueue, serializer: S) -> Result<S::Ok, S::Error>
|
||||
@@ -138,37 +161,34 @@ pub(crate) mod ser_deser {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use matrix_sdk_common::{
|
||||
events::{AnyPossiblyRedactedSyncMessageEvent, AnySyncMessageEvent},
|
||||
identifiers::{room_id, user_id, RoomId},
|
||||
};
|
||||
use matrix_sdk_test::test_json;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
use crate::events::{collections::all::RoomEvent, EventJson};
|
||||
use crate::identifiers::{RoomId, UserId};
|
||||
use super::*;
|
||||
use crate::Room;
|
||||
|
||||
#[test]
|
||||
fn serialize() {
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
let id = room_id!("!roomid:example.com");
|
||||
let user = user_id!("@example:example.com");
|
||||
|
||||
let mut room = Room::new(&id, &user);
|
||||
|
||||
let json = std::fs::read_to_string("../test_data/events/message_text.json").unwrap();
|
||||
let event = serde_json::from_str::<EventJson<RoomEvent>>(&json).unwrap();
|
||||
let json: &serde_json::Value = &test_json::MESSAGE_TEXT;
|
||||
let msg = AnyPossiblyRedactedSyncMessageEvent::Regular(
|
||||
serde_json::from_value::<AnySyncMessageEvent>(json.clone()).unwrap(),
|
||||
);
|
||||
|
||||
let mut msgs = MessageQueue::new();
|
||||
let message = if let RoomEvent::RoomMessage(msg) = event.deserialize().unwrap() {
|
||||
msgs.push(msg.clone());
|
||||
msg
|
||||
} else {
|
||||
panic!("this should always be a RoomMessage")
|
||||
};
|
||||
room.messages = msgs.clone();
|
||||
|
||||
msgs.push(msg.clone());
|
||||
room.messages = msgs;
|
||||
let mut joined_rooms = HashMap::new();
|
||||
joined_rooms.insert(id, room);
|
||||
|
||||
@@ -186,8 +206,10 @@ mod test {
|
||||
},
|
||||
"own_user_id": "@example:example.com",
|
||||
"creator": null,
|
||||
"members": {},
|
||||
"messages": [ message ],
|
||||
"direct_target": null,
|
||||
"joined_members": {},
|
||||
"invited_members": {},
|
||||
"messages": [ msg ],
|
||||
"typing_users": [],
|
||||
"power_levels": null,
|
||||
"encrypted": null,
|
||||
@@ -202,25 +224,22 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn deserialize() {
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
let id = room_id!("!roomid:example.com");
|
||||
let user = user_id!("@example:example.com");
|
||||
|
||||
let mut room = Room::new(&id, &user);
|
||||
|
||||
let json = std::fs::read_to_string("../test_data/events/message_text.json").unwrap();
|
||||
let event = serde_json::from_str::<EventJson<RoomEvent>>(&json).unwrap();
|
||||
let json: &serde_json::Value = &test_json::MESSAGE_TEXT;
|
||||
let msg = AnyPossiblyRedactedSyncMessageEvent::Regular(
|
||||
serde_json::from_value::<AnySyncMessageEvent>(json.clone()).unwrap(),
|
||||
);
|
||||
|
||||
let mut msgs = MessageQueue::new();
|
||||
let message = if let RoomEvent::RoomMessage(msg) = event.deserialize().unwrap() {
|
||||
msgs.push(msg.clone());
|
||||
msg
|
||||
} else {
|
||||
panic!("this should always be a RoomMessage")
|
||||
};
|
||||
msgs.push(msg.clone());
|
||||
room.messages = msgs;
|
||||
|
||||
let mut joined_rooms = HashMap::new();
|
||||
joined_rooms.insert(id, room.clone());
|
||||
joined_rooms.insert(id, room);
|
||||
|
||||
let json = serde_json::json!({
|
||||
"!roomid:example.com": {
|
||||
@@ -235,8 +254,10 @@ mod test {
|
||||
},
|
||||
"own_user_id": "@example:example.com",
|
||||
"creator": null,
|
||||
"members": {},
|
||||
"messages": [ message ],
|
||||
"direct_target": null,
|
||||
"joined_members": {},
|
||||
"invited_members": {},
|
||||
"messages": [ msg ],
|
||||
"typing_users": [],
|
||||
"power_levels": null,
|
||||
"encrypted": null,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
mod event_deser;
|
||||
#[cfg(feature = "messages")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "messages")))]
|
||||
mod message;
|
||||
mod room;
|
||||
mod room_member;
|
||||
|
||||
#[cfg(feature = "messages")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(messages)))]
|
||||
pub use message::{MessageQueue, PossiblyRedactedExt};
|
||||
pub use room::{Room, RoomName};
|
||||
pub use room_member::RoomMember;
|
||||
|
||||
+1252
-198
File diff suppressed because it is too large
Load Diff
@@ -15,27 +15,26 @@
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use crate::events::collections::all::Event;
|
||||
use crate::events::presence::{PresenceEvent, PresenceEventContent, PresenceState};
|
||||
use crate::events::room::{
|
||||
member::{MemberEvent, MembershipChange, MembershipState},
|
||||
power_levels::PowerLevelsEvent,
|
||||
use matrix_sdk_common::{
|
||||
events::{presence::PresenceEvent, room::member::MemberEventContent, SyncStateEvent},
|
||||
identifiers::{RoomId, UserId},
|
||||
presence::PresenceState,
|
||||
Int, UInt,
|
||||
};
|
||||
use crate::identifiers::UserId;
|
||||
|
||||
use crate::js_int::{Int, UInt};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Notes: if Alice invites Bob into a room we will get an event with the sender as Alice and the state key as Bob.
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[cfg_attr(test, derive(Clone))]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
/// A Matrix room member.
|
||||
///
|
||||
pub struct RoomMember {
|
||||
/// The unique mxid of the user.
|
||||
/// The unique MXID of the user.
|
||||
pub user_id: UserId,
|
||||
/// The human readable name of the user.
|
||||
pub display_name: Option<String>,
|
||||
/// Whether the member's display name is ambiguous due to being shared with
|
||||
/// other members.
|
||||
pub display_name_ambiguous: bool,
|
||||
/// The matrix url of the users avatar.
|
||||
pub avatar_url: Option<String>,
|
||||
/// The time, in ms, since the user interacted with the server.
|
||||
@@ -43,7 +42,7 @@ pub struct RoomMember {
|
||||
/// If the user should be considered active.
|
||||
pub currently_active: Option<bool>,
|
||||
/// The unique id of the room.
|
||||
pub room_id: Option<String>,
|
||||
pub room_id: RoomId,
|
||||
/// If the member is typing.
|
||||
pub typing: Option<bool>,
|
||||
/// The presence of the user, if found.
|
||||
@@ -54,15 +53,19 @@ pub struct RoomMember {
|
||||
pub power_level: Option<Int>,
|
||||
/// The normalized power level of this `RoomMember` (0-100).
|
||||
pub power_level_norm: Option<Int>,
|
||||
/// The `MembershipState` of this `RoomMember`.
|
||||
pub membership: MembershipState,
|
||||
/// The human readable name of this room member.
|
||||
pub name: String,
|
||||
// FIXME: The docstring below is currently a lie since we only store the initial event that
|
||||
// creates the member (the one we pass to RoomMember::new).
|
||||
//
|
||||
// The intent of this field is to keep the last (or last few?) state events related to the room
|
||||
// member cached so we can quickly go back to the previous one in case some of them get
|
||||
// redacted. Keeping all state for each room member is probably too much.
|
||||
//
|
||||
// Needs design.
|
||||
/// The events that created the state of this room member.
|
||||
#[serde(deserialize_with = "super::event_deser::deserialize_events")]
|
||||
pub events: Vec<Event>,
|
||||
pub events: Vec<SyncStateEvent<MemberEventContent>>,
|
||||
/// The `PresenceEvent`s connected to this user.
|
||||
#[serde(deserialize_with = "super::event_deser::deserialize_presence")]
|
||||
pub presence_events: Vec<PresenceEvent>,
|
||||
}
|
||||
|
||||
@@ -73,19 +76,28 @@ impl PartialEq for RoomMember {
|
||||
&& self.user_id == other.user_id
|
||||
&& self.name == other.name
|
||||
&& self.display_name == other.display_name
|
||||
&& self.display_name_ambiguous == other.display_name_ambiguous
|
||||
&& self.avatar_url == other.avatar_url
|
||||
&& self.last_active_ago == other.last_active_ago
|
||||
&& self.membership == other.membership
|
||||
}
|
||||
}
|
||||
|
||||
impl RoomMember {
|
||||
pub fn new(event: &MemberEvent) -> Self {
|
||||
/// Create a new room member.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - event associated with a member joining, leaving or getting
|
||||
/// invited to a room.
|
||||
///
|
||||
/// * `room_id` - The unique id of the room this member is part of.
|
||||
pub fn new(event: &SyncStateEvent<MemberEventContent>, room_id: &RoomId) -> Self {
|
||||
Self {
|
||||
name: event.state_key.clone(),
|
||||
room_id: event.room_id.as_ref().map(|id| id.to_string()),
|
||||
room_id: room_id.clone(),
|
||||
user_id: UserId::try_from(event.state_key.as_str()).unwrap(),
|
||||
display_name: event.content.displayname.clone(),
|
||||
display_name_ambiguous: false,
|
||||
avatar_url: event.content.avatar_url.clone(),
|
||||
presence: None,
|
||||
status_msg: None,
|
||||
@@ -94,149 +106,96 @@ impl RoomMember {
|
||||
typing: None,
|
||||
power_level: None,
|
||||
power_level_norm: None,
|
||||
membership: event.content.membership,
|
||||
presence_events: Vec::default(),
|
||||
events: vec![Event::RoomMember(event.clone())],
|
||||
events: vec![event.clone()],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_member(&mut self, event: &MemberEvent) -> bool {
|
||||
use MembershipChange::*;
|
||||
|
||||
match event.membership_change() {
|
||||
ProfileChanged => {
|
||||
self.display_name = event.content.displayname.clone();
|
||||
self.avatar_url = event.content.avatar_url.clone();
|
||||
true
|
||||
}
|
||||
Banned | Kicked | KickedAndBanned | InvitationRejected | InvitationRevoked | Left
|
||||
| Unbanned | Joined | Invited => {
|
||||
self.membership = event.content.membership;
|
||||
true
|
||||
}
|
||||
NotImplemented => false,
|
||||
None => false,
|
||||
// we ignore the error here as only a buggy or malicious server would send this
|
||||
Error => false,
|
||||
}
|
||||
/// Returns the most ergonomic (but potentially ambiguous/non-unique) name
|
||||
/// available for the member.
|
||||
///
|
||||
/// This is the member's display name if it is set, otherwise their MXID.
|
||||
pub fn name(&self) -> String {
|
||||
self.display_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| format!("{}", self.user_id))
|
||||
}
|
||||
|
||||
pub fn update_power(&mut self, event: &PowerLevelsEvent, max_power: Int) -> bool {
|
||||
let changed;
|
||||
if let Some(user_power) = event.content.users.get(&self.user_id) {
|
||||
changed = self.power_level != Some(*user_power);
|
||||
self.power_level = Some(*user_power);
|
||||
/// Returns a name for the member which is guaranteed to be unique, but not
|
||||
/// necessarily the most ergonomic.
|
||||
///
|
||||
/// This is either a name in the format "DISPLAY_NAME (MXID)" if the
|
||||
/// member's display name is set, or simply "MXID" if not.
|
||||
pub fn unique_name(&self) -> String {
|
||||
self.display_name
|
||||
.clone()
|
||||
.map(|d| format!("{} ({})", d, self.user_id))
|
||||
.unwrap_or_else(|| format!("{}", self.user_id))
|
||||
}
|
||||
|
||||
/// Get the disambiguated display name for the member which is as ergonomic
|
||||
/// as possible while still guaranteeing it is unique.
|
||||
///
|
||||
/// If the member's display name is currently ambiguous (i.e. shared by
|
||||
/// other room members), this method will return the same result as
|
||||
/// `RoomMember::unique_name`. Otherwise, this method will return the same
|
||||
/// result as `RoomMember::name`.
|
||||
///
|
||||
/// This is usually the name you want when showing room messages from the
|
||||
/// member or when showing the member in the member list.
|
||||
///
|
||||
/// **Warning**: When displaying a room member's display name, clients
|
||||
/// *must* use a disambiguated name, so they *must not* use
|
||||
/// `RoomMember::display_name` directly. Clients *should* use this method to
|
||||
/// obtain the name, but an acceptable alternative is to use
|
||||
/// `RoomMember::unique_name` in certain situations.
|
||||
pub fn disambiguated_name(&self) -> String {
|
||||
if self.display_name_ambiguous {
|
||||
self.unique_name()
|
||||
} else {
|
||||
changed = self.power_level != Some(event.content.users_default);
|
||||
self.power_level = Some(event.content.users_default);
|
||||
self.name()
|
||||
}
|
||||
|
||||
if max_power > Int::from(0) {
|
||||
self.power_level_norm = Some((self.power_level.unwrap() * Int::from(100)) / max_power);
|
||||
}
|
||||
|
||||
changed
|
||||
}
|
||||
|
||||
/// If the current `PresenceEvent` updated the state of this `User`.
|
||||
///
|
||||
/// Returns true if the specific users presence has changed, false otherwise.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `presence` - The presence event for a this room member.
|
||||
pub fn did_update_presence(&self, presence: &PresenceEvent) -> bool {
|
||||
let PresenceEvent {
|
||||
content:
|
||||
PresenceEventContent {
|
||||
avatar_url,
|
||||
currently_active,
|
||||
displayname,
|
||||
last_active_ago,
|
||||
presence,
|
||||
status_msg,
|
||||
},
|
||||
..
|
||||
} = presence;
|
||||
self.display_name == *displayname
|
||||
&& self.avatar_url == *avatar_url
|
||||
&& self.presence.as_ref() == Some(presence)
|
||||
&& self.status_msg == *status_msg
|
||||
&& self.last_active_ago == *last_active_ago
|
||||
&& self.currently_active == *currently_active
|
||||
}
|
||||
|
||||
/// Updates the `User`s presence.
|
||||
///
|
||||
/// This should only be used if `did_update_presence` was true.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `presence` - The presence event for a this room member.
|
||||
pub fn update_presence(&mut self, presence_ev: &PresenceEvent) {
|
||||
let PresenceEvent {
|
||||
content:
|
||||
PresenceEventContent {
|
||||
avatar_url,
|
||||
currently_active,
|
||||
displayname,
|
||||
last_active_ago,
|
||||
presence,
|
||||
status_msg,
|
||||
},
|
||||
..
|
||||
} = presence_ev;
|
||||
|
||||
self.presence_events.push(presence_ev.clone());
|
||||
self.avatar_url = avatar_url.clone();
|
||||
self.currently_active = *currently_active;
|
||||
self.display_name = displayname.clone();
|
||||
self.last_active_ago = *last_active_ago;
|
||||
self.presence = Some(*presence);
|
||||
self.status_msg = status_msg.clone();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use matrix_sdk_test::{async_test, EventBuilder, EventsFile};
|
||||
use matrix_sdk_test::{async_test, EventBuilder, EventsJson};
|
||||
|
||||
use crate::events::collections::all::RoomEvent;
|
||||
use crate::events::room::member::MembershipState;
|
||||
use crate::identifiers::{RoomId, UserId};
|
||||
use crate::{BaseClient, Session};
|
||||
|
||||
use crate::js_int::Int;
|
||||
use crate::{
|
||||
identifiers::{room_id, user_id, RoomId},
|
||||
int, BaseClient, Session,
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen_test::*;
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
async fn get_client() -> BaseClient {
|
||||
let session = Session {
|
||||
access_token: "1234".to_owned(),
|
||||
user_id: UserId::try_from("@example:localhost").unwrap(),
|
||||
device_id: "DEVICEID".to_owned(),
|
||||
user_id: user_id!("@example:localhost"),
|
||||
device_id: "DEVICEID".into(),
|
||||
};
|
||||
let client = BaseClient::new().unwrap();
|
||||
client.restore_login(session).await.unwrap();
|
||||
client
|
||||
}
|
||||
|
||||
fn get_room_id() -> RoomId {
|
||||
RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap()
|
||||
// TODO: Move this to EventBuilder since it's a magic room ID used in EventBuilder's example
|
||||
// events.
|
||||
fn test_room_id() -> RoomId {
|
||||
room_id!("!SVkFJHzfwvuaIEawgC:localhost")
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn room_member_events() {
|
||||
let client = get_client().await;
|
||||
|
||||
let room_id = get_room_id();
|
||||
let room_id = test_room_id();
|
||||
|
||||
let mut response = EventBuilder::default()
|
||||
.add_room_event(EventsFile::Member, RoomEvent::RoomMember)
|
||||
.add_room_event(EventsFile::PowerLevels, RoomEvent::RoomPowerLevels)
|
||||
.add_room_event(EventsJson::Member)
|
||||
.add_room_event(EventsJson::PowerLevels)
|
||||
.build_sync_response();
|
||||
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
@@ -245,23 +204,72 @@ mod test {
|
||||
let room = room.read().await;
|
||||
|
||||
let member = room
|
||||
.members
|
||||
.get(&UserId::try_from("@example:localhost").unwrap())
|
||||
.joined_members
|
||||
.get(&user_id!("@example:localhost"))
|
||||
.unwrap();
|
||||
assert_eq!(member.membership, MembershipState::Join);
|
||||
assert_eq!(member.power_level, Int::new(100));
|
||||
assert_eq!(member.power_level, Some(int!(100)));
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn room_member_display_name_change() {
|
||||
let client = get_client().await;
|
||||
let room_id = test_room_id();
|
||||
|
||||
let mut builder = EventBuilder::default();
|
||||
let mut initial_response = builder
|
||||
.add_room_event(EventsJson::Member)
|
||||
.build_sync_response();
|
||||
let mut name_change_response = builder
|
||||
.add_room_event(EventsJson::MemberNameChange)
|
||||
.build_sync_response();
|
||||
|
||||
client
|
||||
.receive_sync_response(&mut initial_response)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let room = client.get_joined_room(&room_id).await.unwrap();
|
||||
|
||||
// Initially, the display name is "example".
|
||||
{
|
||||
let room = room.read().await;
|
||||
|
||||
let member = room
|
||||
.joined_members
|
||||
.get(&user_id!("@example:localhost"))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(member.display_name.as_ref().unwrap(), "example");
|
||||
}
|
||||
|
||||
client
|
||||
.receive_sync_response(&mut name_change_response)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Afterwards, the display name is "changed".
|
||||
{
|
||||
let room = room.read().await;
|
||||
|
||||
let member = room
|
||||
.joined_members
|
||||
.get(&user_id!("@example:localhost"))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(member.display_name.as_ref().unwrap(), "changed");
|
||||
}
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn member_presence_events() {
|
||||
let client = get_client().await;
|
||||
|
||||
let room_id = get_room_id();
|
||||
let room_id = test_room_id();
|
||||
|
||||
let mut response = EventBuilder::default()
|
||||
.add_room_event(EventsFile::Member, RoomEvent::RoomMember)
|
||||
.add_room_event(EventsFile::PowerLevels, RoomEvent::RoomPowerLevels)
|
||||
.add_presence_event(EventsFile::Presence)
|
||||
.add_room_event(EventsJson::Member)
|
||||
.add_room_event(EventsJson::PowerLevels)
|
||||
.add_presence_event(EventsJson::Presence)
|
||||
.build_sync_response();
|
||||
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
@@ -270,12 +278,11 @@ mod test {
|
||||
let room = room.read().await;
|
||||
|
||||
let member = room
|
||||
.members
|
||||
.get(&UserId::try_from("@example:localhost").unwrap())
|
||||
.joined_members
|
||||
.get(&user_id!("@example:localhost"))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(member.membership, MembershipState::Join);
|
||||
assert_eq!(member.power_level, Int::new(100));
|
||||
assert_eq!(member.power_level, Some(int!(100)));
|
||||
|
||||
assert!(member.avatar_url.is_none());
|
||||
assert_eq!(member.last_active_ago, None);
|
||||
|
||||
@@ -15,15 +15,18 @@
|
||||
|
||||
//! User sessions.
|
||||
|
||||
use crate::identifiers::UserId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use matrix_sdk_common::identifiers::{DeviceId, UserId};
|
||||
|
||||
/// A user session, containing an access token and information about the
|
||||
/// associated user account.
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Session {
|
||||
/// The access token used for this session.
|
||||
pub access_token: String,
|
||||
/// The user the access token was issued for.
|
||||
pub user_id: UserId,
|
||||
/// The ID of the client device
|
||||
pub device_id: String,
|
||||
pub device_id: Box<DeviceId>,
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt, fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use matrix_sdk_common::identifiers::RoomId;
|
||||
use matrix_sdk_common::locks::RwLock;
|
||||
use tokio::fs as async_fs;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use matrix_sdk_common::{async_trait, identifiers::RoomId, locks::RwLock};
|
||||
use tokio::{fs as async_fs, io::AsyncWriteExt};
|
||||
|
||||
use super::{AllRooms, ClientState, StateStore};
|
||||
use crate::{Error, Result, Room, RoomState, Session};
|
||||
@@ -18,7 +17,7 @@ use crate::{Error, Result, Room, RoomState, Session};
|
||||
/// A default `StateStore` implementation that serializes state as json
|
||||
/// and saves it to disk.
|
||||
///
|
||||
/// When logged in the `JsonStore` appends the user_id to it's folder path,
|
||||
/// When logged in the `JsonStore` appends the user_id to its folder path,
|
||||
/// so all files are saved in `my_client/user_id_localpart/*`.
|
||||
pub struct JsonStore {
|
||||
path: Arc<RwLock<PathBuf>>,
|
||||
@@ -39,6 +38,36 @@ impl JsonStore {
|
||||
user_path_set: AtomicBool::new(false),
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a path for a file where the Room state to be stored in.
|
||||
async fn build_room_path(&self, room_state: &str, room_id: &RoomId) -> PathBuf {
|
||||
let mut path = self.path.read().await.clone();
|
||||
|
||||
path.push("rooms");
|
||||
path.push(room_state);
|
||||
path.push(JsonStore::sanitize_room_id(room_id));
|
||||
path.set_extension("json");
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Build a path for the file where the Client state to be stored in.
|
||||
async fn build_client_path(&self) -> PathBuf {
|
||||
let mut path = self.path.read().await.clone();
|
||||
path.push("client");
|
||||
path.set_extension("json");
|
||||
|
||||
path
|
||||
}
|
||||
|
||||
/// Replace common characters that can't be used in a file name with an
|
||||
/// underscore.
|
||||
fn sanitize_room_id(room_id: &RoomId) -> String {
|
||||
room_id.as_str().replace(
|
||||
&['.', ':', '<', '>', '"', '/', '\\', '|', '?', '*'][..],
|
||||
"_",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for JsonStore {
|
||||
@@ -49,7 +78,7 @@ impl fmt::Debug for JsonStore {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
#[async_trait]
|
||||
impl StateStore for JsonStore {
|
||||
async fn load_client_state(&self, sess: &Session) -> Result<Option<ClientState>> {
|
||||
if !self.user_path_set.load(Ordering::SeqCst) {
|
||||
@@ -57,8 +86,7 @@ impl StateStore for JsonStore {
|
||||
self.path.write().await.push(sess.user_id.localpart())
|
||||
}
|
||||
|
||||
let mut path = self.path.read().await.clone();
|
||||
path.push("client.json");
|
||||
let path = self.build_client_path().await;
|
||||
|
||||
let json = async_fs::read_to_string(path)
|
||||
.await
|
||||
@@ -93,7 +121,6 @@ impl StateStore for JsonStore {
|
||||
}
|
||||
|
||||
let json = async_fs::read_to_string(&file).await?;
|
||||
|
||||
let room = serde_json::from_str::<Room>(&json).map_err(Error::from)?;
|
||||
let room_id = room.room_id.clone();
|
||||
|
||||
@@ -115,8 +142,7 @@ impl StateStore for JsonStore {
|
||||
}
|
||||
|
||||
async fn store_client_state(&self, state: ClientState) -> Result<()> {
|
||||
let mut path = self.path.read().await.clone();
|
||||
path.push("client.json");
|
||||
let path = self.build_client_path().await;
|
||||
|
||||
if !path.exists() {
|
||||
let mut dir = path.clone();
|
||||
@@ -147,9 +173,7 @@ impl StateStore for JsonStore {
|
||||
self.path.write().await.push(room.own_user_id.localpart())
|
||||
}
|
||||
|
||||
let mut path = self.path.read().await.clone();
|
||||
path.push("rooms");
|
||||
path.push(&format!("{}/{}.json", room_state, room.room_id));
|
||||
let path = self.build_room_path(room_state, &room.room_id).await;
|
||||
|
||||
if !path.exists() {
|
||||
let mut dir = path.clone();
|
||||
@@ -179,15 +203,13 @@ impl StateStore for JsonStore {
|
||||
return Err(Error::StateStore("path for JsonStore not set".into()));
|
||||
}
|
||||
|
||||
let mut to_del = self.path.read().await.clone();
|
||||
to_del.push("rooms");
|
||||
to_del.push(&format!("{}/{}.json", room_state, room_id));
|
||||
let path = self.build_room_path(room_state, room_id).await;
|
||||
|
||||
if !to_del.exists() {
|
||||
return Err(Error::StateStore(format!("file {:?} not found", to_del)));
|
||||
if !path.exists() {
|
||||
return Err(Error::StateStore(format!("file {:?} not found", path)));
|
||||
}
|
||||
|
||||
tokio::fs::remove_file(to_del).await.map_err(Error::from)
|
||||
tokio::fs::remove_file(path).await.map_err(Error::from)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,43 +217,33 @@ impl StateStore for JsonStore {
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use http::Response;
|
||||
use std::convert::TryFrom;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tempfile::tempdir;
|
||||
|
||||
use crate::api::r0::sync::sync_events::Response as SyncResponse;
|
||||
use crate::identifiers::{RoomId, UserId};
|
||||
use crate::{BaseClient, BaseClientConfig, Session};
|
||||
|
||||
fn sync_response(file: &str) -> SyncResponse {
|
||||
let mut file = File::open(file).unwrap();
|
||||
let mut data = vec![];
|
||||
file.read_to_end(&mut data).unwrap();
|
||||
let response = Response::builder().body(data).unwrap();
|
||||
SyncResponse::try_from(response).unwrap()
|
||||
}
|
||||
use crate::{
|
||||
identifiers::{room_id, user_id},
|
||||
push::Ruleset,
|
||||
Session,
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_store_client_state() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path: &Path = dir.path();
|
||||
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
let user = user_id!("@example:example.com");
|
||||
|
||||
let sess = Session {
|
||||
access_token: "32nj9zu034btz90".to_string(),
|
||||
user_id: user.clone(),
|
||||
device_id: "Tester".to_string(),
|
||||
device_id: "Tester".into(),
|
||||
};
|
||||
|
||||
let state = ClientState {
|
||||
sync_token: Some("hello".into()),
|
||||
ignored_users: vec![user],
|
||||
push_ruleset: None,
|
||||
push_ruleset: None::<Ruleset>,
|
||||
};
|
||||
|
||||
let mut path_with_user = PathBuf::from(path);
|
||||
@@ -252,8 +264,8 @@ mod test {
|
||||
let path: &Path = dir.path();
|
||||
let store = JsonStore::open(path).unwrap();
|
||||
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
let id = room_id!("!roomid:example.com");
|
||||
let user = user_id!("@example:example.com");
|
||||
|
||||
let room = Room::new(&id, &user);
|
||||
store
|
||||
@@ -270,8 +282,8 @@ mod test {
|
||||
let path: &Path = dir.path();
|
||||
let store = JsonStore::open(path).unwrap();
|
||||
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
let id = room_id!("!roomid:example.com");
|
||||
let user = user_id!("@example:example.com");
|
||||
|
||||
let room = Room::new(&id, &user);
|
||||
store
|
||||
@@ -288,8 +300,8 @@ mod test {
|
||||
let path: &Path = dir.path();
|
||||
let store = JsonStore::open(path).unwrap();
|
||||
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
let id = room_id!("!roomid:example.com");
|
||||
let user = user_id!("@example:example.com");
|
||||
|
||||
let room = Room::new(&id, &user);
|
||||
store
|
||||
@@ -306,8 +318,8 @@ mod test {
|
||||
let path: &Path = dir.path();
|
||||
let store = JsonStore::open(path).unwrap();
|
||||
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
let id = room_id!("!roomid:example.com");
|
||||
let user = user_id!("@example:example.com");
|
||||
|
||||
let room = Room::new(&id, &user);
|
||||
store
|
||||
@@ -330,8 +342,8 @@ mod test {
|
||||
let path: &Path = dir.path();
|
||||
let store = JsonStore::open(path).unwrap();
|
||||
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
let id = room_id!("!roomid:example.com");
|
||||
let user = user_id!("@example:example.com");
|
||||
|
||||
let room = Room::new(&id, &user);
|
||||
store
|
||||
@@ -346,44 +358,4 @@ mod test {
|
||||
// test that we have removed the correct room
|
||||
assert!(invited.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_client_sync_store() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path: &Path = dir.path();
|
||||
|
||||
let session = Session {
|
||||
access_token: "1234".to_owned(),
|
||||
user_id: UserId::try_from("@cheeky_monkey:matrix.org").unwrap(),
|
||||
device_id: "DEVICEID".to_owned(),
|
||||
};
|
||||
|
||||
// a sync response to populate our JSON store
|
||||
let store = Box::new(JsonStore::open(path).unwrap());
|
||||
let client =
|
||||
BaseClient::new_with_config(BaseClientConfig::new().state_store(store)).unwrap();
|
||||
client.restore_login(session.clone()).await.unwrap();
|
||||
|
||||
let mut response = sync_response("../test_data/sync.json");
|
||||
|
||||
// gather state to save to the db, the first time through loading will be skipped
|
||||
client.receive_sync_response(&mut response).await.unwrap();
|
||||
|
||||
// now syncing the client will update from the state store
|
||||
let store = Box::new(JsonStore::open(path).unwrap());
|
||||
let client =
|
||||
BaseClient::new_with_config(BaseClientConfig::new().state_store(store)).unwrap();
|
||||
client.restore_login(session.clone()).await.unwrap();
|
||||
|
||||
// assert the synced client and the logged in client are equal
|
||||
assert_eq!(*client.session().read().await, Some(session));
|
||||
assert_eq!(
|
||||
client.sync_token().await,
|
||||
Some("s526_47314_0_7_1_1_1_11444_1".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
*client.ignored_users.read().await,
|
||||
vec![UserId::try_from("@someone:example.org").unwrap()]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use matrix_sdk_common::{
|
||||
async_trait,
|
||||
identifiers::{RoomId, UserId},
|
||||
push::Ruleset,
|
||||
AsyncTraitDeps,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -22,10 +28,10 @@ mod json_store;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use json_store::JsonStore;
|
||||
|
||||
use crate::client::{BaseClient, Token};
|
||||
use crate::events::push_rules::Ruleset;
|
||||
use crate::identifiers::{RoomId, UserId};
|
||||
use crate::{Result, Room, RoomState, Session};
|
||||
use crate::{
|
||||
client::{BaseClient, Token},
|
||||
Result, Room, RoomState, Session,
|
||||
};
|
||||
|
||||
/// `ClientState` holds all the information to restore a `BaseClient`
|
||||
/// except the `access_token` as the default store is not secure.
|
||||
@@ -84,8 +90,9 @@ pub struct AllRooms {
|
||||
}
|
||||
|
||||
/// Abstraction around the data store to avoid unnecessary request on client initialization.
|
||||
#[async_trait::async_trait]
|
||||
pub trait StateStore: Send + Sync {
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait StateStore: AsyncTraitDeps {
|
||||
/// Loads the state of `BaseClient` through `ClientState` type.
|
||||
///
|
||||
/// An `Option::None` should be returned only if the `StateStore` tries to
|
||||
@@ -114,14 +121,13 @@ mod test {
|
||||
use super::*;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use crate::identifiers::RoomId;
|
||||
use crate::identifiers::{room_id, user_id};
|
||||
|
||||
#[test]
|
||||
fn serialize() {
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
let id = room_id!("!roomid:example.com");
|
||||
let user = user_id!("@example:example.com");
|
||||
|
||||
let room = Room::new(&id, &user);
|
||||
|
||||
@@ -140,64 +146,68 @@ mod test {
|
||||
|
||||
#[cfg(not(feature = "messages"))]
|
||||
assert_eq!(
|
||||
r#"{
|
||||
"!roomid:example.com": {
|
||||
"room_id": "!roomid:example.com",
|
||||
"room_name": {
|
||||
"name": null,
|
||||
"canonical_alias": null,
|
||||
"aliases": [],
|
||||
"heroes": [],
|
||||
"joined_member_count": null,
|
||||
"invited_member_count": null
|
||||
},
|
||||
"own_user_id": "@example:example.com",
|
||||
"creator": null,
|
||||
"members": {},
|
||||
"typing_users": [],
|
||||
"power_levels": null,
|
||||
"encrypted": null,
|
||||
"unread_highlight": null,
|
||||
"unread_notifications": null,
|
||||
"tombstone": null
|
||||
}
|
||||
}"#,
|
||||
serde_json::to_string_pretty(&joined_rooms).unwrap()
|
||||
serde_json::json!({
|
||||
"!roomid:example.com": {
|
||||
"room_id": "!roomid:example.com",
|
||||
"room_name": {
|
||||
"name": null,
|
||||
"canonical_alias": null,
|
||||
"aliases": [],
|
||||
"heroes": [],
|
||||
"joined_member_count": null,
|
||||
"invited_member_count": null
|
||||
},
|
||||
"own_user_id": "@example:example.com",
|
||||
"creator": null,
|
||||
"direct_target": null,
|
||||
"joined_members": {},
|
||||
"invited_members": {},
|
||||
"typing_users": [],
|
||||
"power_levels": null,
|
||||
"encrypted": null,
|
||||
"unread_highlight": null,
|
||||
"unread_notifications": null,
|
||||
"tombstone": null
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&joined_rooms).unwrap()
|
||||
);
|
||||
|
||||
#[cfg(feature = "messages")]
|
||||
assert_eq!(
|
||||
r#"{
|
||||
"!roomid:example.com": {
|
||||
"room_id": "!roomid:example.com",
|
||||
"room_name": {
|
||||
"name": null,
|
||||
"canonical_alias": null,
|
||||
"aliases": [],
|
||||
"heroes": [],
|
||||
"joined_member_count": null,
|
||||
"invited_member_count": null
|
||||
},
|
||||
"own_user_id": "@example:example.com",
|
||||
"creator": null,
|
||||
"members": {},
|
||||
"messages": [],
|
||||
"typing_users": [],
|
||||
"power_levels": null,
|
||||
"encrypted": null,
|
||||
"unread_highlight": null,
|
||||
"unread_notifications": null,
|
||||
"tombstone": null
|
||||
}
|
||||
}"#,
|
||||
serde_json::to_string_pretty(&joined_rooms).unwrap()
|
||||
serde_json::json!({
|
||||
"!roomid:example.com": {
|
||||
"room_id": "!roomid:example.com",
|
||||
"room_name": {
|
||||
"name": null,
|
||||
"canonical_alias": null,
|
||||
"aliases": [],
|
||||
"heroes": [],
|
||||
"joined_member_count": null,
|
||||
"invited_member_count": null
|
||||
},
|
||||
"own_user_id": "@example:example.com",
|
||||
"creator": null,
|
||||
"direct_target": null,
|
||||
"joined_members": {},
|
||||
"invited_members": {},
|
||||
"messages": [],
|
||||
"typing_users": [],
|
||||
"power_levels": null,
|
||||
"encrypted": null,
|
||||
"unread_highlight": null,
|
||||
"unread_notifications": null,
|
||||
"tombstone": null
|
||||
}
|
||||
}),
|
||||
serde_json::to_value(&joined_rooms).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize() {
|
||||
let id = RoomId::try_from("!roomid:example.com").unwrap();
|
||||
let user = UserId::try_from("@example:example.com").unwrap();
|
||||
let id = room_id!("!roomid:example.com");
|
||||
let user = user_id!("@example:example.com");
|
||||
|
||||
let room = Room::new(&id, &user);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk"]
|
||||
description = "Collection of common types used in the matrix-sdk"
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
description = "Collection of common types and imports used in the matrix-sdk"
|
||||
edition = "2018"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
keywords = ["matrix", "chat", "messaging", "ruma", "nio"]
|
||||
@@ -8,24 +8,27 @@ license = "Apache-2.0"
|
||||
name = "matrix-sdk-common"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
[features]
|
||||
unstable-synapse-quirks = ["ruma/unstable-synapse-quirks"]
|
||||
|
||||
[dependencies]
|
||||
js_int = "0.1.5"
|
||||
ruma-api = "0.16.1"
|
||||
ruma-client-api = "0.9.0"
|
||||
ruma-events = "0.21.2"
|
||||
ruma-identifiers = "0.16.1"
|
||||
instant = { version = "0.1.4", features = ["wasm-bindgen", "now"] }
|
||||
instant = { version = "0.1.9", features = ["wasm-bindgen", "now"] }
|
||||
async-trait = "0.1.42"
|
||||
|
||||
[dependencies.ruma]
|
||||
version = "0.0.2"
|
||||
features = ["client-api", "unstable-pre-spec"]
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
uuid = { version = "0.8.1", features = ["v4"] }
|
||||
uuid = { version = "0.8.1", default-features = false, features = ["v4", "serde"] }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.tokio]
|
||||
version = "0.2.21"
|
||||
version = "0.2.24"
|
||||
default-features = false
|
||||
features = ["sync", "time", "fs"]
|
||||
features = ["sync"]
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
futures-locks = { version = "0.5.0", default-features = false }
|
||||
uuid = { version = "0.8.1", features = ["v4", "wasm-bindgen"] }
|
||||
futures-locks = { version = "0.6.0", default-features = false }
|
||||
uuid = { version = "0.8.1", default-features = false, features = ["v4", "wasm-bindgen"] }
|
||||
|
||||
@@ -1,13 +1,32 @@
|
||||
pub use async_trait::async_trait;
|
||||
pub use instant;
|
||||
pub use js_int;
|
||||
pub use ruma_api::{
|
||||
error::{FromHttpResponseError, IntoHttpError, ServerError},
|
||||
Endpoint,
|
||||
pub use ruma::{
|
||||
api::{
|
||||
client as api,
|
||||
error::{FromHttpRequestError, FromHttpResponseError, IntoHttpError, ServerError},
|
||||
AuthScheme, EndpointError, OutgoingRequest,
|
||||
},
|
||||
assign, directory, encryption, events, identifiers, int, presence, push,
|
||||
serde::{CanonicalJsonValue, Raw},
|
||||
thirdparty, uint, Int, Outgoing, UInt,
|
||||
};
|
||||
pub use ruma_client_api as api;
|
||||
pub use ruma_events as events;
|
||||
pub use ruma_identifiers as identifiers;
|
||||
|
||||
pub use uuid;
|
||||
|
||||
pub mod locks;
|
||||
|
||||
/// Super trait that is used for our store traits, this trait will differ if
|
||||
/// it's used on WASM. WASM targets will not require `Send` and `Sync` to have
|
||||
/// implemented, while other targets will.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub trait AsyncTraitDeps: std::fmt::Debug + Send + Sync {}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
impl<T: std::fmt::Debug + Send + Sync> AsyncTraitDeps for T {}
|
||||
|
||||
/// Super trait that is used for our store traits, this trait will differ if
|
||||
/// it's used on WASM. WASM targets will not require `Send` and `Sync` to have
|
||||
/// implemented, while other targets will.
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub trait AsyncTraitDeps: std::fmt::Debug + Send + Sync {}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
impl<T: std::fmt::Debug + Send + Sync> AsyncTraitDeps for T {}
|
||||
|
||||
@@ -3,11 +3,7 @@
|
||||
// https://www.reddit.com/r/rust/comments/f4zldz/i_audited_3_different_implementation_of_async/
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use futures_locks::Mutex;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use futures_locks::RwLock;
|
||||
pub use futures_locks::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use tokio::sync::Mutex;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use tokio::sync::RwLock;
|
||||
pub use tokio::sync::{Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk"]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
description = "Matrix encryption library"
|
||||
edition = "2018"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
@@ -8,44 +8,52 @@ license = "Apache-2.0"
|
||||
name = "matrix-sdk-crypto"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["docs"]
|
||||
rustdoc-args = ["--cfg", "feature=\"docs\""]
|
||||
|
||||
[features]
|
||||
default = []
|
||||
sqlite-cryptostore = ["sqlx"]
|
||||
sqlite_cryptostore = ["sqlx"]
|
||||
docs = ["sqlite_cryptostore"]
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.31"
|
||||
matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" }
|
||||
|
||||
matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" }
|
||||
|
||||
olm-rs = { version = "0.5.0", features = ["serde"] }
|
||||
serde = { version = "1.0.110", features = ["derive"] }
|
||||
serde_json = "1.0.53"
|
||||
cjson = "0.1.0"
|
||||
zeroize = { version = "1.1.0", features = ["zeroize_derive"] }
|
||||
url = "2.1.1"
|
||||
olm-rs = { version = "1.0.0", features = ["serde"] }
|
||||
getrandom = "0.2.1"
|
||||
serde = { version = "1.0.118", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.61"
|
||||
zeroize = { version = "1.2.0", features = ["zeroize_derive"] }
|
||||
url = "2.2.0"
|
||||
|
||||
# Misc dependencies
|
||||
thiserror = "1.0.19"
|
||||
tracing = "0.1.14"
|
||||
atomic = "0.4.5"
|
||||
dashmap = "3.11.2"
|
||||
|
||||
[dependencies.tracing-futures]
|
||||
version = "0.2.4"
|
||||
default-features = false
|
||||
features = ["std", "std-future"]
|
||||
thiserror = "1.0.23"
|
||||
tracing = "0.1.22"
|
||||
atomic = "0.5.0"
|
||||
dashmap = "4.0.1"
|
||||
sha2 = "0.9.2"
|
||||
aes-gcm = "0.8.0"
|
||||
aes-ctr = "0.6.0"
|
||||
pbkdf2 = { version = "0.6.0", default-features = false }
|
||||
hmac = "0.10.1"
|
||||
base64 = "0.13.0"
|
||||
byteorder = "1.3.4"
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies.sqlx]
|
||||
version = "0.3.5"
|
||||
version = "0.4.2"
|
||||
optional = true
|
||||
default-features = false
|
||||
features = ["runtime-tokio", "sqlite"]
|
||||
features = ["runtime-tokio-native-tls", "sqlite", "macros"]
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "0.2.21", features = ["rt-threaded", "macros"] }
|
||||
ruma-identifiers = { version = "0.16.1", features = ["rand"] }
|
||||
serde_json = "1.0.53"
|
||||
tokio = { version = "0.2.24", default-features = false, features = ["rt-threaded", "macros"] }
|
||||
futures = "0.3.8"
|
||||
proptest = "0.10.1"
|
||||
serde_json = "1.0.61"
|
||||
tempfile = "3.1.0"
|
||||
http = "0.2.1"
|
||||
http = "0.2.2"
|
||||
matrix-sdk-test = { version = "0.2.0", path = "../matrix_sdk_test" }
|
||||
indoc = "1.0.3"
|
||||
|
||||
@@ -1,319 +0,0 @@
|
||||
// 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.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
#[cfg(test)]
|
||||
use std::convert::TryFrom;
|
||||
use std::mem;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use atomic::Atomic;
|
||||
|
||||
#[cfg(test)]
|
||||
use super::OlmMachine;
|
||||
use matrix_sdk_common::api::r0::keys::{DeviceKeys, KeyAlgorithm};
|
||||
use matrix_sdk_common::events::Algorithm;
|
||||
use matrix_sdk_common::identifiers::{DeviceId, UserId};
|
||||
|
||||
/// A device represents a E2EE capable client of an user.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Device {
|
||||
user_id: Arc<UserId>,
|
||||
device_id: Arc<DeviceId>,
|
||||
algorithms: Arc<Vec<Algorithm>>,
|
||||
keys: Arc<BTreeMap<KeyAlgorithm, String>>,
|
||||
display_name: Arc<Option<String>>,
|
||||
deleted: Arc<AtomicBool>,
|
||||
trust_state: Arc<Atomic<TrustState>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
/// The trust state of a device.
|
||||
pub enum TrustState {
|
||||
/// The device has been verified and is trusted.
|
||||
Verified = 0,
|
||||
/// The device been blacklisted from communicating.
|
||||
BlackListed = 1,
|
||||
/// The trust state of the device is being ignored.
|
||||
Ignored = 2,
|
||||
/// The trust state is unset.
|
||||
Unset = 3,
|
||||
}
|
||||
|
||||
impl From<i64> for TrustState {
|
||||
fn from(state: i64) -> Self {
|
||||
match state {
|
||||
0 => TrustState::Verified,
|
||||
1 => TrustState::BlackListed,
|
||||
2 => TrustState::Ignored,
|
||||
3 => TrustState::Unset,
|
||||
_ => TrustState::Unset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Device {
|
||||
/// Create a new Device.
|
||||
pub fn new(
|
||||
user_id: UserId,
|
||||
device_id: DeviceId,
|
||||
display_name: Option<String>,
|
||||
trust_state: TrustState,
|
||||
algorithms: Vec<Algorithm>,
|
||||
keys: BTreeMap<KeyAlgorithm, String>,
|
||||
) -> Self {
|
||||
Device {
|
||||
user_id: Arc::new(user_id),
|
||||
device_id: Arc::new(device_id),
|
||||
display_name: Arc::new(display_name),
|
||||
trust_state: Arc::new(Atomic::new(trust_state)),
|
||||
algorithms: Arc::new(algorithms),
|
||||
keys: Arc::new(keys),
|
||||
deleted: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The user id of the device owner.
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
&self.user_id
|
||||
}
|
||||
|
||||
/// The unique ID of the device.
|
||||
pub fn device_id(&self) -> &DeviceId {
|
||||
&self.device_id
|
||||
}
|
||||
|
||||
/// Get the human readable name of the device.
|
||||
pub fn display_name(&self) -> &Option<String> {
|
||||
&self.display_name
|
||||
}
|
||||
|
||||
/// Get the key of the given key algorithm belonging to this device.
|
||||
pub fn get_key(&self, algorithm: KeyAlgorithm) -> Option<&String> {
|
||||
self.keys.get(&algorithm)
|
||||
}
|
||||
|
||||
/// Get a map containing all the device keys.
|
||||
pub fn keys(&self) -> &BTreeMap<KeyAlgorithm, String> {
|
||||
&self.keys
|
||||
}
|
||||
|
||||
/// Get the trust state of the device.
|
||||
pub fn trust_state(&self) -> TrustState {
|
||||
self.trust_state.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Get the list of algorithms this device supports.
|
||||
pub fn algorithms(&self) -> &[Algorithm] {
|
||||
&self.algorithms
|
||||
}
|
||||
|
||||
/// Is the device deleted.
|
||||
pub fn deleted(&self) -> bool {
|
||||
self.deleted.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Update a device with a new device keys struct.
|
||||
pub(crate) fn update_device(&mut self, device_keys: &DeviceKeys) {
|
||||
let mut keys = BTreeMap::new();
|
||||
|
||||
for (key_id, key) in device_keys.keys.iter() {
|
||||
let key_id = key_id.0;
|
||||
let _ = keys.insert(key_id, key.clone());
|
||||
}
|
||||
|
||||
let display_name = Arc::new(
|
||||
device_keys
|
||||
.unsigned
|
||||
.as_ref()
|
||||
.map(|d| d.device_display_name.clone())
|
||||
.flatten(),
|
||||
);
|
||||
|
||||
let _ = mem::replace(
|
||||
&mut self.algorithms,
|
||||
Arc::new(device_keys.algorithms.clone()),
|
||||
);
|
||||
let _ = mem::replace(&mut self.keys, Arc::new(keys));
|
||||
let _ = mem::replace(&mut self.display_name, display_name);
|
||||
}
|
||||
|
||||
/// Mark the device as deleted.
|
||||
pub(crate) fn mark_as_deleted(&self) {
|
||||
self.deleted.store(true, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl From<&OlmMachine> for Device {
|
||||
fn from(machine: &OlmMachine) -> Self {
|
||||
Device {
|
||||
user_id: Arc::new(machine.user_id().clone()),
|
||||
device_id: Arc::new(machine.device_id().clone()),
|
||||
algorithms: Arc::new(vec![
|
||||
Algorithm::MegolmV1AesSha2,
|
||||
Algorithm::OlmV1Curve25519AesSha2,
|
||||
]),
|
||||
keys: Arc::new(
|
||||
machine
|
||||
.identity_keys()
|
||||
.iter()
|
||||
.map(|(key, value)| {
|
||||
(
|
||||
KeyAlgorithm::try_from(key.as_ref()).unwrap(),
|
||||
value.to_owned(),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
display_name: Arc::new(None),
|
||||
deleted: Arc::new(AtomicBool::new(false)),
|
||||
trust_state: Arc::new(Atomic::new(TrustState::Unset)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&DeviceKeys> for Device {
|
||||
fn from(device_keys: &DeviceKeys) -> Self {
|
||||
let mut keys = BTreeMap::new();
|
||||
|
||||
for (key_id, key) in device_keys.keys.iter() {
|
||||
let key_id = key_id.0;
|
||||
let _ = keys.insert(key_id, key.clone());
|
||||
}
|
||||
|
||||
Device {
|
||||
user_id: Arc::new(device_keys.user_id.clone()),
|
||||
device_id: Arc::new(device_keys.device_id.clone()),
|
||||
algorithms: Arc::new(device_keys.algorithms.clone()),
|
||||
keys: Arc::new(keys),
|
||||
display_name: Arc::new(
|
||||
device_keys
|
||||
.unsigned
|
||||
.as_ref()
|
||||
.map(|d| d.device_display_name.clone())
|
||||
.flatten(),
|
||||
),
|
||||
deleted: Arc::new(AtomicBool::new(false)),
|
||||
trust_state: Arc::new(Atomic::new(TrustState::Unset)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Device {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.user_id() == other.user_id() && self.device_id() == other.device_id()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use serde_json::json;
|
||||
use std::convert::{From, TryFrom};
|
||||
|
||||
use crate::device::{Device, TrustState};
|
||||
use matrix_sdk_common::api::r0::keys::{DeviceKeys, KeyAlgorithm};
|
||||
use matrix_sdk_common::identifiers::UserId;
|
||||
|
||||
fn device_keys() -> DeviceKeys {
|
||||
let user_id = UserId::try_from("@alice:example.org").unwrap();
|
||||
let device_id = "DEVICEID";
|
||||
|
||||
let device_keys = json!({
|
||||
"algorithms": vec![
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2"
|
||||
],
|
||||
"device_id": device_id,
|
||||
"user_id": user_id.to_string(),
|
||||
"keys": {
|
||||
"curve25519:DEVICEID": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4",
|
||||
"ed25519:DEVICEID": "nE6W2fCblxDcOFmeEtCHNl8/l8bXcu7GKyAswA4r3mM"
|
||||
},
|
||||
"signatures": {
|
||||
user_id.to_string(): {
|
||||
"ed25519:DEVICEID": "m53Wkbh2HXkc3vFApZvCrfXcX3AI51GsDHustMhKwlv3TuOJMj4wistcOTM8q2+e/Ro7rWFUb9ZfnNbwptSUBA"
|
||||
}
|
||||
},
|
||||
"unsigned": {
|
||||
"device_display_name": "Alice's mobile phone"
|
||||
}
|
||||
});
|
||||
|
||||
serde_json::from_value(device_keys).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn get_device() -> Device {
|
||||
let device_keys = device_keys();
|
||||
Device::from(&device_keys)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_a_device() {
|
||||
let user_id = UserId::try_from("@alice:example.org").unwrap();
|
||||
let device_id = "DEVICEID";
|
||||
|
||||
let device = get_device();
|
||||
|
||||
assert_eq!(&user_id, device.user_id());
|
||||
assert_eq!(device_id, device.device_id());
|
||||
assert_eq!(device.algorithms.len(), 2);
|
||||
assert_eq!(TrustState::Unset, device.trust_state());
|
||||
assert_eq!(
|
||||
"Alice's mobile phone",
|
||||
device.display_name().as_ref().unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
device.get_key(KeyAlgorithm::Curve25519).unwrap(),
|
||||
"wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4"
|
||||
);
|
||||
assert_eq!(
|
||||
device.get_key(KeyAlgorithm::Ed25519).unwrap(),
|
||||
"nE6W2fCblxDcOFmeEtCHNl8/l8bXcu7GKyAswA4r3mM"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_a_device() {
|
||||
let mut device = get_device();
|
||||
|
||||
assert_eq!(
|
||||
"Alice's mobile phone",
|
||||
device.display_name().as_ref().unwrap()
|
||||
);
|
||||
|
||||
let mut device_keys = device_keys();
|
||||
device_keys.unsigned.as_mut().unwrap().device_display_name =
|
||||
Some("Alice's work computer".to_owned());
|
||||
device.update_device(&device_keys);
|
||||
|
||||
assert_eq!(
|
||||
"Alice's work computer",
|
||||
device.display_name().as_ref().unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_a_device() {
|
||||
let device = get_device();
|
||||
assert!(!device.deleted());
|
||||
|
||||
let device_clone = device.clone();
|
||||
|
||||
device.mark_as_deleted();
|
||||
assert!(device.deleted());
|
||||
assert!(device_clone.deleted());
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use cjson::Error as CjsonError;
|
||||
use matrix_sdk_common::identifiers::{DeviceId, Error as IdentifierError, UserId};
|
||||
use olm_rs::errors::{OlmGroupSessionError, OlmSessionError};
|
||||
use serde_json::Error as SerdeError;
|
||||
use thiserror::Error;
|
||||
@@ -47,8 +47,23 @@ pub enum OlmError {
|
||||
Store(#[from] CryptoStoreError),
|
||||
|
||||
/// The session with a device has become corrupted.
|
||||
#[error("decryption failed likely because a Olm session was wedged")]
|
||||
SessionWedged,
|
||||
#[error(
|
||||
"decryption failed likely because an Olm session from {0} with sender key {1} was wedged"
|
||||
)]
|
||||
SessionWedged(UserId, String),
|
||||
|
||||
/// An Olm message got replayed while the Olm ratchet has already moved
|
||||
/// forward.
|
||||
#[error("decryption failed because an Olm message from {0} with sender key {1} was replayed")]
|
||||
ReplayedMessage(UserId, String),
|
||||
|
||||
/// Encryption failed because the device does not have a valid Olm session
|
||||
/// with us.
|
||||
#[error(
|
||||
"encryption failed because the device does not \
|
||||
have a valid Olm session with us"
|
||||
)]
|
||||
MissingSession,
|
||||
}
|
||||
|
||||
/// Error representing a failure during a group encryption operation.
|
||||
@@ -93,6 +108,9 @@ pub enum EventError {
|
||||
#[error("the Encrypted message is missing the signing key of the sender")]
|
||||
MissingSigningKey,
|
||||
|
||||
#[error("the Encrypted message is missing the sender key")]
|
||||
MissingSenderKey,
|
||||
|
||||
#[error("the Encrypted message is missing the field {0}")]
|
||||
MissingField(String),
|
||||
|
||||
@@ -104,22 +122,61 @@ pub enum EventError {
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub(crate) enum SignatureError {
|
||||
pub enum SessionUnpicklingError {
|
||||
/// The underlying Olm session operation returned an error.
|
||||
#[error("can't finish Olm Session operation {0}")]
|
||||
OlmSession(#[from] OlmSessionError),
|
||||
/// The Session timestamp was invalid.
|
||||
#[error("can't load session timestamps")]
|
||||
SessionTimestampError,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum SignatureError {
|
||||
#[error("the signature used a unsupported algorithm")]
|
||||
UnsupportedAlgorithm,
|
||||
|
||||
#[error("the key id of the signing key is invalid")]
|
||||
InvalidKeyId(#[from] IdentifierError),
|
||||
|
||||
#[error("the signing key is missing from the object that signed the message")]
|
||||
MissingSigningKey,
|
||||
|
||||
#[error("the user id of the signing differs from the subkey user id")]
|
||||
UserIdMissmatch,
|
||||
|
||||
#[error("the provided JSON value isn't an object")]
|
||||
NotAnObject,
|
||||
|
||||
#[error("the provided JSON object doesn't contain a signatures field")]
|
||||
NoSignatureFound,
|
||||
|
||||
#[error("the provided JSON object can't be converted to a canonical representation")]
|
||||
CanonicalJsonError(CjsonError),
|
||||
|
||||
#[error("the signature didn't match the provided key")]
|
||||
VerificationError,
|
||||
|
||||
#[error(transparent)]
|
||||
JsonError(#[from] SerdeError),
|
||||
}
|
||||
|
||||
impl From<CjsonError> for SignatureError {
|
||||
fn from(error: CjsonError) -> Self {
|
||||
Self::CanonicalJsonError(error)
|
||||
}
|
||||
#[derive(Error, Debug)]
|
||||
pub(crate) enum SessionCreationError {
|
||||
#[error(
|
||||
"Failed to create a new Olm session for {0} {1}, the requested \
|
||||
one-time key isn't a signed curve key"
|
||||
)]
|
||||
OneTimeKeyNotSigned(UserId, Box<DeviceId>),
|
||||
#[error(
|
||||
"Tried to create a new Olm session for {0} {1}, but the signed \
|
||||
one-time key is missing"
|
||||
)]
|
||||
OneTimeKeyMissing(UserId, Box<DeviceId>),
|
||||
#[error("Failed to verify the one-time key signatures for {0} {1}: {2:?}")]
|
||||
InvalidSignature(UserId, Box<DeviceId>, SignatureError),
|
||||
#[error(
|
||||
"Tried to create an Olm session for {0} {1}, but the device is missing \
|
||||
a curve25519 key"
|
||||
)]
|
||||
DeviceMissingCurveKey(UserId, Box<DeviceId>),
|
||||
#[error("Error creating new Olm session for {0} {1}: {2:?}")]
|
||||
OlmError(UserId, Box<DeviceId>, OlmSessionError),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,347 @@
|
||||
// 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.
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
io::{Error as IoError, ErrorKind, Read},
|
||||
};
|
||||
|
||||
use thiserror::Error;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use matrix_sdk_common::events::room::JsonWebKey;
|
||||
|
||||
use getrandom::getrandom;
|
||||
|
||||
use aes_ctr::{
|
||||
cipher::{NewStreamCipher, SyncStreamCipher},
|
||||
Aes256Ctr,
|
||||
};
|
||||
use base64::DecodeError;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::utilities::{decode, decode_url_safe, encode, encode_url_safe};
|
||||
|
||||
const IV_SIZE: usize = 16;
|
||||
const KEY_SIZE: usize = 32;
|
||||
const VERSION: &str = "v2";
|
||||
|
||||
/// A wrapper that transparently encrypts anything that implements `Read` as an
|
||||
/// Matrix attachment.
|
||||
#[derive(Debug)]
|
||||
pub struct AttachmentDecryptor<'a, R: 'a + Read> {
|
||||
inner_reader: &'a mut R,
|
||||
expected_hash: Vec<u8>,
|
||||
sha: Sha256,
|
||||
aes: Aes256Ctr,
|
||||
}
|
||||
|
||||
impl<'a, R: Read> Read for AttachmentDecryptor<'a, R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let read_bytes = self.inner_reader.read(buf)?;
|
||||
|
||||
if read_bytes == 0 {
|
||||
let hash = self.sha.finalize_reset();
|
||||
|
||||
if hash.as_slice() == self.expected_hash.as_slice() {
|
||||
Ok(0)
|
||||
} else {
|
||||
Err(IoError::new(
|
||||
ErrorKind::Other,
|
||||
"Hash missmatch while decrypting",
|
||||
))
|
||||
}
|
||||
} else {
|
||||
self.sha.update(&buf[0..read_bytes]);
|
||||
self.aes.apply_keystream(&mut buf[0..read_bytes]);
|
||||
|
||||
Ok(read_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for attachment decryption.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum DecryptorError {
|
||||
/// Some data in the encrypted attachment coldn't be decoded, this may be a
|
||||
/// hash, the secret key, or the initialization vector.
|
||||
#[error(transparent)]
|
||||
Decode(#[from] DecodeError),
|
||||
/// A hash is missing from the encryption info.
|
||||
#[error("The encryption info is missing a hash")]
|
||||
MissingHash,
|
||||
/// The supplied key or IV has an invalid length.
|
||||
#[error("The supplied key or IV has an invalid length.")]
|
||||
KeyNonceLength,
|
||||
/// The supplied data was encrypted with an unknown version of the
|
||||
/// attachment encryption spec.
|
||||
#[error("Unknown version for the encrypted attachment.")]
|
||||
UnknownVersion,
|
||||
}
|
||||
|
||||
impl<'a, R: Read + 'a> AttachmentDecryptor<'a, R> {
|
||||
/// Wrap the given reader decrypting all the data we read from it.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `reader` - The `Reader` that should be wrapped and decrypted.
|
||||
///
|
||||
/// * `info` - The encryption info that is necessary to decrypt data from
|
||||
/// the reader.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::io::{Cursor, Read};
|
||||
/// # use matrix_sdk_crypto::{AttachmentEncryptor, AttachmentDecryptor};
|
||||
/// let data = "Hello world".to_owned();
|
||||
/// let mut cursor = Cursor::new(data.clone());
|
||||
///
|
||||
/// let mut encryptor = AttachmentEncryptor::new(&mut cursor);
|
||||
///
|
||||
/// let mut encrypted = Vec::new();
|
||||
/// encryptor.read_to_end(&mut encrypted).unwrap();
|
||||
/// let info = encryptor.finish();
|
||||
///
|
||||
/// let mut cursor = Cursor::new(encrypted);
|
||||
/// let mut decryptor = AttachmentDecryptor::new(&mut cursor, info).unwrap();
|
||||
/// let mut decrypted_data = Vec::new();
|
||||
/// decryptor.read_to_end(&mut decrypted_data).unwrap();
|
||||
///
|
||||
/// let decrypted = String::from_utf8(decrypted_data).unwrap();
|
||||
/// ```
|
||||
pub fn new(
|
||||
input: &'a mut R,
|
||||
info: EncryptionInfo,
|
||||
) -> Result<AttachmentDecryptor<'a, R>, DecryptorError> {
|
||||
if info.version != VERSION {
|
||||
return Err(DecryptorError::UnknownVersion);
|
||||
}
|
||||
|
||||
let hash = decode(
|
||||
info.hashes
|
||||
.get("sha256")
|
||||
.ok_or(DecryptorError::MissingHash)?,
|
||||
)?;
|
||||
let key = Zeroizing::from(decode_url_safe(info.web_key.k)?);
|
||||
let iv = decode(info.iv)?;
|
||||
|
||||
let sha = Sha256::default();
|
||||
let aes = Aes256Ctr::new_var(&key, &iv).map_err(|_| DecryptorError::KeyNonceLength)?;
|
||||
|
||||
Ok(AttachmentDecryptor {
|
||||
inner_reader: input,
|
||||
expected_hash: hash,
|
||||
sha,
|
||||
aes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper that transparently encrypts anything that implements `Read`.
|
||||
#[derive(Debug)]
|
||||
pub struct AttachmentEncryptor<'a, R: Read + 'a> {
|
||||
finished: bool,
|
||||
inner_reader: &'a mut R,
|
||||
web_key: JsonWebKey,
|
||||
iv: String,
|
||||
hashes: BTreeMap<String, String>,
|
||||
aes: Aes256Ctr,
|
||||
sha: Sha256,
|
||||
}
|
||||
|
||||
impl<'a, R: Read + 'a> Read for AttachmentEncryptor<'a, R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let read_bytes = self.inner_reader.read(buf)?;
|
||||
|
||||
if read_bytes == 0 {
|
||||
let hash = self.sha.finalize_reset();
|
||||
self.hashes
|
||||
.entry("sha256".to_owned())
|
||||
.or_insert_with(|| encode(hash));
|
||||
Ok(0)
|
||||
} else {
|
||||
self.aes.apply_keystream(&mut buf[0..read_bytes]);
|
||||
self.sha.update(&buf[0..read_bytes]);
|
||||
|
||||
Ok(read_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, R: Read + 'a> AttachmentEncryptor<'a, R> {
|
||||
/// Wrap the given reader encrypting all the data we read from it.
|
||||
///
|
||||
/// After all the reads are done, and all the data is encrypted that we wish
|
||||
/// to encrypt a call to [`finish()`](#method.finish) is necessary to get
|
||||
/// the decryption key for the data.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `reader` - The `Reader` that should be wrapped and enrypted.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if we can't generate enough random data to create a fresh
|
||||
/// encryption key.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use std::io::{Cursor, Read};
|
||||
/// # use matrix_sdk_crypto::AttachmentEncryptor;
|
||||
/// let data = "Hello world".to_owned();
|
||||
/// let mut cursor = Cursor::new(data.clone());
|
||||
///
|
||||
/// let mut encryptor = AttachmentEncryptor::new(&mut cursor);
|
||||
///
|
||||
/// let mut encrypted = Vec::new();
|
||||
/// encryptor.read_to_end(&mut encrypted).unwrap();
|
||||
/// let key = encryptor.finish();
|
||||
/// ```
|
||||
pub fn new(reader: &'a mut R) -> Self {
|
||||
let mut key = Zeroizing::new([0u8; KEY_SIZE]);
|
||||
let mut iv = Zeroizing::new([0u8; IV_SIZE]);
|
||||
|
||||
getrandom(&mut *key).expect("Can't generate randomness");
|
||||
// Only populate the the first 8 bits with randomness, the rest is 0
|
||||
// initialized.
|
||||
getrandom(&mut iv[0..8]).expect("Can't generate randomness");
|
||||
|
||||
let web_key = JsonWebKey {
|
||||
kty: "oct".to_owned(),
|
||||
key_ops: vec!["encrypt".to_owned(), "decrypt".to_owned()],
|
||||
alg: "A256CTR".to_owned(),
|
||||
k: encode_url_safe(&*key),
|
||||
ext: true,
|
||||
};
|
||||
let encoded_iv = encode(&*iv);
|
||||
|
||||
let aes = Aes256Ctr::new_var(&*key, &*iv).expect("Cannot create AES encryption object.");
|
||||
|
||||
AttachmentEncryptor {
|
||||
finished: false,
|
||||
inner_reader: reader,
|
||||
iv: encoded_iv,
|
||||
web_key,
|
||||
hashes: BTreeMap::new(),
|
||||
aes,
|
||||
sha: Sha256::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Consume the encryptor and get the encryption key.
|
||||
pub fn finish(mut self) -> EncryptionInfo {
|
||||
let hash = self.sha.finalize();
|
||||
self.hashes
|
||||
.entry("sha256".to_owned())
|
||||
.or_insert_with(|| encode(hash));
|
||||
|
||||
EncryptionInfo {
|
||||
version: VERSION.to_string(),
|
||||
hashes: self.hashes,
|
||||
iv: self.iv,
|
||||
web_key: self.web_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct EncryptionInfo {
|
||||
#[serde(rename = "v")]
|
||||
pub version: String,
|
||||
pub web_key: JsonWebKey,
|
||||
pub iv: String,
|
||||
pub hashes: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{AttachmentDecryptor, AttachmentEncryptor, EncryptionInfo};
|
||||
use serde_json::json;
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
const EXAMPLE_DATA: &[u8] = &[
|
||||
179, 154, 118, 127, 186, 127, 110, 33, 203, 33, 33, 134, 67, 100, 173, 46, 235, 27, 215,
|
||||
172, 36, 26, 75, 47, 33, 160,
|
||||
];
|
||||
|
||||
fn example_key() -> EncryptionInfo {
|
||||
let info = json!({
|
||||
"v": "v2",
|
||||
"web_key": {
|
||||
"kty": "oct",
|
||||
"alg": "A256CTR",
|
||||
"ext": true,
|
||||
"k": "Voq2nkPme_x8no5-Tjq_laDAdxE6iDbxnlQXxwFPgE4",
|
||||
"key_ops": ["encrypt", "decrypt"]
|
||||
},
|
||||
"iv": "i0DovxYdJEcAAAAAAAAAAA",
|
||||
"hashes": {
|
||||
"sha256": "ANdt819a8bZl4jKy3Z+jcqtiNICa2y0AW4BBJ/iQRAU"
|
||||
}
|
||||
});
|
||||
|
||||
serde_json::from_value(info).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypt_decrypt_cycle() {
|
||||
let data = "Hello world".to_owned();
|
||||
let mut cursor = Cursor::new(data.clone());
|
||||
|
||||
let mut encryptor = AttachmentEncryptor::new(&mut cursor);
|
||||
|
||||
let mut encrypted = Vec::new();
|
||||
|
||||
encryptor.read_to_end(&mut encrypted).unwrap();
|
||||
let key = encryptor.finish();
|
||||
assert_ne!(encrypted.as_slice(), data.as_bytes());
|
||||
|
||||
let mut cursor = Cursor::new(encrypted);
|
||||
let mut decryptor = AttachmentDecryptor::new(&mut cursor, key).unwrap();
|
||||
let mut decrypted_data = Vec::new();
|
||||
|
||||
decryptor.read_to_end(&mut decrypted_data).unwrap();
|
||||
|
||||
let decrypted = String::from_utf8(decrypted_data).unwrap();
|
||||
|
||||
assert_eq!(data, decrypted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_decrypt() {
|
||||
let mut cursor = Cursor::new(EXAMPLE_DATA.to_vec());
|
||||
let key = example_key();
|
||||
|
||||
let mut decryptor = AttachmentDecryptor::new(&mut cursor, key).unwrap();
|
||||
let mut decrypted_data = Vec::new();
|
||||
|
||||
decryptor.read_to_end(&mut decrypted_data).unwrap();
|
||||
let decrypted = String::from_utf8(decrypted_data).unwrap();
|
||||
|
||||
assert_eq!("It's a secret to everybody", decrypted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decrypt_invalid_hash() {
|
||||
let mut cursor = Cursor::new("fake message");
|
||||
let key = example_key();
|
||||
|
||||
let mut decryptor = AttachmentDecryptor::new(&mut cursor, key).unwrap();
|
||||
let mut decrypted_data = Vec::new();
|
||||
|
||||
assert!(decryptor.read_to_end(&mut decrypted_data).is_err())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
// 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.
|
||||
|
||||
use serde_json::Error as SerdeError;
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom};
|
||||
use thiserror::Error;
|
||||
|
||||
use byteorder::{BigEndian, ReadBytesExt};
|
||||
use getrandom::getrandom;
|
||||
|
||||
use aes_ctr::{
|
||||
cipher::{NewStreamCipher, SyncStreamCipher},
|
||||
Aes256Ctr,
|
||||
};
|
||||
use hmac::{Hmac, Mac, NewMac};
|
||||
use pbkdf2::pbkdf2;
|
||||
use sha2::{Sha256, Sha512};
|
||||
|
||||
use crate::{
|
||||
olm::ExportedRoomKey,
|
||||
utilities::{decode, encode, DecodeError},
|
||||
};
|
||||
|
||||
const SALT_SIZE: usize = 16;
|
||||
const IV_SIZE: usize = 16;
|
||||
const MAC_SIZE: usize = 32;
|
||||
const KEY_SIZE: usize = 32;
|
||||
const VERSION: u8 = 1;
|
||||
|
||||
const HEADER: &str = "-----BEGIN MEGOLM SESSION DATA-----";
|
||||
const FOOTER: &str = "-----END MEGOLM SESSION DATA-----";
|
||||
|
||||
/// Error representing a failure during key export or import.
|
||||
#[derive(Error, Debug)]
|
||||
pub enum KeyExportError {
|
||||
/// The key export doesn't contain valid headers.
|
||||
#[error("Invalid or missing key export headers.")]
|
||||
InvalidHeaders,
|
||||
/// The key export has been encrypted with an unsupported version.
|
||||
#[error("The key export has been encrypted with an unsupported version.")]
|
||||
UnsupportedVersion,
|
||||
/// The MAC of the encrypted payload is invalid.
|
||||
#[error("The MAC of the encrypted payload is invalid.")]
|
||||
InvalidMAC,
|
||||
/// The decrypted key export isn't valid UTF-8.
|
||||
#[error(transparent)]
|
||||
InvalidUtf8(#[from] std::string::FromUtf8Error),
|
||||
/// The decrypted key export doesn't contain valid JSON.
|
||||
#[error(transparent)]
|
||||
Json(#[from] SerdeError),
|
||||
/// The key export string isn't valid base64.
|
||||
#[error(transparent)]
|
||||
Decode(#[from] DecodeError),
|
||||
/// The key export doesn't all the required fields.
|
||||
#[error(transparent)]
|
||||
IO(#[from] std::io::Error),
|
||||
}
|
||||
|
||||
/// Try to decrypt a reader into a list of exported room keys.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `passphrase` - The passphrase that was used to encrypt the exported keys.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```no_run
|
||||
/// # use std::io::Cursor;
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, decrypt_key_export};
|
||||
/// # use matrix_sdk_common::identifiers::user_id;
|
||||
/// # use futures::executor::block_on;
|
||||
/// # let alice = user_id!("@alice:example.org");
|
||||
/// # let machine = OlmMachine::new(&alice, "DEVICEID".into());
|
||||
/// # block_on(async {
|
||||
/// # let export = Cursor::new("".to_owned());
|
||||
/// let exported_keys = decrypt_key_export(export, "1234").unwrap();
|
||||
/// machine.import_keys(exported_keys).await.unwrap();
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn decrypt_key_export(
|
||||
mut input: impl Read,
|
||||
passphrase: &str,
|
||||
) -> Result<Vec<ExportedRoomKey>, KeyExportError> {
|
||||
let mut x: String = String::new();
|
||||
|
||||
input.read_to_string(&mut x)?;
|
||||
|
||||
if !(x.trim_start().starts_with(HEADER) && x.trim_end().ends_with(FOOTER)) {
|
||||
return Err(KeyExportError::InvalidHeaders);
|
||||
}
|
||||
|
||||
let payload: String = x
|
||||
.lines()
|
||||
.filter(|l| !(l.starts_with(HEADER) || l.starts_with(FOOTER)))
|
||||
.collect();
|
||||
|
||||
Ok(serde_json::from_str(&decrypt_helper(
|
||||
&payload, passphrase,
|
||||
)?)?)
|
||||
}
|
||||
|
||||
/// Encrypt the list of exported room keys using the given passphrase.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `keys` - A list of sessions that should be encrypted.
|
||||
///
|
||||
/// * `passphrase` - The passphrase that will be used to encrypt the exported
|
||||
/// room keys.
|
||||
///
|
||||
/// * `rounds` - The number of rounds that should be used for the key
|
||||
/// derivation when the passphrase gets turned into an AES key. More rounds are
|
||||
/// increasingly computationally intensive and as such help against bruteforce
|
||||
/// attacks. Should be at least `10000`, while values in the `100000` ranges
|
||||
/// should be preferred.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This method will panic if it can't get enough randomness from the OS to
|
||||
/// encrypt the exported keys securely.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```no_run
|
||||
/// # use matrix_sdk_crypto::{OlmMachine, encrypt_key_export};
|
||||
/// # use matrix_sdk_common::identifiers::{user_id, room_id};
|
||||
/// # use futures::executor::block_on;
|
||||
/// # let alice = user_id!("@alice:example.org");
|
||||
/// # let machine = OlmMachine::new(&alice, "DEVICEID".into());
|
||||
/// # block_on(async {
|
||||
/// let room_id = room_id!("!test:localhost");
|
||||
/// let exported_keys = machine.export_keys(|s| s.room_id() == &room_id).await.unwrap();
|
||||
/// let encrypted_export = encrypt_key_export(&exported_keys, "1234", 1);
|
||||
/// # });
|
||||
/// ```
|
||||
pub fn encrypt_key_export(
|
||||
keys: &[ExportedRoomKey],
|
||||
passphrase: &str,
|
||||
rounds: u32,
|
||||
) -> Result<String, SerdeError> {
|
||||
let mut plaintext = serde_json::to_string(keys)?.into_bytes();
|
||||
let ciphertext = encrypt_helper(&mut plaintext, passphrase, rounds);
|
||||
Ok([HEADER.to_owned(), ciphertext, FOOTER.to_owned()].join("\n"))
|
||||
}
|
||||
|
||||
fn encrypt_helper(mut plaintext: &mut [u8], passphrase: &str, rounds: u32) -> String {
|
||||
let mut salt = [0u8; SALT_SIZE];
|
||||
let mut iv = [0u8; IV_SIZE];
|
||||
let mut derived_keys = [0u8; KEY_SIZE * 2];
|
||||
|
||||
getrandom(&mut salt).expect("Can't generate randomness");
|
||||
getrandom(&mut iv).expect("Can't generate randomness");
|
||||
|
||||
let mut iv = u128::from_be_bytes(iv);
|
||||
iv &= !(1 << 63);
|
||||
|
||||
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), &salt, rounds, &mut derived_keys);
|
||||
let (key, hmac_key) = derived_keys.split_at(KEY_SIZE);
|
||||
|
||||
let mut aes = Aes256Ctr::new_var(&key, &iv.to_be_bytes()).expect("Can't create AES object");
|
||||
|
||||
aes.apply_keystream(&mut plaintext);
|
||||
|
||||
let mut payload: Vec<u8> = vec![];
|
||||
|
||||
payload.extend(&VERSION.to_be_bytes());
|
||||
payload.extend(&salt);
|
||||
payload.extend(&iv.to_be_bytes());
|
||||
payload.extend(&rounds.to_be_bytes());
|
||||
payload.extend_from_slice(&plaintext);
|
||||
|
||||
let mut hmac = Hmac::<Sha256>::new_varkey(hmac_key).expect("Can't create HMAC object");
|
||||
hmac.update(&payload);
|
||||
let mac = hmac.finalize();
|
||||
|
||||
payload.extend(mac.into_bytes());
|
||||
|
||||
encode(payload)
|
||||
}
|
||||
|
||||
fn decrypt_helper(ciphertext: &str, passphrase: &str) -> Result<String, KeyExportError> {
|
||||
let decoded = decode(ciphertext)?;
|
||||
|
||||
let mut decoded = Cursor::new(decoded);
|
||||
|
||||
let mut salt = [0u8; SALT_SIZE];
|
||||
let mut iv = [0u8; IV_SIZE];
|
||||
let mut mac = [0u8; MAC_SIZE];
|
||||
let mut derived_keys = [0u8; KEY_SIZE * 2];
|
||||
|
||||
let version = decoded.read_u8()?;
|
||||
decoded.read_exact(&mut salt)?;
|
||||
decoded.read_exact(&mut iv)?;
|
||||
|
||||
let rounds = decoded.read_u32::<BigEndian>()?;
|
||||
let ciphertext_start = decoded.position() as usize;
|
||||
|
||||
decoded.seek(SeekFrom::End(-32))?;
|
||||
let ciphertext_end = decoded.position() as usize;
|
||||
|
||||
decoded.read_exact(&mut mac)?;
|
||||
|
||||
let mut decoded = decoded.into_inner();
|
||||
|
||||
if version != VERSION {
|
||||
return Err(KeyExportError::UnsupportedVersion);
|
||||
}
|
||||
|
||||
pbkdf2::<Hmac<Sha512>>(passphrase.as_bytes(), &salt, rounds, &mut derived_keys);
|
||||
let (key, hmac_key) = derived_keys.split_at(KEY_SIZE);
|
||||
|
||||
let mut hmac = Hmac::<Sha256>::new_varkey(hmac_key).expect("Can't create an HMAC object");
|
||||
hmac.update(&decoded[0..ciphertext_end]);
|
||||
hmac.verify(&mac).map_err(|_| KeyExportError::InvalidMAC)?;
|
||||
|
||||
let mut ciphertext = &mut decoded[ciphertext_start..ciphertext_end];
|
||||
let mut aes = Aes256Ctr::new_var(&key, &iv).expect("Can't create an AES object");
|
||||
aes.apply_keystream(&mut ciphertext);
|
||||
|
||||
Ok(String::from_utf8(ciphertext.to_owned())?)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use indoc::indoc;
|
||||
use proptest::prelude::*;
|
||||
use std::io::Cursor;
|
||||
|
||||
use matrix_sdk_common::identifiers::room_id;
|
||||
use matrix_sdk_test::async_test;
|
||||
|
||||
use super::{decode, decrypt_helper, decrypt_key_export, encrypt_helper, encrypt_key_export};
|
||||
use crate::machine::test::get_prepared_machine;
|
||||
|
||||
const PASSPHRASE: &str = "1234";
|
||||
|
||||
const TEST_EXPORT: &str = indoc! {"
|
||||
-----BEGIN MEGOLM SESSION DATA-----
|
||||
Af7mGhlzQ+eGvHu93u0YXd3D/+vYMs3E7gQqOhuCtkvGAAAAASH7pEdWvFyAP1JUisAcpEo
|
||||
Xke2Q7Kr9hVl/SCc6jXBNeJCZcrUbUV4D/tRQIl3E9L4fOk928YI1J+3z96qiH0uE7hpsCI
|
||||
CkHKwjPU+0XTzFdIk1X8H7sZ+MD/2Sg/q3y8rtUjz7uEj4GUTnb+9SCOTVmJsRfqgUpM1CU
|
||||
bDLytHf1JkohY4tWEgpsCc67xdzgodjr12qYrfg/zNm3LGpxlrffJknw4rk5QFTj4kMbqbD
|
||||
ZZgDTni+HxRTDGge2J620lMOiznvXX+H09Rwruqx5aJvvaaKd86jWRpiO2oSFqHn4u5ONl9
|
||||
41uzm62Sj0eIm6ZbA9NQs87jQw4LxsejhZVL+NdjIg80zVSBTWhTdo0DTnbFSNP4ReOiz0U
|
||||
XosOF8A5T8Vdx2nvA0GXltfcHKVKQYh/LJAkNQ7P9UYL4ae/5TtQZkhB1KxCLTRWqADCl53
|
||||
uBMGpG53EMgY6G6K2DEIOkcv7sdXQF5WpemiSWZqJRWj+cjfs9BpCTbkp/rszWFl2TniWpR
|
||||
RqIbT2jORlN4rTvdtF0F4z1pqP4qWyR3sLNTkXm9CFRzWADNG0RDZKxbCoo6RPvtaCTfaHo
|
||||
SwfvzBS6CjfAG+FOugpV48o7+XetaUUPZ6/tZSPhCdeV8eP9q5r0QwWeXFogzoNzWt4HYx9
|
||||
MdXxzD+f0mtg5gzehrrEEARwI2bCvPpHxlt/Na9oW/GBpkjwR1LSKgg4CtpRyWngPjdEKpZ
|
||||
GYW19pdjg0qdXNk/eqZsQTsNWVo6A
|
||||
-----END MEGOLM SESSION DATA-----
|
||||
"};
|
||||
|
||||
fn export_wihtout_headers() -> String {
|
||||
TEST_EXPORT
|
||||
.lines()
|
||||
.filter(|l| !l.starts_with("-----"))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_decode() {
|
||||
let export = export_wihtout_headers();
|
||||
assert!(decode(export).is_ok());
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn proptest_encrypt_cycle(plaintext in prop::string::string_regex(".*").unwrap()) {
|
||||
let mut plaintext_bytes = plaintext.clone().into_bytes();
|
||||
|
||||
let ciphertext = encrypt_helper(&mut plaintext_bytes, "test", 1);
|
||||
let decrypted = decrypt_helper(&ciphertext, "test").unwrap();
|
||||
|
||||
prop_assert!(plaintext == decrypted);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt() {
|
||||
let data = "It's a secret to everybody";
|
||||
let mut bytes = data.to_owned().into_bytes();
|
||||
|
||||
let encrypted = encrypt_helper(&mut bytes, PASSPHRASE, 10);
|
||||
let decrypted = decrypt_helper(&encrypted, PASSPHRASE).unwrap();
|
||||
|
||||
assert_eq!(data, decrypted);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_session_encrypt() {
|
||||
let (machine, _) = get_prepared_machine().await;
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
machine
|
||||
.create_outbound_group_session_with_defaults(&room_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let export = machine
|
||||
.export_keys(|s| s.room_id() == &room_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!export.is_empty());
|
||||
|
||||
let encrypted = encrypt_key_export(&export, "1234", 1).unwrap();
|
||||
let decrypted = decrypt_key_export(Cursor::new(encrypted), "1234").unwrap();
|
||||
|
||||
assert_eq!(export, decrypted);
|
||||
assert_eq!(machine.import_keys(decrypted).await.unwrap(), (0, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_real_decrypt() {
|
||||
let reader = Cursor::new(TEST_EXPORT);
|
||||
let imported = decrypt_key_export(reader, PASSPHRASE).expect("Can't decrypt key export");
|
||||
assert!(!imported.is_empty())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod attachments;
|
||||
mod key_export;
|
||||
|
||||
pub use attachments::{AttachmentDecryptor, AttachmentEncryptor, DecryptorError};
|
||||
pub use key_export::{decrypt_key_export, encrypt_key_export};
|
||||
@@ -0,0 +1,618 @@
|
||||
// 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.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap},
|
||||
convert::{TryFrom, TryInto},
|
||||
ops::Deref,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use atomic::Atomic;
|
||||
use matrix_sdk_common::{
|
||||
api::r0::keys::SignedKey,
|
||||
encryption::DeviceKeys,
|
||||
events::{
|
||||
forwarded_room_key::ForwardedRoomKeyToDeviceEventContent,
|
||||
room::encrypted::EncryptedEventContent, EventType,
|
||||
},
|
||||
identifiers::{
|
||||
DeviceId, DeviceIdBox, DeviceKeyAlgorithm, DeviceKeyId, EventEncryptionAlgorithm, UserId,
|
||||
},
|
||||
locks::Mutex,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
use tracing::warn;
|
||||
|
||||
use crate::{
|
||||
olm::{InboundGroupSession, PrivateCrossSigningIdentity, Session},
|
||||
store::{Changes, DeviceChanges},
|
||||
};
|
||||
#[cfg(test)]
|
||||
use crate::{OlmMachine, ReadOnlyAccount};
|
||||
|
||||
use crate::{
|
||||
error::{EventError, OlmError, OlmResult, SignatureError},
|
||||
identities::{OwnUserIdentity, UserIdentities},
|
||||
olm::Utility,
|
||||
store::{CryptoStore, Result as StoreResult},
|
||||
verification::VerificationMachine,
|
||||
Sas, ToDeviceRequest,
|
||||
};
|
||||
|
||||
/// A read-only version of a `Device`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ReadOnlyDevice {
|
||||
user_id: Arc<UserId>,
|
||||
device_id: Arc<Box<DeviceId>>,
|
||||
algorithms: Arc<[EventEncryptionAlgorithm]>,
|
||||
keys: Arc<BTreeMap<DeviceKeyId, String>>,
|
||||
pub(crate) signatures: Arc<BTreeMap<UserId, BTreeMap<DeviceKeyId, String>>>,
|
||||
display_name: Arc<Option<String>>,
|
||||
deleted: Arc<AtomicBool>,
|
||||
trust_state: Arc<Atomic<LocalTrust>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
/// A device represents a E2EE capable client of an user.
|
||||
pub struct Device {
|
||||
pub(crate) inner: ReadOnlyDevice,
|
||||
pub(crate) private_identity: Arc<Mutex<PrivateCrossSigningIdentity>>,
|
||||
pub(crate) verification_machine: VerificationMachine,
|
||||
pub(crate) own_identity: Option<OwnUserIdentity>,
|
||||
pub(crate) device_owner_identity: Option<UserIdentities>,
|
||||
}
|
||||
|
||||
impl Deref for Device {
|
||||
type Target = ReadOnlyDevice;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl Device {
|
||||
/// Start a interactive verification with this `Device`
|
||||
///
|
||||
/// Returns a `Sas` object and to-device request that needs to be sent out.
|
||||
pub async fn start_verification(&self) -> StoreResult<(Sas, ToDeviceRequest)> {
|
||||
self.verification_machine
|
||||
.start_sas(self.inner.clone())
|
||||
.await
|
||||
}
|
||||
|
||||
/// Get the Olm sessions that belong to this device.
|
||||
pub(crate) async fn get_sessions(&self) -> StoreResult<Option<Arc<Mutex<Vec<Session>>>>> {
|
||||
if let Some(k) = self.get_key(DeviceKeyAlgorithm::Curve25519) {
|
||||
self.verification_machine.store.get_sessions(k).await
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the trust state of the device.
|
||||
pub fn trust_state(&self) -> bool {
|
||||
self.inner
|
||||
.trust_state(&self.own_identity, &self.device_owner_identity)
|
||||
}
|
||||
|
||||
/// Set the local trust state of the device to the given state.
|
||||
///
|
||||
/// This won't affect any cross signing trust state, this only sets a flag
|
||||
/// marking to have the given trust state.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `trust_state` - The new trust state that should be set for the device.
|
||||
pub async fn set_local_trust(&self, trust_state: LocalTrust) -> StoreResult<()> {
|
||||
self.inner.set_trust_state(trust_state);
|
||||
|
||||
let changes = Changes {
|
||||
devices: DeviceChanges {
|
||||
changed: vec![self.inner.clone()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
self.verification_machine.store.save_changes(changes).await
|
||||
}
|
||||
|
||||
/// Encrypt the given content for this `Device`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event_type` - The type of the event.
|
||||
///
|
||||
/// * `content` - The content of the event that should be encrypted.
|
||||
pub(crate) async fn encrypt(
|
||||
&self,
|
||||
event_type: EventType,
|
||||
content: Value,
|
||||
) -> OlmResult<(Session, EncryptedEventContent)> {
|
||||
self.inner
|
||||
.encrypt(&**self.verification_machine.store, event_type, content)
|
||||
.await
|
||||
}
|
||||
|
||||
/// Encrypt the given inbound group session as a forwarded room key for this
|
||||
/// device.
|
||||
pub async fn encrypt_session(
|
||||
&self,
|
||||
session: InboundGroupSession,
|
||||
) -> OlmResult<(Session, EncryptedEventContent)> {
|
||||
let export = session.export().await;
|
||||
|
||||
let content: ForwardedRoomKeyToDeviceEventContent = if let Ok(c) = export.try_into() {
|
||||
c
|
||||
} else {
|
||||
// TODO remove this panic.
|
||||
panic!(
|
||||
"Can't share session {} with device {} {}, key export can't \
|
||||
be converted to a forwarded room key content",
|
||||
session.session_id(),
|
||||
self.user_id(),
|
||||
self.device_id()
|
||||
);
|
||||
};
|
||||
|
||||
let content = serde_json::to_value(content)?;
|
||||
self.encrypt(EventType::ForwardedRoomKey, content).await
|
||||
}
|
||||
}
|
||||
|
||||
/// A read only view over all devices belonging to a user.
|
||||
#[derive(Debug)]
|
||||
pub struct UserDevices {
|
||||
pub(crate) inner: HashMap<DeviceIdBox, ReadOnlyDevice>,
|
||||
pub(crate) private_identity: Arc<Mutex<PrivateCrossSigningIdentity>>,
|
||||
pub(crate) verification_machine: VerificationMachine,
|
||||
pub(crate) own_identity: Option<OwnUserIdentity>,
|
||||
pub(crate) device_owner_identity: Option<UserIdentities>,
|
||||
}
|
||||
|
||||
impl UserDevices {
|
||||
/// Get the specific device with the given device id.
|
||||
pub fn get(&self, device_id: &DeviceId) -> Option<Device> {
|
||||
self.inner.get(device_id).map(|d| Device {
|
||||
inner: d.clone(),
|
||||
private_identity: self.private_identity.clone(),
|
||||
verification_machine: self.verification_machine.clone(),
|
||||
own_identity: self.own_identity.clone(),
|
||||
device_owner_identity: self.device_owner_identity.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterator over all the device ids of the user devices.
|
||||
pub fn keys(&self) -> impl Iterator<Item = &DeviceIdBox> {
|
||||
self.inner.keys()
|
||||
}
|
||||
|
||||
/// Iterator over all the devices of the user devices.
|
||||
pub fn devices(&self) -> impl Iterator<Item = Device> + '_ {
|
||||
self.inner.values().map(move |d| Device {
|
||||
inner: d.clone(),
|
||||
private_identity: self.private_identity.clone(),
|
||||
verification_machine: self.verification_machine.clone(),
|
||||
own_identity: self.own_identity.clone(),
|
||||
device_owner_identity: self.device_owner_identity.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
/// The local trust state of a device.
|
||||
pub enum LocalTrust {
|
||||
/// The device has been verified and is trusted.
|
||||
Verified = 0,
|
||||
/// The device been blacklisted from communicating.
|
||||
BlackListed = 1,
|
||||
/// The trust state of the device is being ignored.
|
||||
Ignored = 2,
|
||||
/// The trust state is unset.
|
||||
Unset = 3,
|
||||
}
|
||||
|
||||
impl From<i64> for LocalTrust {
|
||||
fn from(state: i64) -> Self {
|
||||
match state {
|
||||
0 => LocalTrust::Verified,
|
||||
1 => LocalTrust::BlackListed,
|
||||
2 => LocalTrust::Ignored,
|
||||
3 => LocalTrust::Unset,
|
||||
_ => LocalTrust::Unset,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReadOnlyDevice {
|
||||
/// Create a new Device.
|
||||
pub fn new(
|
||||
user_id: UserId,
|
||||
device_id: Box<DeviceId>,
|
||||
display_name: Option<String>,
|
||||
trust_state: LocalTrust,
|
||||
algorithms: Vec<EventEncryptionAlgorithm>,
|
||||
keys: BTreeMap<DeviceKeyId, String>,
|
||||
signatures: BTreeMap<UserId, BTreeMap<DeviceKeyId, String>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
user_id: Arc::new(user_id),
|
||||
device_id: Arc::new(device_id),
|
||||
display_name: Arc::new(display_name),
|
||||
trust_state: Arc::new(Atomic::new(trust_state)),
|
||||
signatures: Arc::new(signatures),
|
||||
algorithms: algorithms.into(),
|
||||
keys: Arc::new(keys),
|
||||
deleted: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// The user id of the device owner.
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
&self.user_id
|
||||
}
|
||||
|
||||
/// The unique ID of the device.
|
||||
pub fn device_id(&self) -> &DeviceId {
|
||||
&self.device_id
|
||||
}
|
||||
|
||||
/// Get the human readable name of the device.
|
||||
pub fn display_name(&self) -> &Option<String> {
|
||||
&self.display_name
|
||||
}
|
||||
|
||||
/// Get the key of the given key algorithm belonging to this device.
|
||||
pub fn get_key(&self, algorithm: DeviceKeyAlgorithm) -> Option<&String> {
|
||||
self.keys
|
||||
.get(&DeviceKeyId::from_parts(algorithm, &self.device_id))
|
||||
}
|
||||
|
||||
/// Get a map containing all the device keys.
|
||||
pub fn keys(&self) -> &BTreeMap<DeviceKeyId, String> {
|
||||
&self.keys
|
||||
}
|
||||
|
||||
/// Get a map containing all the device signatures.
|
||||
pub fn signatures(&self) -> &BTreeMap<UserId, BTreeMap<DeviceKeyId, String>> {
|
||||
&self.signatures
|
||||
}
|
||||
|
||||
/// Get the trust state of the device.
|
||||
pub fn local_trust_state(&self) -> LocalTrust {
|
||||
self.trust_state.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Is the device locally marked as trusted.
|
||||
pub fn is_trusted(&self) -> bool {
|
||||
self.local_trust_state() == LocalTrust::Verified
|
||||
}
|
||||
|
||||
/// Is the device locally marked as blacklisted.
|
||||
///
|
||||
/// Blacklisted devices won't receive any group sessions.
|
||||
pub fn is_blacklisted(&self) -> bool {
|
||||
self.local_trust_state() == LocalTrust::BlackListed
|
||||
}
|
||||
|
||||
/// Set the trust state of the device to the given state.
|
||||
///
|
||||
/// Note: This should only done in the cryptostore where the trust state can
|
||||
/// be stored.
|
||||
pub(crate) fn set_trust_state(&self, state: LocalTrust) {
|
||||
self.trust_state.store(state, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Get the list of algorithms this device supports.
|
||||
pub fn algorithms(&self) -> &[EventEncryptionAlgorithm] {
|
||||
&self.algorithms
|
||||
}
|
||||
|
||||
/// Is the device deleted.
|
||||
pub fn deleted(&self) -> bool {
|
||||
self.deleted.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub(crate) fn trust_state(
|
||||
&self,
|
||||
own_identity: &Option<OwnUserIdentity>,
|
||||
device_owner: &Option<UserIdentities>,
|
||||
) -> bool {
|
||||
// TODO we want to return an enum mentioning if the trust is local, if
|
||||
// only the identity is trusted, if the identity and the device are
|
||||
// trusted.
|
||||
if self.is_trusted() {
|
||||
// If the device is localy marked as verified just return so, no
|
||||
// need to check signatures.
|
||||
true
|
||||
} else {
|
||||
own_identity.as_ref().map_or(false, |own_identity| {
|
||||
// Our own identity needs to be marked as verified.
|
||||
own_identity.is_verified()
|
||||
&& device_owner
|
||||
.as_ref()
|
||||
.map(|device_identity| match device_identity {
|
||||
// If it's one of our own devices, just check that
|
||||
// we signed the device.
|
||||
UserIdentities::Own(_) => {
|
||||
own_identity.is_device_signed(&self).map_or(false, |_| true)
|
||||
}
|
||||
|
||||
// If it's a device from someone else, first check
|
||||
// that our user has signed the other user and then
|
||||
// check if the other user has signed this device.
|
||||
UserIdentities::Other(device_identity) => {
|
||||
own_identity
|
||||
.is_identity_signed(&device_identity)
|
||||
.map_or(false, |_| true)
|
||||
&& device_identity
|
||||
.is_device_signed(&self)
|
||||
.map_or(false, |_| true)
|
||||
}
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn encrypt(
|
||||
&self,
|
||||
store: &dyn CryptoStore,
|
||||
event_type: EventType,
|
||||
content: Value,
|
||||
) -> OlmResult<(Session, EncryptedEventContent)> {
|
||||
let sender_key = if let Some(k) = self.get_key(DeviceKeyAlgorithm::Curve25519) {
|
||||
k
|
||||
} else {
|
||||
warn!(
|
||||
"Trying to encrypt a Megolm session for user {} on device {}, \
|
||||
but the device doesn't have a curve25519 key",
|
||||
self.user_id(),
|
||||
self.device_id()
|
||||
);
|
||||
return Err(EventError::MissingSenderKey.into());
|
||||
};
|
||||
|
||||
let session = if let Some(s) = store.get_sessions(sender_key).await? {
|
||||
let sessions = s.lock().await;
|
||||
sessions.get(0).cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut session = if let Some(s) = session {
|
||||
s
|
||||
} else {
|
||||
warn!(
|
||||
"Trying to encrypt a Megolm session for user {} on device {}, \
|
||||
but no Olm session is found",
|
||||
self.user_id(),
|
||||
self.device_id()
|
||||
);
|
||||
return Err(OlmError::MissingSession);
|
||||
};
|
||||
|
||||
let message = session.encrypt(&self, event_type, content).await?;
|
||||
|
||||
Ok((session, message))
|
||||
}
|
||||
|
||||
/// Update a device with a new device keys struct.
|
||||
pub(crate) fn update_device(&mut self, device_keys: &DeviceKeys) -> Result<(), SignatureError> {
|
||||
self.verify_device_keys(device_keys)?;
|
||||
|
||||
let display_name = Arc::new(device_keys.unsigned.device_display_name.clone());
|
||||
|
||||
self.algorithms = device_keys.algorithms.as_slice().into();
|
||||
self.keys = Arc::new(device_keys.keys.clone());
|
||||
self.signatures = Arc::new(device_keys.signatures.clone());
|
||||
self.display_name = display_name;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_signed_by_device(&self, json: &mut Value) -> Result<(), SignatureError> {
|
||||
let signing_key = self
|
||||
.get_key(DeviceKeyAlgorithm::Ed25519)
|
||||
.ok_or(SignatureError::MissingSigningKey)?;
|
||||
|
||||
let utility = Utility::new();
|
||||
|
||||
utility.verify_json(
|
||||
&self.user_id,
|
||||
&DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, self.device_id()),
|
||||
signing_key,
|
||||
json,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn as_device_keys(&self) -> DeviceKeys {
|
||||
DeviceKeys::new(
|
||||
self.user_id().clone(),
|
||||
self.device_id().into(),
|
||||
self.algorithms().to_vec(),
|
||||
self.keys().clone(),
|
||||
self.signatures().to_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn as_signature_message(&self) -> Value {
|
||||
json!({
|
||||
"user_id": &*self.user_id,
|
||||
"device_id": &*self.device_id,
|
||||
"keys": &*self.keys,
|
||||
"algorithms": &*self.algorithms,
|
||||
"signatures": &*self.signatures,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn verify_device_keys(
|
||||
&self,
|
||||
device_keys: &DeviceKeys,
|
||||
) -> Result<(), SignatureError> {
|
||||
let mut device_keys = serde_json::to_value(device_keys).unwrap();
|
||||
self.is_signed_by_device(&mut device_keys)
|
||||
}
|
||||
|
||||
pub(crate) fn verify_one_time_key(
|
||||
&self,
|
||||
one_time_key: &SignedKey,
|
||||
) -> Result<(), SignatureError> {
|
||||
self.is_signed_by_device(&mut json!(&one_time_key))
|
||||
}
|
||||
|
||||
/// Mark the device as deleted.
|
||||
pub(crate) fn mark_as_deleted(&self) {
|
||||
self.deleted.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn from_machine(machine: &OlmMachine) -> ReadOnlyDevice {
|
||||
ReadOnlyDevice::from_account(machine.account()).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn from_account(account: &ReadOnlyAccount) -> ReadOnlyDevice {
|
||||
let device_keys = account.device_keys().await;
|
||||
ReadOnlyDevice::try_from(&device_keys).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&DeviceKeys> for ReadOnlyDevice {
|
||||
type Error = SignatureError;
|
||||
|
||||
fn try_from(device_keys: &DeviceKeys) -> Result<Self, Self::Error> {
|
||||
let device = Self {
|
||||
user_id: Arc::new(device_keys.user_id.clone()),
|
||||
device_id: Arc::new(device_keys.device_id.clone()),
|
||||
algorithms: device_keys.algorithms.as_slice().into(),
|
||||
signatures: Arc::new(device_keys.signatures.clone()),
|
||||
keys: Arc::new(device_keys.keys.clone()),
|
||||
display_name: Arc::new(device_keys.unsigned.device_display_name.clone()),
|
||||
deleted: Arc::new(AtomicBool::new(false)),
|
||||
trust_state: Arc::new(Atomic::new(LocalTrust::Unset)),
|
||||
};
|
||||
|
||||
device.verify_device_keys(device_keys)?;
|
||||
Ok(device)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ReadOnlyDevice {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.user_id() == other.user_id() && self.device_id() == other.device_id()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use serde_json::json;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use crate::identities::{LocalTrust, ReadOnlyDevice};
|
||||
use matrix_sdk_common::{
|
||||
encryption::DeviceKeys,
|
||||
identifiers::{user_id, DeviceKeyAlgorithm},
|
||||
};
|
||||
|
||||
fn device_keys() -> DeviceKeys {
|
||||
let device_keys = json!({
|
||||
"algorithms": vec![
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2"
|
||||
],
|
||||
"device_id": "BNYQQWUMXO",
|
||||
"user_id": "@example:localhost",
|
||||
"keys": {
|
||||
"curve25519:BNYQQWUMXO": "xfgbLIC5WAl1OIkpOzoxpCe8FsRDT6nch7NQsOb15nc",
|
||||
"ed25519:BNYQQWUMXO": "2/5LWJMow5zhJqakV88SIc7q/1pa8fmkfgAzx72w9G4"
|
||||
},
|
||||
"signatures": {
|
||||
"@example:localhost": {
|
||||
"ed25519:BNYQQWUMXO": "kTwMrbsLJJM/uFGOj/oqlCaRuw7i9p/6eGrTlXjo8UJMCFAetoyWzoMcF35vSe4S6FTx8RJmqX6rM7ep53MHDQ"
|
||||
}
|
||||
},
|
||||
"unsigned": {
|
||||
"device_display_name": "Alice's mobile phone"
|
||||
}
|
||||
});
|
||||
|
||||
serde_json::from_value(device_keys).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn get_device() -> ReadOnlyDevice {
|
||||
let device_keys = device_keys();
|
||||
ReadOnlyDevice::try_from(&device_keys).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_a_device() {
|
||||
let user_id = user_id!("@example:localhost");
|
||||
let device_id = "BNYQQWUMXO";
|
||||
|
||||
let device = get_device();
|
||||
|
||||
assert_eq!(&user_id, device.user_id());
|
||||
assert_eq!(device_id, device.device_id());
|
||||
assert_eq!(device.algorithms.len(), 2);
|
||||
assert_eq!(LocalTrust::Unset, device.local_trust_state());
|
||||
assert_eq!(
|
||||
"Alice's mobile phone",
|
||||
device.display_name().as_ref().unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
device.get_key(DeviceKeyAlgorithm::Curve25519).unwrap(),
|
||||
"xfgbLIC5WAl1OIkpOzoxpCe8FsRDT6nch7NQsOb15nc"
|
||||
);
|
||||
assert_eq!(
|
||||
device.get_key(DeviceKeyAlgorithm::Ed25519).unwrap(),
|
||||
"2/5LWJMow5zhJqakV88SIc7q/1pa8fmkfgAzx72w9G4"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_a_device() {
|
||||
let mut device = get_device();
|
||||
|
||||
assert_eq!(
|
||||
"Alice's mobile phone",
|
||||
device.display_name().as_ref().unwrap()
|
||||
);
|
||||
|
||||
let display_name = "Alice's work computer".to_owned();
|
||||
|
||||
let mut device_keys = device_keys();
|
||||
device_keys.unsigned.device_display_name = Some(display_name.clone());
|
||||
device.update_device(&device_keys).unwrap();
|
||||
|
||||
assert_eq!(&display_name, device.display_name().as_ref().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_a_device() {
|
||||
let device = get_device();
|
||||
assert!(!device.deleted());
|
||||
|
||||
let device_clone = device.clone();
|
||||
|
||||
device.mark_as_deleted();
|
||||
assert!(device.deleted());
|
||||
assert!(device_clone.deleted());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,734 @@
|
||||
// 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.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
convert::TryFrom,
|
||||
sync::Arc,
|
||||
};
|
||||
use tracing::{info, trace, warn};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
api::r0::keys::get_keys::Response as KeysQueryResponse,
|
||||
encryption::DeviceKeys,
|
||||
identifiers::{DeviceId, DeviceIdBox, UserId},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::OlmResult,
|
||||
identities::{
|
||||
MasterPubkey, OwnUserIdentity, ReadOnlyDevice, SelfSigningPubkey, UserIdentities,
|
||||
UserIdentity, UserSigningPubkey,
|
||||
},
|
||||
requests::KeysQueryRequest,
|
||||
session_manager::GroupSessionManager,
|
||||
store::{Changes, DeviceChanges, IdentityChanges, Result as StoreResult, Store},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct IdentityManager {
|
||||
user_id: Arc<UserId>,
|
||||
device_id: Arc<DeviceIdBox>,
|
||||
group_manager: GroupSessionManager,
|
||||
store: Store,
|
||||
}
|
||||
|
||||
impl IdentityManager {
|
||||
pub fn new(
|
||||
user_id: Arc<UserId>,
|
||||
device_id: Arc<DeviceIdBox>,
|
||||
store: Store,
|
||||
group_manager: GroupSessionManager,
|
||||
) -> Self {
|
||||
IdentityManager {
|
||||
user_id,
|
||||
device_id,
|
||||
store,
|
||||
group_manager,
|
||||
}
|
||||
}
|
||||
|
||||
fn user_id(&self) -> &UserId {
|
||||
&self.user_id
|
||||
}
|
||||
|
||||
fn device_id(&self) -> &DeviceId {
|
||||
&self.device_id
|
||||
}
|
||||
|
||||
/// Receive a successful keys query response.
|
||||
///
|
||||
/// Returns a list of devices newly discovered devices and devices that
|
||||
/// changed.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `response` - The keys query response of the request that the client
|
||||
/// performed.
|
||||
pub async fn receive_keys_query_response(
|
||||
&self,
|
||||
response: &KeysQueryResponse,
|
||||
) -> OlmResult<(DeviceChanges, IdentityChanges)> {
|
||||
// TODO create a enum that tells us how the device/identity changed,
|
||||
// e.g. new/deleted/display name change.
|
||||
//
|
||||
// TODO create a struct that will hold the device/identity and the
|
||||
// change enum and return the struct.
|
||||
//
|
||||
// TODO once outbound group sessions hold on to the set of users that
|
||||
// received the session, invalidate the session if a user device
|
||||
// got added/deleted.
|
||||
let changed_devices = self
|
||||
.handle_devices_from_key_query(&response.device_keys)
|
||||
.await?;
|
||||
let changed_identities = self.handle_cross_singing_keys(response).await?;
|
||||
|
||||
let changes = Changes {
|
||||
identities: changed_identities.clone(),
|
||||
devices: changed_devices.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
self.store.save_changes(changes).await?;
|
||||
|
||||
Ok((changed_devices, changed_identities))
|
||||
}
|
||||
|
||||
/// Handle the device keys part of a key query response.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `device_keys_map` - A map holding the device keys of the users for
|
||||
/// which the key query was done.
|
||||
///
|
||||
/// Returns a list of devices that changed. Changed here means either
|
||||
/// they are new, one of their properties has changed or they got deleted.
|
||||
async fn handle_devices_from_key_query(
|
||||
&self,
|
||||
device_keys_map: &BTreeMap<UserId, BTreeMap<DeviceIdBox, DeviceKeys>>,
|
||||
) -> StoreResult<DeviceChanges> {
|
||||
let mut users_with_new_or_deleted_devices = HashSet::new();
|
||||
|
||||
let mut changes = DeviceChanges::default();
|
||||
|
||||
for (user_id, device_map) in device_keys_map {
|
||||
// TODO move this out into the handle keys query response method
|
||||
// since we might fail handle the new device at any point here or
|
||||
// when updating the user identities.
|
||||
self.store.update_tracked_user(user_id, false).await?;
|
||||
|
||||
for (device_id, device_keys) in device_map.iter() {
|
||||
// We don't need our own device in the device store.
|
||||
if user_id == self.user_id() && &**device_id == self.device_id() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if user_id != &device_keys.user_id || device_id != &device_keys.device_id {
|
||||
warn!(
|
||||
"Mismatch in device keys payload of device {}|{} from user {}|{}",
|
||||
device_id, device_keys.device_id, user_id, device_keys.user_id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let device = self.store.get_readonly_device(&user_id, device_id).await?;
|
||||
|
||||
if let Some(mut device) = device {
|
||||
if let Err(e) = device.update_device(device_keys) {
|
||||
warn!(
|
||||
"Failed to update the device keys for {} {}: {:?}",
|
||||
user_id, device_id, e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
changes.changed.push(device);
|
||||
} else {
|
||||
let device = match ReadOnlyDevice::try_from(device_keys) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to create a new device for {} {}: {:?}",
|
||||
user_id, device_id, e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
info!("Adding a new device to the device store {:?}", device);
|
||||
users_with_new_or_deleted_devices.insert(user_id);
|
||||
changes.new.push(device);
|
||||
}
|
||||
}
|
||||
|
||||
let current_devices: HashSet<&DeviceIdBox> = device_map.keys().collect();
|
||||
let stored_devices = self.store.get_readonly_devices(&user_id).await?;
|
||||
let stored_devices_set: HashSet<&DeviceIdBox> = stored_devices.keys().collect();
|
||||
|
||||
let deleted_devices_set = stored_devices_set.difference(¤t_devices);
|
||||
|
||||
for device_id in deleted_devices_set {
|
||||
users_with_new_or_deleted_devices.insert(user_id);
|
||||
if let Some(device) = stored_devices.get(*device_id) {
|
||||
device.mark_as_deleted();
|
||||
changes.deleted.push(device.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.group_manager
|
||||
.invalidate_sessions_new_devices(&users_with_new_or_deleted_devices);
|
||||
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
/// Handle the device keys part of a key query response.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `response` - The keys query response.
|
||||
///
|
||||
/// Returns a list of identities that changed. Changed here means either
|
||||
/// they are new, one of their properties has changed or they got deleted.
|
||||
async fn handle_cross_singing_keys(
|
||||
&self,
|
||||
response: &KeysQueryResponse,
|
||||
) -> StoreResult<IdentityChanges> {
|
||||
let mut changes = IdentityChanges::default();
|
||||
|
||||
for (user_id, master_key) in &response.master_keys {
|
||||
let master_key = MasterPubkey::from(master_key);
|
||||
|
||||
let self_signing = if let Some(s) = response.self_signing_keys.get(user_id) {
|
||||
SelfSigningPubkey::from(s)
|
||||
} else {
|
||||
warn!(
|
||||
"User identity for user {} didn't contain a self signing pubkey",
|
||||
user_id
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
let result = if let Some(mut i) = self.store.get_user_identity(user_id).await? {
|
||||
match &mut i {
|
||||
UserIdentities::Own(ref mut identity) => {
|
||||
let user_signing = if let Some(s) = response.user_signing_keys.get(user_id)
|
||||
{
|
||||
UserSigningPubkey::from(s)
|
||||
} else {
|
||||
warn!(
|
||||
"User identity for our own user {} didn't \
|
||||
contain a user signing pubkey",
|
||||
user_id
|
||||
);
|
||||
continue;
|
||||
};
|
||||
|
||||
identity
|
||||
.update(master_key, self_signing, user_signing)
|
||||
.map(|_| (i, false))
|
||||
}
|
||||
UserIdentities::Other(ref mut identity) => identity
|
||||
.update(master_key, self_signing)
|
||||
.map(|_| (i, false)),
|
||||
}
|
||||
} else if user_id == self.user_id() {
|
||||
if let Some(s) = response.user_signing_keys.get(user_id) {
|
||||
let user_signing = UserSigningPubkey::from(s);
|
||||
|
||||
if master_key.user_id() != user_id
|
||||
|| self_signing.user_id() != user_id
|
||||
|| user_signing.user_id() != user_id
|
||||
{
|
||||
warn!(
|
||||
"User id mismatch in one of the cross signing keys for user {}",
|
||||
user_id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
OwnUserIdentity::new(master_key, self_signing, user_signing)
|
||||
.map(|i| (UserIdentities::Own(i), true))
|
||||
} else {
|
||||
warn!(
|
||||
"User identity for our own user {} didn't contain a \
|
||||
user signing pubkey",
|
||||
user_id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
} else if master_key.user_id() != user_id || self_signing.user_id() != user_id {
|
||||
warn!(
|
||||
"User id mismatch in one of the cross signing keys for user {}",
|
||||
user_id
|
||||
);
|
||||
continue;
|
||||
} else {
|
||||
UserIdentity::new(master_key, self_signing)
|
||||
.map(|i| (UserIdentities::Other(i), true))
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok((i, new)) => {
|
||||
trace!(
|
||||
"Updated or created new user identity for {}: {:?}",
|
||||
user_id,
|
||||
i
|
||||
);
|
||||
if new {
|
||||
changes.new.push(i);
|
||||
} else {
|
||||
changes.changed.push(i);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Couldn't update or create new user identity for {}: {:?}",
|
||||
user_id, e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changes)
|
||||
}
|
||||
|
||||
/// Get a key query request if one is needed.
|
||||
///
|
||||
/// Returns a key query reqeust if the client should query E2E keys,
|
||||
/// otherwise None.
|
||||
///
|
||||
/// The response of a successful key query requests needs to be passed to
|
||||
/// the [`OlmMachine`] with the [`receive_keys_query_response`].
|
||||
///
|
||||
/// [`OlmMachine`]: struct.OlmMachine.html
|
||||
/// [`receive_keys_query_response`]: #method.receive_keys_query_response
|
||||
pub async fn users_for_key_query(&self) -> Option<KeysQueryRequest> {
|
||||
let mut users = self.store.users_for_key_query();
|
||||
|
||||
if users.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let mut device_keys: BTreeMap<UserId, Vec<Box<DeviceId>>> = BTreeMap::new();
|
||||
|
||||
for user in users.drain() {
|
||||
device_keys.insert(user, Vec::new());
|
||||
}
|
||||
|
||||
Some(KeysQueryRequest::new(device_keys))
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark that the given user has changed his devices.
|
||||
///
|
||||
/// This will queue up the given user for a key query.
|
||||
///
|
||||
/// Note: The user already needs to be tracked for it to be queued up for a
|
||||
/// key query.
|
||||
///
|
||||
/// Returns true if the user was queued up for a key query, false otherwise.
|
||||
pub async fn mark_user_as_changed(&self, user_id: &UserId) -> StoreResult<bool> {
|
||||
if self.store.is_user_tracked(user_id) {
|
||||
self.store.update_tracked_user(user_id, true).await?;
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Update the tracked users.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `users` - An iterator over user ids that should be marked for
|
||||
/// tracking.
|
||||
///
|
||||
/// This will mark users that weren't seen before for a key query and
|
||||
/// tracking.
|
||||
///
|
||||
/// If the user is already known to the Olm machine it will not be
|
||||
/// considered for a key query.
|
||||
pub async fn update_tracked_users(&self, users: impl IntoIterator<Item = &UserId>) {
|
||||
for user in users {
|
||||
if self.store.is_user_tracked(user) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(e) = self.store.update_tracked_user(user, true).await {
|
||||
warn!("Error storing users for tracking {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use std::{convert::TryFrom, sync::Arc};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
api::r0::keys::get_keys::Response as KeyQueryResponse,
|
||||
identifiers::{room_id, user_id, DeviceIdBox, RoomId, UserId},
|
||||
locks::Mutex,
|
||||
};
|
||||
|
||||
use matrix_sdk_test::async_test;
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{
|
||||
identities::IdentityManager,
|
||||
machine::test::response_from_file,
|
||||
olm::{Account, PrivateCrossSigningIdentity, ReadOnlyAccount},
|
||||
session_manager::GroupSessionManager,
|
||||
store::{CryptoStore, MemoryStore, Store},
|
||||
verification::VerificationMachine,
|
||||
};
|
||||
|
||||
fn user_id() -> UserId {
|
||||
user_id!("@example:localhost")
|
||||
}
|
||||
|
||||
fn other_user_id() -> UserId {
|
||||
user_id!("@example2:localhost")
|
||||
}
|
||||
|
||||
fn device_id() -> DeviceIdBox {
|
||||
"WSKKLTJZCL".into()
|
||||
}
|
||||
|
||||
fn room_id() -> RoomId {
|
||||
room_id!("!test:localhost")
|
||||
}
|
||||
|
||||
fn manager() -> IdentityManager {
|
||||
let identity = Arc::new(Mutex::new(PrivateCrossSigningIdentity::empty(user_id())));
|
||||
let user_id = Arc::new(user_id());
|
||||
let account = ReadOnlyAccount::new(&user_id, &device_id());
|
||||
let store: Arc<Box<dyn CryptoStore>> = Arc::new(Box::new(MemoryStore::new()));
|
||||
let verification = VerificationMachine::new(account.clone(), identity.clone(), store);
|
||||
let store = Store::new(
|
||||
user_id.clone(),
|
||||
identity,
|
||||
Arc::new(Box::new(MemoryStore::new())),
|
||||
verification,
|
||||
);
|
||||
let account = Account {
|
||||
inner: account,
|
||||
store: store.clone(),
|
||||
};
|
||||
let group = GroupSessionManager::new(account, store.clone());
|
||||
IdentityManager::new(user_id, Arc::new(device_id()), store, group)
|
||||
}
|
||||
|
||||
pub(crate) fn other_key_query() -> KeyQueryResponse {
|
||||
let data = response_from_file(&json!({
|
||||
"device_keys": {
|
||||
"@example2:localhost": {
|
||||
"SKISMLNIMH": {
|
||||
"algorithms": ["m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"],
|
||||
"device_id": "SKISMLNIMH",
|
||||
"keys": {
|
||||
"curve25519:SKISMLNIMH": "qO9xFazIcW8dE0oqHGMojGgJwbBpMOhGnIfJy2pzvmI",
|
||||
"ed25519:SKISMLNIMH": "y3wV3AoyIGREqrJJVH8DkQtlwHBUxoZ9ApP76kFgXQ8"
|
||||
},
|
||||
"signatures": {
|
||||
"@example2:localhost": {
|
||||
"ed25519:SKISMLNIMH": "YwbT35rbjKoYFZVU1tQP8MsL06+znVNhNzUMPt6jTEYRBFoC4GDq9hQEJBiFSq37r1jvLMteggVAWw37fs1yBA",
|
||||
"ed25519:ZtFrSkJ1qB8Jph/ql9Eo/lKpIYCzwvKAKXfkaS4XZNc": "PWuuTE/aTkp1EJQkPHhRx2BxbF+wjMIDFxDRp7JAerlMkDsNFUTfRRusl6vqROPU36cl+yY8oeJTZGFkU6+pBQ"
|
||||
}
|
||||
},
|
||||
"user_id": "@example2:localhost",
|
||||
"unsigned": {
|
||||
"device_display_name": "Riot Desktop (Linux)"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"failures": {},
|
||||
"master_keys": {
|
||||
"@example2:localhost": {
|
||||
"user_id": "@example2:localhost",
|
||||
"usage": ["master"],
|
||||
"keys": {
|
||||
"ed25519:kC/HmRYw4HNqUp/i4BkwYENrf+hd9tvdB7A1YOf5+Do": "kC/HmRYw4HNqUp/i4BkwYENrf+hd9tvdB7A1YOf5+Do"
|
||||
},
|
||||
"signatures": {
|
||||
"@example2:localhost": {
|
||||
"ed25519:SKISMLNIMH": "KdUZqzt8VScGNtufuQ8lOf25byYLWIhmUYpPENdmM8nsldexD7vj+Sxoo7PknnTX/BL9h2N7uBq0JuykjunCAw"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"self_signing_keys": {
|
||||
"@example2:localhost": {
|
||||
"user_id": "@example2:localhost",
|
||||
"usage": ["self_signing"],
|
||||
"keys": {
|
||||
"ed25519:ZtFrSkJ1qB8Jph/ql9Eo/lKpIYCzwvKAKXfkaS4XZNc": "ZtFrSkJ1qB8Jph/ql9Eo/lKpIYCzwvKAKXfkaS4XZNc"
|
||||
},
|
||||
"signatures": {
|
||||
"@example2:localhost": {
|
||||
"ed25519:kC/HmRYw4HNqUp/i4BkwYENrf+hd9tvdB7A1YOf5+Do": "W/O8BnmiUETPpH02mwYaBgvvgF/atXnusmpSTJZeUSH/vHg66xiZOhveQDG4cwaW8iMa+t9N4h1DWnRoHB4mCQ"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_signing_keys": {}
|
||||
}));
|
||||
KeyQueryResponse::try_from(data).expect("Can't parse the keys upload response")
|
||||
}
|
||||
|
||||
pub(crate) fn own_key_query() -> KeyQueryResponse {
|
||||
let data = response_from_file(&json!({
|
||||
"device_keys": {
|
||||
"@example:localhost": {
|
||||
"WSKKLTJZCL": {
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2"
|
||||
],
|
||||
"device_id": "WSKKLTJZCL",
|
||||
"keys": {
|
||||
"curve25519:WSKKLTJZCL": "wnip2tbJBJxrFayC88NNJpm61TeSNgYcqBH4T9yEDhU",
|
||||
"ed25519:WSKKLTJZCL": "lQ+eshkhgKoo+qp9Qgnj3OX5PBoWMU5M9zbuEevwYqE"
|
||||
},
|
||||
"signatures": {
|
||||
"@example:localhost": {
|
||||
"ed25519:WSKKLTJZCL": "SKpIUnq7QK0xleav0PrIQyKjVm+TgZr7Yi8cKjLeZDtkgyToE2d4/e3Aj79dqOlLB92jFVE4d1cM/Ry04wFwCA",
|
||||
"ed25519:0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210": "9UGu1iC5YhFCdELGfB29YaV+QE0t/X5UDSsPf4QcdZyXIwyp9zBbHX2lh9vWudNQ+akZpaq7ZRaaM+4TCnw/Ag"
|
||||
}
|
||||
},
|
||||
"user_id": "@example:localhost",
|
||||
"unsigned": {
|
||||
"device_display_name": "Cross signing capable"
|
||||
}
|
||||
},
|
||||
"LVWOVGOXME": {
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2"
|
||||
],
|
||||
"device_id": "LVWOVGOXME",
|
||||
"keys": {
|
||||
"curve25519:LVWOVGOXME": "KMfWKUhnDW1D11hNzATs/Ax1FQRsJxKCWzq0NyGtIiI",
|
||||
"ed25519:LVWOVGOXME": "k+NC3L7CBD6fBClcHBrKLOkqCyGNSKhWXiH5Q2STRnA"
|
||||
},
|
||||
"signatures": {
|
||||
"@example:localhost": {
|
||||
"ed25519:LVWOVGOXME": "39Ir5Bttpc5+bQwzLj7rkjm5E5/cp/JTbMJ/t0enj6J5w9MXVBFOUqqM2hpaRaRwILMMpwYbJ8IOGjl0Y/MGAw"
|
||||
}
|
||||
},
|
||||
"user_id": "@example:localhost",
|
||||
"unsigned": {
|
||||
"device_display_name": "Non-cross signing"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"failures": {},
|
||||
"master_keys": {
|
||||
"@example:localhost": {
|
||||
"user_id": "@example:localhost",
|
||||
"usage": [
|
||||
"master"
|
||||
],
|
||||
"keys": {
|
||||
"ed25519:rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0": "rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0"
|
||||
},
|
||||
"signatures": {
|
||||
"@example:localhost": {
|
||||
"ed25519:WSKKLTJZCL": "ZzJp1wtmRdykXAUEItEjNiFlBrxx8L6/Vaen9am8AuGwlxxJtOkuY4m+4MPLvDPOgavKHLsrRuNLAfCeakMlCQ"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"self_signing_keys": {
|
||||
"@example:localhost": {
|
||||
"user_id": "@example:localhost",
|
||||
"usage": [
|
||||
"self_signing"
|
||||
],
|
||||
"keys": {
|
||||
"ed25519:0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210": "0C8lCBxrvrv/O7BQfsKnkYogHZX3zAgw3RfJuyiq210"
|
||||
},
|
||||
"signatures": {
|
||||
"@example:localhost": {
|
||||
"ed25519:rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0": "AC7oDUW4rUhtInwb4lAoBJ0wAuu4a5k+8e34B5+NKsDB8HXRwgVwUWN/MRWc/sJgtSbVlhzqS9THEmQQ1C51Bw"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"user_signing_keys": {
|
||||
"@example:localhost": {
|
||||
"user_id": "@example:localhost",
|
||||
"usage": [
|
||||
"user_signing"
|
||||
],
|
||||
"keys": {
|
||||
"ed25519:DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo": "DU9z4gBFKFKCk7a13sW9wjT0Iyg7Hqv5f0BPM7DEhPo"
|
||||
},
|
||||
"signatures": {
|
||||
"@example:localhost": {
|
||||
"ed25519:rJ2TAGkEOP6dX41Ksll6cl8K3J48l8s/59zaXyvl2p0": "C4L2sx9frGqj8w41KyynHGqwUbbwBYRZpYCB+6QWnvQFA5Oi/1PJj8w5anwzEsoO0TWmLYmf7FXuAGewanOWDg"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
KeyQueryResponse::try_from(data).expect("Can't parse the keys upload response")
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_manager_creation() {
|
||||
let manager = manager();
|
||||
assert!(manager.users_for_key_query().await.is_none())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_manager_key_query_response() {
|
||||
let manager = manager();
|
||||
let other_user = other_user_id();
|
||||
let devices = manager.store.get_user_devices(&other_user).await.unwrap();
|
||||
assert_eq!(devices.devices().count(), 0);
|
||||
|
||||
manager
|
||||
.receive_keys_query_response(&other_key_query())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let devices = manager.store.get_user_devices(&other_user).await.unwrap();
|
||||
assert_eq!(devices.devices().count(), 1);
|
||||
|
||||
let device = manager
|
||||
.store
|
||||
.get_readonly_device(&other_user, "SKISMLNIMH".into())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let identity = manager
|
||||
.store
|
||||
.get_user_identity(&other_user)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let identity = identity.other().unwrap();
|
||||
|
||||
assert!(identity.is_device_signed(&device).is_ok())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_manager_own_key_query_response() {
|
||||
let manager = manager();
|
||||
let other_user = other_user_id();
|
||||
let devices = manager.store.get_user_devices(&other_user).await.unwrap();
|
||||
assert_eq!(devices.devices().count(), 0);
|
||||
|
||||
manager
|
||||
.receive_keys_query_response(&other_key_query())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let devices = manager.store.get_user_devices(&other_user).await.unwrap();
|
||||
assert_eq!(devices.devices().count(), 1);
|
||||
|
||||
let device = manager
|
||||
.store
|
||||
.get_readonly_device(&other_user, "SKISMLNIMH".into())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let identity = manager
|
||||
.store
|
||||
.get_user_identity(&other_user)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let identity = identity.other().unwrap();
|
||||
|
||||
assert!(identity.is_device_signed(&device).is_ok())
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn test_session_invalidation() {
|
||||
let manager = manager();
|
||||
let room_id = room_id();
|
||||
let user_id = other_user_id();
|
||||
let device_id: DeviceIdBox = "SKISMLNIMH".into();
|
||||
|
||||
manager
|
||||
.group_manager
|
||||
.create_outbound_group_session(&room_id, Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let session = manager
|
||||
.group_manager
|
||||
.get_outbound_group_session(&room_id)
|
||||
.unwrap();
|
||||
|
||||
session.add_recipient(&user_id);
|
||||
session.mark_as_shared();
|
||||
|
||||
assert!(!session.invalidated());
|
||||
assert!(!session.expired());
|
||||
|
||||
// Receiving a new device invalidates the session.
|
||||
manager
|
||||
.receive_keys_query_response(&other_key_query())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(session.invalidated());
|
||||
|
||||
manager
|
||||
.group_manager
|
||||
.create_outbound_group_session(&room_id, Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
let session = manager
|
||||
.group_manager
|
||||
.get_outbound_group_session(&room_id)
|
||||
.unwrap();
|
||||
|
||||
session.add_recipient(&user_id);
|
||||
session.mark_as_shared();
|
||||
|
||||
assert!(!session.invalidated());
|
||||
assert!(!session.expired());
|
||||
|
||||
let device = manager
|
||||
.store
|
||||
.get_device(&user_id, &device_id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(!device.deleted());
|
||||
|
||||
let response = KeyQueryResponse::try_from(response_from_file(&json!({
|
||||
"device_keys": {
|
||||
user_id: {}
|
||||
},
|
||||
"failures": {},
|
||||
})))
|
||||
.unwrap();
|
||||
|
||||
// Noticing that a device got deleted invalidates the session as well
|
||||
manager
|
||||
.receive_keys_query_response(&response)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(device.deleted());
|
||||
assert!(session.invalidated());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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.
|
||||
|
||||
//! Collection of public identities used in Matrix.
|
||||
//!
|
||||
//! Matrix supports two main types of identities, a per-device identity and a
|
||||
//! per-user identity.
|
||||
//!
|
||||
//! ## Device
|
||||
//!
|
||||
//! Every E2EE capable Matrix client will create a new Olm account and upload
|
||||
//! the public keys of the Olm account to the server. This is represented as a
|
||||
//! `ReadOnlyDevice`.
|
||||
//!
|
||||
//! Devices can have a local trust state which is needs to be saved in our
|
||||
//! `CryptoStore`, to avoid reference cycles a wrapper for the `ReadOnlyDevice`
|
||||
//! exists which adds methods to manipulate the local trust state.
|
||||
//!
|
||||
//! ## User
|
||||
//!
|
||||
//! Cross-signing capable devices will upload 3 additional (master, self-signing,
|
||||
//! user-signing) public keys which represent the user identity owning all the
|
||||
//! devices. This is represented in two ways, as a `UserIdentity` for other
|
||||
//! users and as `OwnUserIdentity` for our own user.
|
||||
//!
|
||||
//! This is done because the server will only give us access to 2 of the 3
|
||||
//! additional public keys for other users, while it will give us access to all
|
||||
//! 3 for our own user.
|
||||
//!
|
||||
//! Both identity sets need to reqularly fetched from the server using the
|
||||
//! `/keys/query` API call.
|
||||
pub(crate) mod device;
|
||||
mod manager;
|
||||
pub(crate) mod user;
|
||||
|
||||
pub use device::{Device, LocalTrust, ReadOnlyDevice, UserDevices};
|
||||
pub(crate) use manager::IdentityManager;
|
||||
pub use user::{
|
||||
MasterPubkey, OwnUserIdentity, SelfSigningPubkey, UserIdentities, UserIdentity,
|
||||
UserSigningPubkey,
|
||||
};
|
||||
@@ -0,0 +1,857 @@
|
||||
// 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.
|
||||
|
||||
use std::{
|
||||
collections::{btree_map::Iter, BTreeMap},
|
||||
convert::TryFrom,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::to_value;
|
||||
|
||||
use matrix_sdk_common::{
|
||||
api::r0::keys::{CrossSigningKey, KeyUsage},
|
||||
identifiers::{DeviceKeyId, UserId},
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
use crate::olm::PrivateCrossSigningIdentity;
|
||||
use crate::{error::SignatureError, olm::Utility, ReadOnlyDevice};
|
||||
|
||||
/// Wrapper for a cross signing key marking it as the master key.
|
||||
///
|
||||
/// Master keys are used to sign other cross signing keys, the self signing and
|
||||
/// user signing keys of an user will be signed by their master key.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct MasterPubkey(Arc<CrossSigningKey>);
|
||||
|
||||
/// Wrapper for a cross signing key marking it as a self signing key.
|
||||
///
|
||||
/// Self signing keys are used to sign the user's own devices.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SelfSigningPubkey(Arc<CrossSigningKey>);
|
||||
|
||||
/// Wrapper for a cross signing key marking it as a user signing key.
|
||||
///
|
||||
/// User signing keys are used to sign the master keys of other users.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserSigningPubkey(Arc<CrossSigningKey>);
|
||||
|
||||
impl PartialEq for MasterPubkey {
|
||||
fn eq(&self, other: &MasterPubkey) -> bool {
|
||||
self.0.user_id == other.0.user_id && self.0.keys == other.0.keys
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for SelfSigningPubkey {
|
||||
fn eq(&self, other: &SelfSigningPubkey) -> bool {
|
||||
self.0.user_id == other.0.user_id && self.0.keys == other.0.keys
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for UserSigningPubkey {
|
||||
fn eq(&self, other: &UserSigningPubkey) -> bool {
|
||||
self.0.user_id == other.0.user_id && self.0.keys == other.0.keys
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CrossSigningKey> for MasterPubkey {
|
||||
fn from(key: CrossSigningKey) -> Self {
|
||||
Self(Arc::new(key))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CrossSigningKey> for SelfSigningPubkey {
|
||||
fn from(key: CrossSigningKey) -> Self {
|
||||
Self(Arc::new(key))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CrossSigningKey> for UserSigningPubkey {
|
||||
fn from(key: CrossSigningKey) -> Self {
|
||||
Self(Arc::new(key))
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<CrossSigningKey> for MasterPubkey {
|
||||
fn into(self) -> CrossSigningKey {
|
||||
self.0.as_ref().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<CrossSigningKey> for UserSigningPubkey {
|
||||
fn into(self) -> CrossSigningKey {
|
||||
self.0.as_ref().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<CrossSigningKey> for SelfSigningPubkey {
|
||||
fn into(self) -> CrossSigningKey {
|
||||
self.0.as_ref().clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<CrossSigningKey> for MasterPubkey {
|
||||
fn as_ref(&self) -> &CrossSigningKey {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<CrossSigningKey> for SelfSigningPubkey {
|
||||
fn as_ref(&self) -> &CrossSigningKey {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<CrossSigningKey> for UserSigningPubkey {
|
||||
fn as_ref(&self) -> &CrossSigningKey {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CrossSigningKey> for MasterPubkey {
|
||||
fn from(key: &CrossSigningKey) -> Self {
|
||||
Self(Arc::new(key.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CrossSigningKey> for SelfSigningPubkey {
|
||||
fn from(key: &CrossSigningKey) -> Self {
|
||||
Self(Arc::new(key.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&CrossSigningKey> for UserSigningPubkey {
|
||||
fn from(key: &CrossSigningKey) -> Self {
|
||||
Self(Arc::new(key.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a SelfSigningPubkey> for CrossSigningSubKeys<'a> {
|
||||
fn from(key: &'a SelfSigningPubkey) -> Self {
|
||||
CrossSigningSubKeys::SelfSigning(key)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a UserSigningPubkey> for CrossSigningSubKeys<'a> {
|
||||
fn from(key: &'a UserSigningPubkey) -> Self {
|
||||
CrossSigningSubKeys::UserSigning(key)
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum over the cross signing sub-keys.
|
||||
pub(crate) enum CrossSigningSubKeys<'a> {
|
||||
/// The self signing subkey.
|
||||
SelfSigning(&'a SelfSigningPubkey),
|
||||
/// The user signing subkey.
|
||||
UserSigning(&'a UserSigningPubkey),
|
||||
}
|
||||
|
||||
impl<'a> CrossSigningSubKeys<'a> {
|
||||
/// Get the id of the user that owns this cross signing subkey.
|
||||
fn user_id(&self) -> &UserId {
|
||||
match self {
|
||||
CrossSigningSubKeys::SelfSigning(key) => &key.0.user_id,
|
||||
CrossSigningSubKeys::UserSigning(key) => &key.0.user_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the `CrossSigningKey` from an sub-keys enum
|
||||
pub(crate) fn cross_signing_key(&self) -> &CrossSigningKey {
|
||||
match self {
|
||||
CrossSigningSubKeys::SelfSigning(key) => &key.0,
|
||||
CrossSigningSubKeys::UserSigning(key) => &key.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MasterPubkey {
|
||||
/// Get the user id of the master key's owner.
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
&self.0.user_id
|
||||
}
|
||||
|
||||
/// Get the keys map of containing the master keys.
|
||||
pub fn keys(&self) -> &BTreeMap<String, String> {
|
||||
&self.0.keys
|
||||
}
|
||||
|
||||
/// Get the list of `KeyUsage` that is set for this key.
|
||||
pub fn usage(&self) -> &[KeyUsage] {
|
||||
&self.0.usage
|
||||
}
|
||||
|
||||
/// Get the signatures map of this cross signing key.
|
||||
pub fn signatures(&self) -> &BTreeMap<UserId, BTreeMap<String, String>> {
|
||||
&self.0.signatures
|
||||
}
|
||||
|
||||
/// Get the master key with the given key id.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `key_id` - The id of the key that should be fetched.
|
||||
pub fn get_key(&self, key_id: &DeviceKeyId) -> Option<&str> {
|
||||
self.0.keys.get(key_id.as_str()).map(|k| k.as_str())
|
||||
}
|
||||
|
||||
/// Check if the given cross signing sub-key is signed by the master key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `subkey` - The subkey that should be checked for a valid signature.
|
||||
///
|
||||
/// Returns an empty result if the signature check succeeded, otherwise a
|
||||
/// SignatureError indicating why the check failed.
|
||||
pub(crate) fn verify_subkey<'a>(
|
||||
&self,
|
||||
subkey: impl Into<CrossSigningSubKeys<'a>>,
|
||||
) -> Result<(), SignatureError> {
|
||||
let (key_id, key) = self
|
||||
.0
|
||||
.keys
|
||||
.iter()
|
||||
.next()
|
||||
.ok_or(SignatureError::MissingSigningKey)?;
|
||||
|
||||
let key_id = DeviceKeyId::try_from(key_id.as_str())?;
|
||||
|
||||
// FIXME `KeyUsage is missing PartialEq.
|
||||
// if self.0.usage.contains(&KeyUsage::Master) {
|
||||
// return Err(SignatureError::MissingSigningKey);
|
||||
// }
|
||||
let subkey: CrossSigningSubKeys = subkey.into();
|
||||
|
||||
if &self.0.user_id != subkey.user_id() {
|
||||
return Err(SignatureError::UserIdMissmatch);
|
||||
}
|
||||
|
||||
let utility = Utility::new();
|
||||
utility.verify_json(
|
||||
&self.0.user_id,
|
||||
&key_id,
|
||||
key,
|
||||
&mut to_value(subkey.cross_signing_key()).map_err(|_| SignatureError::NotAnObject)?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a MasterPubkey {
|
||||
type Item = (&'a String, &'a String);
|
||||
type IntoIter = Iter<'a, String, String>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.keys().iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl UserSigningPubkey {
|
||||
/// Get the user id of the user signing key's owner.
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
&self.0.user_id
|
||||
}
|
||||
|
||||
/// Get the keys map of containing the user signing keys.
|
||||
pub fn keys(&self) -> &BTreeMap<String, String> {
|
||||
&self.0.keys
|
||||
}
|
||||
|
||||
/// Check if the given master key is signed by this user signing key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `master_key` - The master key that should be checked for a valid
|
||||
/// signature.
|
||||
///
|
||||
/// Returns an empty result if the signature check succeeded, otherwise a
|
||||
/// SignatureError indicating why the check failed.
|
||||
pub(crate) fn verify_master_key(
|
||||
&self,
|
||||
master_key: &MasterPubkey,
|
||||
) -> Result<(), SignatureError> {
|
||||
let (key_id, key) = self
|
||||
.0
|
||||
.keys
|
||||
.iter()
|
||||
.next()
|
||||
.ok_or(SignatureError::MissingSigningKey)?;
|
||||
|
||||
// TODO check that the usage is OK.
|
||||
|
||||
let utility = Utility::new();
|
||||
utility.verify_json(
|
||||
&self.0.user_id,
|
||||
&DeviceKeyId::try_from(key_id.as_str())?,
|
||||
key,
|
||||
&mut to_value(&*master_key.0).map_err(|_| SignatureError::NotAnObject)?,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a UserSigningPubkey {
|
||||
type Item = (&'a String, &'a String);
|
||||
type IntoIter = Iter<'a, String, String>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.keys().iter()
|
||||
}
|
||||
}
|
||||
|
||||
impl SelfSigningPubkey {
|
||||
/// Get the user id of the self signing key's owner.
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
&self.0.user_id
|
||||
}
|
||||
|
||||
/// Get the keys map of containing the self signing keys.
|
||||
pub fn keys(&self) -> &BTreeMap<String, String> {
|
||||
&self.0.keys
|
||||
}
|
||||
|
||||
/// Check if the given device is signed by this self signing key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `device` - The device that should be checked for a valid signature.
|
||||
///
|
||||
/// Returns an empty result if the signature check succeeded, otherwise a
|
||||
/// SignatureError indicating why the check failed.
|
||||
pub(crate) fn verify_device(&self, device: &ReadOnlyDevice) -> Result<(), SignatureError> {
|
||||
let (key_id, key) = self
|
||||
.0
|
||||
.keys
|
||||
.iter()
|
||||
.next()
|
||||
.ok_or(SignatureError::MissingSigningKey)?;
|
||||
|
||||
// TODO check that the usage is OK.
|
||||
|
||||
let utility = Utility::new();
|
||||
utility.verify_json(
|
||||
&self.0.user_id,
|
||||
&DeviceKeyId::try_from(key_id.as_str())?,
|
||||
key,
|
||||
&mut device.as_signature_message(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a SelfSigningPubkey {
|
||||
type Item = (&'a String, &'a String);
|
||||
type IntoIter = Iter<'a, String, String>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.keys().iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum over the different user identity types we can have.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum UserIdentities {
|
||||
/// Our own user identity.
|
||||
Own(OwnUserIdentity),
|
||||
/// Identities of other users.
|
||||
Other(UserIdentity),
|
||||
}
|
||||
|
||||
impl From<OwnUserIdentity> for UserIdentities {
|
||||
fn from(identity: OwnUserIdentity) -> Self {
|
||||
UserIdentities::Own(identity)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<UserIdentity> for UserIdentities {
|
||||
fn from(identity: UserIdentity) -> Self {
|
||||
UserIdentities::Other(identity)
|
||||
}
|
||||
}
|
||||
|
||||
impl UserIdentities {
|
||||
/// The unique user id of this identity.
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
match self {
|
||||
UserIdentities::Own(i) => i.user_id(),
|
||||
UserIdentities::Other(i) => i.user_id(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the master key of the identity.
|
||||
pub fn master_key(&self) -> &MasterPubkey {
|
||||
match self {
|
||||
UserIdentities::Own(i) => i.master_key(),
|
||||
UserIdentities::Other(i) => i.master_key(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the self-signing key of the identity.
|
||||
pub fn self_signing_key(&self) -> &SelfSigningPubkey {
|
||||
match self {
|
||||
UserIdentities::Own(i) => &i.self_signing_key,
|
||||
UserIdentities::Other(i) => &i.self_signing_key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the user-signing key of the identity, this is only present for our
|
||||
/// own user identity..
|
||||
pub fn user_signing_key(&self) -> Option<&UserSigningPubkey> {
|
||||
match self {
|
||||
UserIdentities::Own(i) => Some(&i.user_signing_key),
|
||||
UserIdentities::Other(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Destructure the enum into an `OwnUserIdentity` if it's of the correct
|
||||
/// type.
|
||||
pub fn own(&self) -> Option<&OwnUserIdentity> {
|
||||
match self {
|
||||
UserIdentities::Own(i) => Some(i),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Destructure the enum into an `UserIdentity` if it's of the correct
|
||||
/// type.
|
||||
pub fn other(&self) -> Option<&UserIdentity> {
|
||||
match self {
|
||||
UserIdentities::Other(i) => Some(i),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for UserIdentities {
|
||||
fn eq(&self, other: &UserIdentities) -> bool {
|
||||
self.user_id() == other.user_id()
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct representing a cross signing identity of a user.
|
||||
///
|
||||
/// This is the user identity of a user that isn't our own. Other users will
|
||||
/// only contain a master key and a self signing key, meaning that only device
|
||||
/// signatures can be checked with this identity.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct UserIdentity {
|
||||
user_id: Arc<UserId>,
|
||||
pub(crate) master_key: MasterPubkey,
|
||||
self_signing_key: SelfSigningPubkey,
|
||||
}
|
||||
|
||||
impl UserIdentity {
|
||||
/// Create a new user identity with the given master and self signing key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `master_key` - The master key of the user identity.
|
||||
///
|
||||
/// * `self signing key` - The self signing key of user identity.
|
||||
///
|
||||
/// Returns a `SignatureError` if the self signing key fails to be correctly
|
||||
/// verified by the given master key.
|
||||
pub fn new(
|
||||
master_key: MasterPubkey,
|
||||
self_signing_key: SelfSigningPubkey,
|
||||
) -> Result<Self, SignatureError> {
|
||||
master_key.verify_subkey(&self_signing_key)?;
|
||||
|
||||
Ok(Self {
|
||||
user_id: Arc::new(master_key.0.user_id.clone()),
|
||||
master_key,
|
||||
self_signing_key,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn from_private(identity: &PrivateCrossSigningIdentity) -> Self {
|
||||
let master_key = identity
|
||||
.master_key
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.public_key
|
||||
.clone();
|
||||
let self_signing_key = identity
|
||||
.self_signing_key
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.public_key
|
||||
.clone();
|
||||
|
||||
Self {
|
||||
user_id: Arc::new(identity.user_id().clone()),
|
||||
master_key,
|
||||
self_signing_key,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the user id of this identity.
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
&self.user_id
|
||||
}
|
||||
|
||||
/// Get the public master key of the identity.
|
||||
pub fn master_key(&self) -> &MasterPubkey {
|
||||
&self.master_key
|
||||
}
|
||||
|
||||
/// Get the public self-signing key of the identity.
|
||||
pub fn self_signing_key(&self) -> &SelfSigningPubkey {
|
||||
&self.self_signing_key
|
||||
}
|
||||
|
||||
/// Update the identity with a new master key and self signing key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `master_key` - The new master key of the user identity.
|
||||
///
|
||||
/// * `self_signing_key` - The new self signing key of user identity.
|
||||
///
|
||||
/// Returns a `SignatureError` if we failed to update the identity.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
master_key: MasterPubkey,
|
||||
self_signing_key: SelfSigningPubkey,
|
||||
) -> Result<(), SignatureError> {
|
||||
master_key.verify_subkey(&self_signing_key)?;
|
||||
|
||||
self.master_key = master_key;
|
||||
self.self_signing_key = self_signing_key;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the given device has been signed by this identity.
|
||||
///
|
||||
/// The user_id of the user identity and the user_id of the device need to
|
||||
/// match for the signature check to succeed as we don't trust users to sign
|
||||
/// devices of other users.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `device` - The device that should be checked for a valid signature.
|
||||
///
|
||||
/// Returns an empty result if the signature check succeeded, otherwise a
|
||||
/// SignatureError indicating why the check failed.
|
||||
pub fn is_device_signed(&self, device: &ReadOnlyDevice) -> Result<(), SignatureError> {
|
||||
if self.user_id() != device.user_id() {
|
||||
return Err(SignatureError::UserIdMissmatch);
|
||||
}
|
||||
|
||||
self.self_signing_key.verify_device(device)
|
||||
}
|
||||
}
|
||||
|
||||
/// Struct representing a cross signing identity of our own user.
|
||||
///
|
||||
/// This is the user identity of our own user. This user identity will contain a
|
||||
/// master key, self signing key as well as a user signing key.
|
||||
///
|
||||
/// This identity can verify other identities as well as devices belonging to
|
||||
/// the identity.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OwnUserIdentity {
|
||||
user_id: Arc<UserId>,
|
||||
master_key: MasterPubkey,
|
||||
self_signing_key: SelfSigningPubkey,
|
||||
user_signing_key: UserSigningPubkey,
|
||||
verified: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl OwnUserIdentity {
|
||||
/// Create a new own user identity with the given master, self signing, and
|
||||
/// user signing key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `master_key` - The master key of the user identity.
|
||||
///
|
||||
/// * `self_signing_key` - The self signing key of user identity.
|
||||
///
|
||||
/// * `user_signing_key` - The user signing key of user identity.
|
||||
///
|
||||
/// Returns a `SignatureError` if the self signing key fails to be correctly
|
||||
/// verified by the given master key.
|
||||
pub fn new(
|
||||
master_key: MasterPubkey,
|
||||
self_signing_key: SelfSigningPubkey,
|
||||
user_signing_key: UserSigningPubkey,
|
||||
) -> Result<Self, SignatureError> {
|
||||
master_key.verify_subkey(&self_signing_key)?;
|
||||
master_key.verify_subkey(&user_signing_key)?;
|
||||
|
||||
Ok(Self {
|
||||
user_id: Arc::new(master_key.0.user_id.clone()),
|
||||
master_key,
|
||||
self_signing_key,
|
||||
user_signing_key,
|
||||
verified: Arc::new(AtomicBool::new(false)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the user id of this identity.
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
&self.user_id
|
||||
}
|
||||
|
||||
/// Get the public master key of the identity.
|
||||
pub fn master_key(&self) -> &MasterPubkey {
|
||||
&self.master_key
|
||||
}
|
||||
|
||||
/// Get the public self-signing key of the identity.
|
||||
pub fn self_signing_key(&self) -> &SelfSigningPubkey {
|
||||
&self.self_signing_key
|
||||
}
|
||||
|
||||
/// Get the public user-signing key of the identity.
|
||||
pub fn user_signing_key(&self) -> &UserSigningPubkey {
|
||||
&self.user_signing_key
|
||||
}
|
||||
|
||||
/// Check if the given identity has been signed by this identity.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `identity` - The identity of another user that we want to check if
|
||||
/// it's has been signed.
|
||||
///
|
||||
/// Returns an empty result if the signature check succeeded, otherwise a
|
||||
/// SignatureError indicating why the check failed.
|
||||
pub fn is_identity_signed(&self, identity: &UserIdentity) -> Result<(), SignatureError> {
|
||||
self.user_signing_key
|
||||
.verify_master_key(&identity.master_key)
|
||||
}
|
||||
|
||||
/// Check if the given device has been signed by this identity.
|
||||
///
|
||||
/// Only devices of our own user should be checked with this method, if a
|
||||
/// device of a different user is given the signature check will always fail
|
||||
/// even if a valid signature exists.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `device` - The device that should be checked for a valid signature.
|
||||
///
|
||||
/// Returns an empty result if the signature check succeeded, otherwise a
|
||||
/// SignatureError indicating why the check failed.
|
||||
pub fn is_device_signed(&self, device: &ReadOnlyDevice) -> Result<(), SignatureError> {
|
||||
if self.user_id() != device.user_id() {
|
||||
return Err(SignatureError::UserIdMissmatch);
|
||||
}
|
||||
|
||||
self.self_signing_key.verify_device(device)
|
||||
}
|
||||
|
||||
/// Mark our identity as verified.
|
||||
pub fn mark_as_verified(&self) {
|
||||
self.verified.store(true, Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Check if our identity is verified.
|
||||
pub fn is_verified(&self) -> bool {
|
||||
self.verified.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Update the identity with a new master key and self signing key.
|
||||
///
|
||||
/// Note: This will reset the verification state if the master keys differ.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `master_key` - The new master key of the user identity.
|
||||
///
|
||||
/// * `self_signing_key` - The new self signing key of user identity.
|
||||
///
|
||||
/// * `user_signing_key` - The new user signing key of user identity.
|
||||
///
|
||||
/// Returns a `SignatureError` if we failed to update the identity.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
master_key: MasterPubkey,
|
||||
self_signing_key: SelfSigningPubkey,
|
||||
user_signing_key: UserSigningPubkey,
|
||||
) -> Result<(), SignatureError> {
|
||||
master_key.verify_subkey(&self_signing_key)?;
|
||||
master_key.verify_subkey(&user_signing_key)?;
|
||||
|
||||
self.self_signing_key = self_signing_key;
|
||||
self.user_signing_key = user_signing_key;
|
||||
|
||||
if self.master_key != master_key {
|
||||
self.verified.store(false, Ordering::SeqCst)
|
||||
}
|
||||
|
||||
self.master_key = master_key;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use std::{convert::TryFrom, sync::Arc};
|
||||
|
||||
use crate::{
|
||||
identities::{
|
||||
manager::test::{other_key_query, own_key_query},
|
||||
Device, ReadOnlyDevice,
|
||||
},
|
||||
olm::{PrivateCrossSigningIdentity, ReadOnlyAccount},
|
||||
store::MemoryStore,
|
||||
verification::VerificationMachine,
|
||||
};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
api::r0::keys::get_keys::Response as KeyQueryResponse, identifiers::user_id, locks::Mutex,
|
||||
};
|
||||
use matrix_sdk_test::async_test;
|
||||
|
||||
use super::{OwnUserIdentity, UserIdentities, UserIdentity};
|
||||
|
||||
fn device(response: &KeyQueryResponse) -> (ReadOnlyDevice, ReadOnlyDevice) {
|
||||
let mut devices = response.device_keys.values().next().unwrap().values();
|
||||
let first = ReadOnlyDevice::try_from(devices.next().unwrap()).unwrap();
|
||||
let second = ReadOnlyDevice::try_from(devices.next().unwrap()).unwrap();
|
||||
(first, second)
|
||||
}
|
||||
|
||||
fn own_identity(response: &KeyQueryResponse) -> OwnUserIdentity {
|
||||
let user_id = user_id!("@example:localhost");
|
||||
|
||||
let master_key = response.master_keys.get(&user_id).unwrap();
|
||||
let user_signing = response.user_signing_keys.get(&user_id).unwrap();
|
||||
let self_signing = response.self_signing_keys.get(&user_id).unwrap();
|
||||
|
||||
OwnUserIdentity::new(master_key.into(), self_signing.into(), user_signing.into()).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) fn get_own_identity() -> OwnUserIdentity {
|
||||
own_identity(&own_key_query())
|
||||
}
|
||||
|
||||
pub(crate) fn get_other_identity() -> UserIdentity {
|
||||
let user_id = user_id!("@example2:localhost");
|
||||
let response = other_key_query();
|
||||
|
||||
let master_key = response.master_keys.get(&user_id).unwrap();
|
||||
let self_signing = response.self_signing_keys.get(&user_id).unwrap();
|
||||
|
||||
UserIdentity::new(master_key.into(), self_signing.into()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn own_identity_create() {
|
||||
let user_id = user_id!("@example:localhost");
|
||||
let response = own_key_query();
|
||||
|
||||
let master_key = response.master_keys.get(&user_id).unwrap();
|
||||
let user_signing = response.user_signing_keys.get(&user_id).unwrap();
|
||||
let self_signing = response.self_signing_keys.get(&user_id).unwrap();
|
||||
|
||||
OwnUserIdentity::new(master_key.into(), self_signing.into(), user_signing.into()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn other_identity_create() {
|
||||
get_other_identity();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn own_identity_check_signatures() {
|
||||
let response = own_key_query();
|
||||
let identity = get_own_identity();
|
||||
let (first, second) = device(&response);
|
||||
|
||||
assert!(identity.is_device_signed(&first).is_err());
|
||||
assert!(identity.is_device_signed(&second).is_ok());
|
||||
|
||||
let private_identity = Arc::new(Mutex::new(PrivateCrossSigningIdentity::empty(
|
||||
second.user_id().clone(),
|
||||
)));
|
||||
let verification_machine = VerificationMachine::new(
|
||||
ReadOnlyAccount::new(second.user_id(), second.device_id()),
|
||||
private_identity.clone(),
|
||||
Arc::new(Box::new(MemoryStore::new())),
|
||||
);
|
||||
|
||||
let first = Device {
|
||||
inner: first,
|
||||
verification_machine: verification_machine.clone(),
|
||||
private_identity: private_identity.clone(),
|
||||
own_identity: Some(identity.clone()),
|
||||
device_owner_identity: Some(UserIdentities::Own(identity.clone())),
|
||||
};
|
||||
|
||||
let second = Device {
|
||||
inner: second,
|
||||
verification_machine,
|
||||
private_identity,
|
||||
own_identity: Some(identity.clone()),
|
||||
device_owner_identity: Some(UserIdentities::Own(identity.clone())),
|
||||
};
|
||||
|
||||
assert!(!second.trust_state());
|
||||
assert!(!second.is_trusted());
|
||||
|
||||
assert!(!first.trust_state());
|
||||
assert!(!first.is_trusted());
|
||||
|
||||
identity.mark_as_verified();
|
||||
assert!(second.trust_state());
|
||||
assert!(!first.trust_state());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn own_device_with_private_identity() {
|
||||
let response = own_key_query();
|
||||
let (_, device) = device(&response);
|
||||
|
||||
let account = ReadOnlyAccount::new(device.user_id(), device.device_id());
|
||||
let (identity, _, _) = PrivateCrossSigningIdentity::new_with_account(&account).await;
|
||||
|
||||
let id = Arc::new(Mutex::new(identity.clone()));
|
||||
|
||||
let verification_machine = VerificationMachine::new(
|
||||
ReadOnlyAccount::new(device.user_id(), device.device_id()),
|
||||
id.clone(),
|
||||
Arc::new(Box::new(MemoryStore::new())),
|
||||
);
|
||||
|
||||
let public_identity = identity.as_public_identity().await.unwrap();
|
||||
|
||||
let mut device = Device {
|
||||
inner: device,
|
||||
verification_machine: verification_machine.clone(),
|
||||
private_identity: id.clone(),
|
||||
own_identity: Some(public_identity.clone()),
|
||||
device_owner_identity: Some(public_identity.clone().into()),
|
||||
};
|
||||
|
||||
assert!(!device.trust_state());
|
||||
|
||||
let mut device_keys = device.as_device_keys();
|
||||
|
||||
identity.sign_device_keys(&mut device_keys).await.unwrap();
|
||||
device.inner.signatures = Arc::new(device_keys.signatures);
|
||||
assert!(device.trust_state());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -25,19 +25,32 @@
|
||||
unused_import_braces,
|
||||
unused_qualifications
|
||||
)]
|
||||
#![cfg_attr(feature = "docs", feature(doc_cfg))]
|
||||
|
||||
mod device;
|
||||
mod error;
|
||||
mod file_encryption;
|
||||
mod identities;
|
||||
mod key_request;
|
||||
mod machine;
|
||||
mod memory_stores;
|
||||
mod olm;
|
||||
mod store;
|
||||
pub mod olm;
|
||||
mod requests;
|
||||
mod session_manager;
|
||||
pub mod store;
|
||||
mod utilities;
|
||||
mod verification;
|
||||
|
||||
pub use device::{Device, TrustState};
|
||||
pub use error::{MegolmError, OlmError};
|
||||
pub use machine::{OlmMachine, OneTimeKeys};
|
||||
pub use memory_stores::{DeviceStore, GroupSessionStore, SessionStore, UserDevices};
|
||||
pub use olm::{Account, InboundGroupSession, OutboundGroupSession, Session};
|
||||
#[cfg(feature = "sqlite-cryptostore")]
|
||||
pub use store::sqlite::SqliteStore;
|
||||
pub use store::{CryptoStore, CryptoStoreError};
|
||||
pub use file_encryption::{
|
||||
decrypt_key_export, encrypt_key_export, AttachmentDecryptor, AttachmentEncryptor,
|
||||
DecryptorError,
|
||||
};
|
||||
pub use identities::{
|
||||
Device, LocalTrust, OwnUserIdentity, ReadOnlyDevice, UserDevices, UserIdentities, UserIdentity,
|
||||
};
|
||||
pub use machine::OlmMachine;
|
||||
pub use olm::EncryptionSettings;
|
||||
pub(crate) use olm::ReadOnlyAccount;
|
||||
pub use requests::{
|
||||
IncomingResponse, KeysQueryRequest, OutgoingRequest, OutgoingRequests, ToDeviceRequest,
|
||||
};
|
||||
pub use verification::Sas;
|
||||
|
||||
+1129
-1398
File diff suppressed because it is too large
Load Diff
@@ -1,793 +0,0 @@
|
||||
// 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.
|
||||
|
||||
use matrix_sdk_common::instant::Instant;
|
||||
use std::fmt;
|
||||
use std::mem;
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
use serde::Serialize;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
pub use olm_rs::account::IdentityKeys;
|
||||
use olm_rs::account::{OlmAccount, OneTimeKeys};
|
||||
use olm_rs::errors::{OlmAccountError, OlmGroupSessionError, OlmSessionError};
|
||||
use olm_rs::inbound_group_session::OlmInboundGroupSession;
|
||||
use olm_rs::outbound_group_session::OlmOutboundGroupSession;
|
||||
use olm_rs::session::OlmSession;
|
||||
use olm_rs::PicklingMode;
|
||||
|
||||
pub use olm_rs::{
|
||||
session::{OlmMessage, PreKeyMessage},
|
||||
utility::OlmUtility,
|
||||
};
|
||||
|
||||
use matrix_sdk_common::api::r0::keys::SignedKey;
|
||||
use matrix_sdk_common::identifiers::RoomId;
|
||||
|
||||
/// Account holding identity keys for which sessions can be created.
|
||||
///
|
||||
/// An account is the central identity for encrypted communication between two
|
||||
/// devices.
|
||||
#[derive(Clone)]
|
||||
pub struct Account {
|
||||
inner: Arc<Mutex<OlmAccount>>,
|
||||
identity_keys: Arc<IdentityKeys>,
|
||||
shared: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
#[cfg_attr(tarpaulin, skip)]
|
||||
impl fmt::Debug for Account {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Account")
|
||||
.field("identity_keys", self.identity_keys())
|
||||
.field("shared", &self.shared())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(tarpaulin, skip)]
|
||||
impl Default for Account {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Account {
|
||||
/// Create a fresh new account, this will generate the identity key-pair.
|
||||
pub fn new() -> Self {
|
||||
let account = OlmAccount::new();
|
||||
let identity_keys = account.parsed_identity_keys();
|
||||
|
||||
Account {
|
||||
inner: Arc::new(Mutex::new(account)),
|
||||
identity_keys: Arc::new(identity_keys),
|
||||
shared: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the public parts of the identity keys for the account.
|
||||
pub fn identity_keys(&self) -> &IdentityKeys {
|
||||
&self.identity_keys
|
||||
}
|
||||
|
||||
/// Has the account been shared with the server.
|
||||
pub fn shared(&self) -> bool {
|
||||
self.shared.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Mark the account as shared.
|
||||
///
|
||||
/// Messages shouldn't be encrypted with the session before it has been
|
||||
/// shared.
|
||||
pub fn mark_as_shared(&self) {
|
||||
self.shared.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Get the one-time keys of the account.
|
||||
///
|
||||
/// This can be empty, keys need to be generated first.
|
||||
pub async fn one_time_keys(&self) -> OneTimeKeys {
|
||||
self.inner.lock().await.parsed_one_time_keys()
|
||||
}
|
||||
|
||||
/// Generate count number of one-time keys.
|
||||
pub async fn generate_one_time_keys(&self, count: usize) {
|
||||
self.inner.lock().await.generate_one_time_keys(count);
|
||||
}
|
||||
|
||||
/// Get the maximum number of one-time keys the account can hold.
|
||||
pub async fn max_one_time_keys(&self) -> usize {
|
||||
self.inner.lock().await.max_number_of_one_time_keys()
|
||||
}
|
||||
|
||||
/// Mark the current set of one-time keys as being published.
|
||||
pub async fn mark_keys_as_published(&self) {
|
||||
self.inner.lock().await.mark_keys_as_published();
|
||||
}
|
||||
|
||||
/// Sign the given string using the accounts signing key.
|
||||
///
|
||||
/// Returns the signature as a base64 encoded string.
|
||||
pub async fn sign(&self, string: &str) -> String {
|
||||
self.inner.lock().await.sign(string)
|
||||
}
|
||||
|
||||
/// Store the account as a base64 encoded string.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pickle_mode` - The mode that was used to pickle the account, either an
|
||||
/// unencrypted mode or an encrypted using passphrase.
|
||||
pub async fn pickle(&self, pickle_mode: PicklingMode) -> String {
|
||||
self.inner.lock().await.pickle(pickle_mode)
|
||||
}
|
||||
|
||||
/// Restore an account from a previously pickled string.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pickle` - The pickled string of the account.
|
||||
///
|
||||
/// * `pickle_mode` - The mode that was used to pickle the account, either an
|
||||
/// unencrypted mode or an encrypted using passphrase.
|
||||
///
|
||||
/// * `shared` - Boolean determining if the account was uploaded to the
|
||||
/// server.
|
||||
pub fn from_pickle(
|
||||
pickle: String,
|
||||
pickle_mode: PicklingMode,
|
||||
shared: bool,
|
||||
) -> Result<Self, OlmAccountError> {
|
||||
let account = OlmAccount::unpickle(pickle, pickle_mode)?;
|
||||
let identity_keys = account.parsed_identity_keys();
|
||||
|
||||
Ok(Account {
|
||||
inner: Arc::new(Mutex::new(account)),
|
||||
identity_keys: Arc::new(identity_keys),
|
||||
shared: Arc::new(AtomicBool::from(shared)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new session with another account given a one-time key.
|
||||
///
|
||||
/// Returns the newly created session or a `OlmSessionError` if creating a
|
||||
/// session failed.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `their_identity_key` - The other account's identity/curve25519 key.
|
||||
///
|
||||
/// * `their_one_time_key` - A signed one-time key that the other account
|
||||
/// created and shared with us.
|
||||
pub async fn create_outbound_session(
|
||||
&self,
|
||||
their_identity_key: &str,
|
||||
their_one_time_key: &SignedKey,
|
||||
) -> Result<Session, OlmSessionError> {
|
||||
let session = self
|
||||
.inner
|
||||
.lock()
|
||||
.await
|
||||
.create_outbound_session(their_identity_key, &their_one_time_key.key)?;
|
||||
|
||||
let now = Instant::now();
|
||||
let session_id = session.session_id();
|
||||
|
||||
Ok(Session {
|
||||
inner: Arc::new(Mutex::new(session)),
|
||||
session_id: Arc::new(session_id),
|
||||
sender_key: Arc::new(their_identity_key.to_owned()),
|
||||
creation_time: Arc::new(now),
|
||||
last_use_time: Arc::new(now),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a new session with another account given a pre-key Olm message.
|
||||
///
|
||||
/// Returns the newly created session or a `OlmSessionError` if creating a
|
||||
/// session failed.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `their_identity_key` - The other account's identitiy/curve25519 key.
|
||||
///
|
||||
/// * `message` - A pre-key Olm message that was sent to us by the other
|
||||
/// account.
|
||||
pub async fn create_inbound_session(
|
||||
&self,
|
||||
their_identity_key: &str,
|
||||
message: PreKeyMessage,
|
||||
) -> Result<Session, OlmSessionError> {
|
||||
let session = self
|
||||
.inner
|
||||
.lock()
|
||||
.await
|
||||
.create_inbound_session_from(their_identity_key, message)?;
|
||||
|
||||
self.inner
|
||||
.lock()
|
||||
.await
|
||||
.remove_one_time_keys(&session)
|
||||
.expect(
|
||||
"Session was successfully created but the account doesn't hold a matching one-time key",
|
||||
);
|
||||
|
||||
let now = Instant::now();
|
||||
let session_id = session.session_id();
|
||||
|
||||
Ok(Session {
|
||||
inner: Arc::new(Mutex::new(session)),
|
||||
session_id: Arc::new(session_id),
|
||||
sender_key: Arc::new(their_identity_key.to_owned()),
|
||||
creation_time: Arc::new(now),
|
||||
last_use_time: Arc::new(now),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Account {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.identity_keys() == other.identity_keys() && self.shared() == other.shared()
|
||||
}
|
||||
}
|
||||
|
||||
/// Cryptographic session that enables secure communication between two
|
||||
/// `Account`s
|
||||
#[derive(Clone)]
|
||||
pub struct Session {
|
||||
inner: Arc<Mutex<OlmSession>>,
|
||||
session_id: Arc<String>,
|
||||
pub(crate) sender_key: Arc<String>,
|
||||
pub(crate) creation_time: Arc<Instant>,
|
||||
pub(crate) last_use_time: Arc<Instant>,
|
||||
}
|
||||
|
||||
#[cfg_attr(tarpaulin, skip)]
|
||||
impl fmt::Debug for Session {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Session")
|
||||
.field("session_id", &self.session_id())
|
||||
.field("sender_key", &self.sender_key)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Decrypt the given Olm message.
|
||||
///
|
||||
/// Returns the decrypted plaintext or an `OlmSessionError` if decryption
|
||||
/// failed.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `message` - The Olm message that should be decrypted.
|
||||
pub async fn decrypt(&mut self, message: OlmMessage) -> Result<String, OlmSessionError> {
|
||||
let plaintext = self.inner.lock().await.decrypt(message)?;
|
||||
mem::replace(&mut self.last_use_time, Arc::new(Instant::now()));
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
/// Encrypt the given plaintext as a OlmMessage.
|
||||
///
|
||||
/// Returns the encrypted Olm message.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `plaintext` - The plaintext that should be encrypted.
|
||||
pub async fn encrypt(&mut self, plaintext: &str) -> OlmMessage {
|
||||
let message = self.inner.lock().await.encrypt(plaintext);
|
||||
mem::replace(&mut self.last_use_time, Arc::new(Instant::now()));
|
||||
message
|
||||
}
|
||||
|
||||
/// Check if a pre-key Olm message was encrypted for this session.
|
||||
///
|
||||
/// Returns true if it matches, false if not and a OlmSessionError if there
|
||||
/// was an error checking if it matches.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `their_identity_key` - The identity/curve25519 key of the account
|
||||
/// that encrypted this Olm message.
|
||||
///
|
||||
/// * `message` - The pre-key Olm message that should be checked.
|
||||
pub async fn matches(
|
||||
&self,
|
||||
their_identity_key: &str,
|
||||
message: PreKeyMessage,
|
||||
) -> Result<bool, OlmSessionError> {
|
||||
self.inner
|
||||
.lock()
|
||||
.await
|
||||
.matches_inbound_session_from(their_identity_key, message)
|
||||
}
|
||||
|
||||
/// Returns the unique identifier for this session.
|
||||
pub fn session_id(&self) -> &str {
|
||||
&self.session_id
|
||||
}
|
||||
|
||||
/// Store the session as a base64 encoded string.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pickle_mode` - The mode that was used to pickle the session, either
|
||||
/// an unencrypted mode or an encrypted using passphrase.
|
||||
pub async fn pickle(&self, pickle_mode: PicklingMode) -> String {
|
||||
self.inner.lock().await.pickle(pickle_mode)
|
||||
}
|
||||
|
||||
/// Restore a Session from a previously pickled string.
|
||||
///
|
||||
/// Returns the restored Olm Session or a `OlmSessionError` if there was an
|
||||
/// error.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pickle` - The pickled string of the session.
|
||||
///
|
||||
/// * `pickle_mode` - The mode that was used to pickle the session, either
|
||||
/// an unencrypted mode or an encrypted using passphrase.
|
||||
///
|
||||
/// * `sender_key` - The public curve25519 key of the account that
|
||||
/// established the session with us.
|
||||
///
|
||||
/// * `creation_time` - The timestamp that marks when the session was
|
||||
/// created.
|
||||
///
|
||||
/// * `last_use_time` - The timestamp that marks when the session was
|
||||
/// last used to encrypt or decrypt an Olm message.
|
||||
pub fn from_pickle(
|
||||
pickle: String,
|
||||
pickle_mode: PicklingMode,
|
||||
sender_key: String,
|
||||
creation_time: Instant,
|
||||
last_use_time: Instant,
|
||||
) -> Result<Self, OlmSessionError> {
|
||||
let session = OlmSession::unpickle(pickle, pickle_mode)?;
|
||||
let session_id = session.session_id();
|
||||
|
||||
Ok(Session {
|
||||
inner: Arc::new(Mutex::new(session)),
|
||||
session_id: Arc::new(session_id),
|
||||
sender_key: Arc::new(sender_key),
|
||||
creation_time: Arc::new(creation_time),
|
||||
last_use_time: Arc::new(last_use_time),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Session {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.session_id() == other.session_id()
|
||||
}
|
||||
}
|
||||
|
||||
/// The private session key of a group session.
|
||||
/// Can be used to create a new inbound group session.
|
||||
#[derive(Clone, Debug, Serialize, Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
pub struct GroupSessionKey(pub String);
|
||||
|
||||
/// Inbound group session.
|
||||
///
|
||||
/// Inbound group sessions are used to exchange room messages between a group of
|
||||
/// participants. Inbound group sessions are used to decrypt the room messages.
|
||||
#[derive(Clone)]
|
||||
pub struct InboundGroupSession {
|
||||
inner: Arc<Mutex<OlmInboundGroupSession>>,
|
||||
session_id: Arc<String>,
|
||||
pub(crate) sender_key: Arc<String>,
|
||||
pub(crate) signing_key: Arc<String>,
|
||||
pub(crate) room_id: Arc<RoomId>,
|
||||
forwarding_chains: Arc<Mutex<Option<Vec<String>>>>,
|
||||
}
|
||||
|
||||
impl InboundGroupSession {
|
||||
/// Create a new inbound group session for the given room.
|
||||
///
|
||||
/// These sessions are used to decrypt room messages.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `sender_key` - The public curve25519 key of the account that
|
||||
/// sent us the session
|
||||
///
|
||||
/// * `signing_key` - The public ed25519 key of the account that
|
||||
/// sent us the session.
|
||||
///
|
||||
/// * `room_id` - The id of the room that the session is used in.
|
||||
///
|
||||
/// * `session_key` - The private session key that is used to decrypt
|
||||
/// messages.
|
||||
pub fn new(
|
||||
sender_key: &str,
|
||||
signing_key: &str,
|
||||
room_id: &RoomId,
|
||||
session_key: GroupSessionKey,
|
||||
) -> Result<Self, OlmGroupSessionError> {
|
||||
let session = OlmInboundGroupSession::new(&session_key.0)?;
|
||||
let session_id = session.session_id();
|
||||
|
||||
Ok(InboundGroupSession {
|
||||
inner: Arc::new(Mutex::new(session)),
|
||||
session_id: Arc::new(session_id),
|
||||
sender_key: Arc::new(sender_key.to_owned()),
|
||||
signing_key: Arc::new(signing_key.to_owned()),
|
||||
room_id: Arc::new(room_id.clone()),
|
||||
forwarding_chains: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Store the group session as a base64 encoded string.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pickle_mode` - The mode that was used to pickle the group session,
|
||||
/// either an unencrypted mode or an encrypted using passphrase.
|
||||
pub async fn pickle(&self, pickle_mode: PicklingMode) -> String {
|
||||
self.inner.lock().await.pickle(pickle_mode)
|
||||
}
|
||||
|
||||
/// Restore a Session from a previously pickled string.
|
||||
///
|
||||
/// Returns the restored group session or a `OlmGroupSessionError` if there
|
||||
/// was an error.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pickle` - The pickled string of the group session session.
|
||||
///
|
||||
/// * `pickle_mode` - The mode that was used to pickle the session, either
|
||||
/// an unencrypted mode or an encrypted using passphrase.
|
||||
///
|
||||
/// * `sender_key` - The public curve25519 key of the account that
|
||||
/// sent us the session
|
||||
///
|
||||
/// * `signing_key` - The public ed25519 key of the account that
|
||||
/// sent us the session.
|
||||
///
|
||||
/// * `room_id` - The id of the room that the session is used in.
|
||||
pub fn from_pickle(
|
||||
pickle: String,
|
||||
pickle_mode: PicklingMode,
|
||||
sender_key: String,
|
||||
signing_key: String,
|
||||
room_id: RoomId,
|
||||
) -> Result<Self, OlmGroupSessionError> {
|
||||
let session = OlmInboundGroupSession::unpickle(pickle, pickle_mode)?;
|
||||
let session_id = session.session_id();
|
||||
|
||||
Ok(InboundGroupSession {
|
||||
inner: Arc::new(Mutex::new(session)),
|
||||
session_id: Arc::new(session_id),
|
||||
sender_key: Arc::new(sender_key),
|
||||
signing_key: Arc::new(signing_key),
|
||||
room_id: Arc::new(room_id),
|
||||
forwarding_chains: Arc::new(Mutex::new(None)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the unique identifier for this session.
|
||||
pub fn session_id(&self) -> &str {
|
||||
&self.session_id
|
||||
}
|
||||
|
||||
/// Get the first message index we know how to decrypt.
|
||||
pub async fn first_known_index(&self) -> u32 {
|
||||
self.inner.lock().await.first_known_index()
|
||||
}
|
||||
|
||||
/// Decrypt the given ciphertext.
|
||||
///
|
||||
/// Returns the decrypted plaintext or an `OlmGroupSessionError` if
|
||||
/// decryption failed.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `message` - The message that should be decrypted.
|
||||
pub async fn decrypt(&self, message: String) -> Result<(String, u32), OlmGroupSessionError> {
|
||||
self.inner.lock().await.decrypt(message)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(tarpaulin, skip)]
|
||||
impl fmt::Debug for InboundGroupSession {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("InboundGroupSession")
|
||||
.field("session_id", &self.session_id())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for InboundGroupSession {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.session_id() == other.session_id()
|
||||
}
|
||||
}
|
||||
|
||||
/// Outbound group session.
|
||||
///
|
||||
/// Outbound group sessions are used to exchange room messages between a group
|
||||
/// of participants. Outbound group sessions are used to encrypt the room
|
||||
/// messages.
|
||||
#[derive(Clone)]
|
||||
pub struct OutboundGroupSession {
|
||||
inner: Arc<Mutex<OlmOutboundGroupSession>>,
|
||||
session_id: Arc<String>,
|
||||
room_id: Arc<RoomId>,
|
||||
creation_time: Arc<Instant>,
|
||||
message_count: Arc<AtomicUsize>,
|
||||
shared: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl OutboundGroupSession {
|
||||
/// Create a new outbound group session for the given room.
|
||||
///
|
||||
/// Outbound group sessions are used to encrypt room messages.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The id of the room that the session is used in.
|
||||
pub fn new(room_id: &RoomId) -> Self {
|
||||
let session = OlmOutboundGroupSession::new();
|
||||
let session_id = session.session_id();
|
||||
|
||||
OutboundGroupSession {
|
||||
inner: Arc::new(Mutex::new(session)),
|
||||
room_id: Arc::new(room_id.to_owned()),
|
||||
session_id: Arc::new(session_id),
|
||||
creation_time: Arc::new(Instant::now()),
|
||||
message_count: Arc::new(AtomicUsize::new(0)),
|
||||
shared: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypt the given plaintext using this session.
|
||||
///
|
||||
/// Returns the encrypted ciphertext.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `plaintext` - The plaintext that should be encrypted.
|
||||
pub async fn encrypt(&self, plaintext: String) -> String {
|
||||
let session = self.inner.lock().await;
|
||||
session.encrypt(plaintext)
|
||||
}
|
||||
|
||||
/// Check if the session has expired and if it should be rotated.
|
||||
///
|
||||
/// A session will expire after some time or if enough messages have been
|
||||
/// encrypted using it.
|
||||
pub fn expired(&self) -> bool {
|
||||
// TODO implement this.
|
||||
false
|
||||
}
|
||||
|
||||
/// Mark the session as shared.
|
||||
///
|
||||
/// Messages shouldn't be encrypted with the session before it has been
|
||||
/// shared.
|
||||
pub fn mark_as_shared(&self) {
|
||||
self.shared.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Check if the session has been marked as shared.
|
||||
pub fn shared(&self) -> bool {
|
||||
self.shared.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Get the session key of this session.
|
||||
///
|
||||
/// A session key can be used to to create an `InboundGroupSession`.
|
||||
pub async fn session_key(&self) -> GroupSessionKey {
|
||||
let session = self.inner.lock().await;
|
||||
GroupSessionKey(session.session_key())
|
||||
}
|
||||
|
||||
/// Returns the unique identifier for this session.
|
||||
pub fn session_id(&self) -> &str {
|
||||
&self.session_id
|
||||
}
|
||||
|
||||
/// Get the current message index for this session.
|
||||
///
|
||||
/// Each message is sent with an increasing index. This returns the
|
||||
/// message index that will be used for the next encrypted message.
|
||||
pub async fn message_index(&self) -> u32 {
|
||||
let session = self.inner.lock().await;
|
||||
session.session_message_index()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(tarpaulin, skip)]
|
||||
impl std::fmt::Debug for OutboundGroupSession {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("OutboundGroupSession")
|
||||
.field("session_id", &self.session_id)
|
||||
.field("room_id", &self.room_id)
|
||||
.field("creation_time", &self.creation_time)
|
||||
.field("message_count", &self.message_count)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use crate::olm::{Account, InboundGroupSession, OutboundGroupSession, Session};
|
||||
use matrix_sdk_common::api::r0::keys::SignedKey;
|
||||
use matrix_sdk_common::identifiers::RoomId;
|
||||
use olm_rs::session::OlmMessage;
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
pub(crate) async fn get_account_and_session() -> (Account, Session) {
|
||||
let alice = Account::new();
|
||||
|
||||
let bob = Account::new();
|
||||
|
||||
bob.generate_one_time_keys(1).await;
|
||||
let one_time_key = bob
|
||||
.one_time_keys()
|
||||
.await
|
||||
.curve25519()
|
||||
.iter()
|
||||
.nth(0)
|
||||
.unwrap()
|
||||
.1
|
||||
.to_owned();
|
||||
let one_time_key = SignedKey {
|
||||
key: one_time_key,
|
||||
signatures: BTreeMap::new(),
|
||||
};
|
||||
let sender_key = bob.identity_keys().curve25519().to_owned();
|
||||
let session = alice
|
||||
.create_outbound_session(&sender_key, &one_time_key)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
(alice, session)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_creation() {
|
||||
let account = Account::new();
|
||||
let identyty_keys = account.identity_keys();
|
||||
|
||||
assert!(!account.shared());
|
||||
assert!(!identyty_keys.ed25519().is_empty());
|
||||
assert_ne!(identyty_keys.values().len(), 0);
|
||||
assert_ne!(identyty_keys.keys().len(), 0);
|
||||
assert_ne!(identyty_keys.iter().len(), 0);
|
||||
assert!(identyty_keys.contains_key("ed25519"));
|
||||
assert_eq!(
|
||||
identyty_keys.ed25519(),
|
||||
identyty_keys.get("ed25519").unwrap()
|
||||
);
|
||||
assert!(!identyty_keys.curve25519().is_empty());
|
||||
|
||||
account.mark_as_shared();
|
||||
assert!(account.shared());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn one_time_keys_creation() {
|
||||
let account = Account::new();
|
||||
let one_time_keys = account.one_time_keys().await;
|
||||
|
||||
assert!(one_time_keys.curve25519().is_empty());
|
||||
assert_ne!(account.max_one_time_keys().await, 0);
|
||||
|
||||
account.generate_one_time_keys(10).await;
|
||||
let one_time_keys = account.one_time_keys().await;
|
||||
|
||||
assert!(!one_time_keys.curve25519().is_empty());
|
||||
assert_ne!(one_time_keys.values().len(), 0);
|
||||
assert_ne!(one_time_keys.keys().len(), 0);
|
||||
assert_ne!(one_time_keys.iter().len(), 0);
|
||||
assert!(one_time_keys.contains_key("curve25519"));
|
||||
assert_eq!(one_time_keys.curve25519().keys().len(), 10);
|
||||
assert_eq!(
|
||||
one_time_keys.curve25519(),
|
||||
one_time_keys.get("curve25519").unwrap()
|
||||
);
|
||||
|
||||
account.mark_keys_as_published().await;
|
||||
let one_time_keys = account.one_time_keys().await;
|
||||
assert!(one_time_keys.curve25519().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_creation() {
|
||||
let alice = Account::new();
|
||||
let bob = Account::new();
|
||||
let alice_keys = alice.identity_keys();
|
||||
alice.generate_one_time_keys(1).await;
|
||||
let one_time_keys = alice.one_time_keys().await;
|
||||
alice.mark_keys_as_published().await;
|
||||
|
||||
let one_time_key = one_time_keys
|
||||
.curve25519()
|
||||
.iter()
|
||||
.nth(0)
|
||||
.unwrap()
|
||||
.1
|
||||
.to_owned();
|
||||
|
||||
let one_time_key = SignedKey {
|
||||
key: one_time_key,
|
||||
signatures: BTreeMap::new(),
|
||||
};
|
||||
|
||||
let mut bob_session = bob
|
||||
.create_outbound_session(alice_keys.curve25519(), &one_time_key)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let plaintext = "Hello world";
|
||||
|
||||
let message = bob_session.encrypt(plaintext).await;
|
||||
|
||||
let prekey_message = match message.clone() {
|
||||
OlmMessage::PreKey(m) => m,
|
||||
OlmMessage::Message(_) => panic!("Incorrect message type"),
|
||||
};
|
||||
|
||||
let bob_keys = bob.identity_keys();
|
||||
let mut alice_session = alice
|
||||
.create_inbound_session(bob_keys.curve25519(), prekey_message.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(alice_session
|
||||
.matches(bob_keys.curve25519(), prekey_message)
|
||||
.await
|
||||
.unwrap());
|
||||
|
||||
assert_eq!(bob_session.session_id(), alice_session.session_id());
|
||||
|
||||
let decyrpted = alice_session.decrypt(message).await.unwrap();
|
||||
assert_eq!(plaintext, decyrpted);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn group_session_creation() {
|
||||
let room_id = RoomId::try_from("!test:localhost").unwrap();
|
||||
|
||||
let outbound = OutboundGroupSession::new(&room_id);
|
||||
|
||||
assert_eq!(0, outbound.message_index().await);
|
||||
assert!(!outbound.shared());
|
||||
outbound.mark_as_shared();
|
||||
assert!(outbound.shared());
|
||||
|
||||
let inbound = InboundGroupSession::new(
|
||||
"test_key",
|
||||
"test_key",
|
||||
&room_id,
|
||||
outbound.session_key().await,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(0, inbound.first_known_index().await);
|
||||
|
||||
assert_eq!(outbound.session_id(), inbound.session_id());
|
||||
|
||||
let plaintext = "This is a secret to everybody".to_owned();
|
||||
let ciphertext = outbound.encrypt(plaintext.clone()).await;
|
||||
|
||||
assert_eq!(plaintext, inbound.decrypt(ciphertext).await.unwrap().0);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,412 @@
|
||||
// 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.
|
||||
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
convert::{TryFrom, TryInto},
|
||||
fmt, mem,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use olm_rs::{
|
||||
errors::OlmGroupSessionError, inbound_group_session::OlmInboundGroupSession, PicklingMode,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
pub use olm_rs::{
|
||||
account::IdentityKeys,
|
||||
session::{OlmMessage, PreKeyMessage},
|
||||
utility::OlmUtility,
|
||||
};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
events::{
|
||||
forwarded_room_key::ForwardedRoomKeyToDeviceEventContent,
|
||||
room::encrypted::EncryptedEventContent, AnySyncRoomEvent, SyncMessageEvent,
|
||||
},
|
||||
identifiers::{DeviceKeyAlgorithm, EventEncryptionAlgorithm, RoomId},
|
||||
locks::Mutex,
|
||||
Raw,
|
||||
};
|
||||
|
||||
use super::{ExportedGroupSessionKey, ExportedRoomKey, GroupSessionKey};
|
||||
use crate::error::{EventError, MegolmResult};
|
||||
|
||||
// TODO add creation times to the inbound grop sessions so we can export
|
||||
// sessions that were created between some time period, this should only be set
|
||||
// for non-imported sessoins.
|
||||
|
||||
/// Inbound group session.
|
||||
///
|
||||
/// Inbound group sessions are used to exchange room messages between a group of
|
||||
/// participants. Inbound group sessions are used to decrypt the room messages.
|
||||
#[derive(Clone)]
|
||||
pub struct InboundGroupSession {
|
||||
inner: Arc<Mutex<OlmInboundGroupSession>>,
|
||||
session_id: Arc<str>,
|
||||
first_known_index: u32,
|
||||
pub(crate) sender_key: Arc<str>,
|
||||
pub(crate) signing_key: Arc<BTreeMap<DeviceKeyAlgorithm, String>>,
|
||||
pub(crate) room_id: Arc<RoomId>,
|
||||
forwarding_chains: Arc<Mutex<Option<Vec<String>>>>,
|
||||
imported: Arc<bool>,
|
||||
}
|
||||
|
||||
impl InboundGroupSession {
|
||||
/// Create a new inbound group session for the given room.
|
||||
///
|
||||
/// These sessions are used to decrypt room messages.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `sender_key` - The public curve25519 key of the account that
|
||||
/// sent us the session
|
||||
///
|
||||
/// * `signing_key` - The public ed25519 key of the account that
|
||||
/// sent us the session.
|
||||
///
|
||||
/// * `room_id` - The id of the room that the session is used in.
|
||||
///
|
||||
/// * `session_key` - The private session key that is used to decrypt
|
||||
/// messages.
|
||||
pub(crate) fn new(
|
||||
sender_key: &str,
|
||||
signing_key: &str,
|
||||
room_id: &RoomId,
|
||||
session_key: GroupSessionKey,
|
||||
) -> Result<Self, OlmGroupSessionError> {
|
||||
let session = OlmInboundGroupSession::new(&session_key.0)?;
|
||||
let session_id = session.session_id();
|
||||
let first_known_index = session.first_known_index();
|
||||
|
||||
let mut keys: BTreeMap<DeviceKeyAlgorithm, String> = BTreeMap::new();
|
||||
keys.insert(DeviceKeyAlgorithm::Ed25519, signing_key.to_owned());
|
||||
|
||||
Ok(InboundGroupSession {
|
||||
inner: Arc::new(Mutex::new(session)),
|
||||
session_id: session_id.into(),
|
||||
sender_key: sender_key.to_owned().into(),
|
||||
first_known_index,
|
||||
signing_key: Arc::new(keys),
|
||||
room_id: Arc::new(room_id.clone()),
|
||||
forwarding_chains: Arc::new(Mutex::new(None)),
|
||||
imported: Arc::new(false),
|
||||
})
|
||||
}
|
||||
|
||||
/// Create a InboundGroupSession from an exported version of the group
|
||||
/// session.
|
||||
///
|
||||
/// Most notably this can be called with an `ExportedRoomKey` from a
|
||||
/// previous [`export()`] call.
|
||||
///
|
||||
///
|
||||
/// [`export()`]: #method.export
|
||||
pub fn from_export(
|
||||
exported_session: impl Into<ExportedRoomKey>,
|
||||
) -> Result<Self, OlmGroupSessionError> {
|
||||
Self::try_from(exported_session.into())
|
||||
}
|
||||
|
||||
/// Create a new inbound group session from a forwarded room key content.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `sender_key` - The public curve25519 key of the account that
|
||||
/// sent us the session
|
||||
///
|
||||
/// * `content` - A forwarded room key content that contains the session key
|
||||
/// to create the `InboundGroupSession`.
|
||||
pub(crate) fn from_forwarded_key(
|
||||
sender_key: &str,
|
||||
content: &mut ForwardedRoomKeyToDeviceEventContent,
|
||||
) -> Result<Self, OlmGroupSessionError> {
|
||||
let key = Zeroizing::from(mem::take(&mut content.session_key));
|
||||
|
||||
let session = OlmInboundGroupSession::import(&key)?;
|
||||
let first_known_index = session.first_known_index();
|
||||
let mut forwarding_chains = content.forwarding_curve25519_key_chain.clone();
|
||||
forwarding_chains.push(sender_key.to_owned());
|
||||
|
||||
let mut sender_claimed_key = BTreeMap::new();
|
||||
sender_claimed_key.insert(
|
||||
DeviceKeyAlgorithm::Ed25519,
|
||||
content.sender_claimed_ed25519_key.to_owned(),
|
||||
);
|
||||
|
||||
Ok(InboundGroupSession {
|
||||
inner: Arc::new(Mutex::new(session)),
|
||||
session_id: content.session_id.as_str().into(),
|
||||
sender_key: content.sender_key.as_str().into(),
|
||||
first_known_index,
|
||||
signing_key: Arc::new(sender_claimed_key),
|
||||
room_id: Arc::new(content.room_id.clone()),
|
||||
forwarding_chains: Arc::new(Mutex::new(Some(forwarding_chains))),
|
||||
imported: Arc::new(true),
|
||||
})
|
||||
}
|
||||
|
||||
/// Store the group session as a base64 encoded string.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pickle_mode` - The mode that was used to pickle the group session,
|
||||
/// either an unencrypted mode or an encrypted using passphrase.
|
||||
pub async fn pickle(&self, pickle_mode: PicklingMode) -> PickledInboundGroupSession {
|
||||
let pickle = self.inner.lock().await.pickle(pickle_mode);
|
||||
|
||||
PickledInboundGroupSession {
|
||||
pickle: InboundGroupSessionPickle::from(pickle),
|
||||
sender_key: self.sender_key.to_string(),
|
||||
signing_key: (&*self.signing_key).clone(),
|
||||
room_id: (&*self.room_id).clone(),
|
||||
forwarding_chains: self.forwarding_chains.lock().await.clone(),
|
||||
imported: *self.imported,
|
||||
}
|
||||
}
|
||||
|
||||
/// Export this session at the first known message index.
|
||||
///
|
||||
/// If only a limited part of this session should be exported use
|
||||
/// [`export_at_index()`](#method.export_at_index).
|
||||
pub async fn export(&self) -> ExportedRoomKey {
|
||||
self.export_at_index(self.first_known_index())
|
||||
.await
|
||||
.expect("Can't export at the first known index")
|
||||
}
|
||||
|
||||
/// Export this session at the given message index.
|
||||
pub async fn export_at_index(&self, message_index: u32) -> Option<ExportedRoomKey> {
|
||||
let session_key =
|
||||
ExportedGroupSessionKey(self.inner.lock().await.export(message_index).ok()?);
|
||||
|
||||
Some(ExportedRoomKey {
|
||||
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
room_id: (&*self.room_id).clone(),
|
||||
sender_key: (&*self.sender_key).to_owned(),
|
||||
session_id: self.session_id().to_owned(),
|
||||
forwarding_curve25519_key_chain: self
|
||||
.forwarding_chains
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.unwrap_or_default(),
|
||||
sender_claimed_keys: (&*self.signing_key).clone(),
|
||||
session_key,
|
||||
})
|
||||
}
|
||||
|
||||
/// Restore a Session from a previously pickled string.
|
||||
///
|
||||
/// Returns the restored group session or a `OlmGroupSessionError` if there
|
||||
/// was an error.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pickle` - The pickled version of the `InboundGroupSession`.
|
||||
///
|
||||
/// * `pickle_mode` - The mode that was used to pickle the session, either
|
||||
/// an unencrypted mode or an encrypted using passphrase.
|
||||
pub fn from_pickle(
|
||||
pickle: PickledInboundGroupSession,
|
||||
pickle_mode: PicklingMode,
|
||||
) -> Result<Self, OlmGroupSessionError> {
|
||||
let session = OlmInboundGroupSession::unpickle(pickle.pickle.0, pickle_mode)?;
|
||||
let first_known_index = session.first_known_index();
|
||||
let session_id = session.session_id();
|
||||
|
||||
Ok(InboundGroupSession {
|
||||
inner: Arc::new(Mutex::new(session)),
|
||||
session_id: session_id.into(),
|
||||
sender_key: pickle.sender_key.into(),
|
||||
first_known_index,
|
||||
signing_key: Arc::new(pickle.signing_key),
|
||||
room_id: Arc::new(pickle.room_id),
|
||||
forwarding_chains: Arc::new(Mutex::new(pickle.forwarding_chains)),
|
||||
imported: Arc::new(pickle.imported),
|
||||
})
|
||||
}
|
||||
|
||||
/// The room where this session is used in.
|
||||
pub fn room_id(&self) -> &RoomId {
|
||||
&self.room_id
|
||||
}
|
||||
|
||||
/// Returns the unique identifier for this session.
|
||||
pub fn session_id(&self) -> &str {
|
||||
&self.session_id
|
||||
}
|
||||
|
||||
/// Get the first message index we know how to decrypt.
|
||||
pub fn first_known_index(&self) -> u32 {
|
||||
self.first_known_index
|
||||
}
|
||||
|
||||
/// Decrypt the given ciphertext.
|
||||
///
|
||||
/// Returns the decrypted plaintext or an `OlmGroupSessionError` if
|
||||
/// decryption failed.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `message` - The message that should be decrypted.
|
||||
pub(crate) async fn decrypt_helper(
|
||||
&self,
|
||||
message: String,
|
||||
) -> Result<(String, u32), OlmGroupSessionError> {
|
||||
self.inner.lock().await.decrypt(message)
|
||||
}
|
||||
|
||||
/// Decrypt an event from a room timeline.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `event` - The event that should be decrypted.
|
||||
pub(crate) async fn decrypt(
|
||||
&self,
|
||||
event: &SyncMessageEvent<EncryptedEventContent>,
|
||||
) -> MegolmResult<(Raw<AnySyncRoomEvent>, u32)> {
|
||||
let content = match &event.content {
|
||||
EncryptedEventContent::MegolmV1AesSha2(c) => c,
|
||||
_ => return Err(EventError::UnsupportedAlgorithm.into()),
|
||||
};
|
||||
|
||||
let (plaintext, message_index) = self.decrypt_helper(content.ciphertext.clone()).await?;
|
||||
|
||||
let mut decrypted_value = serde_json::from_str::<Value>(&plaintext)?;
|
||||
let decrypted_object = decrypted_value
|
||||
.as_object_mut()
|
||||
.ok_or(EventError::NotAnObject)?;
|
||||
|
||||
// TODO better number conversion here.
|
||||
let server_ts = event
|
||||
.origin_server_ts
|
||||
.duration_since(std::time::SystemTime::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis();
|
||||
let server_ts: i64 = server_ts.try_into().unwrap_or_default();
|
||||
|
||||
decrypted_object.insert("sender".to_owned(), event.sender.to_string().into());
|
||||
decrypted_object.insert("event_id".to_owned(), event.event_id.to_string().into());
|
||||
decrypted_object.insert("origin_server_ts".to_owned(), server_ts.into());
|
||||
|
||||
decrypted_object.insert(
|
||||
"unsigned".to_owned(),
|
||||
serde_json::to_value(&event.unsigned).unwrap_or_default(),
|
||||
);
|
||||
|
||||
if let Some(decrypted_content) = decrypted_object
|
||||
.get_mut("content")
|
||||
.map(|c| c.as_object_mut())
|
||||
.flatten()
|
||||
{
|
||||
if !decrypted_content.contains_key("m.relates_to") {
|
||||
if let Some(relation) = &content.relates_to {
|
||||
decrypted_content.insert(
|
||||
"m.relates_to".to_owned(),
|
||||
serde_json::to_value(relation).unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok((
|
||||
serde_json::from_value::<Raw<AnySyncRoomEvent>>(decrypted_value)?,
|
||||
message_index,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for InboundGroupSession {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("InboundGroupSession")
|
||||
.field("session_id", &self.session_id())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for InboundGroupSession {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.session_id() == other.session_id()
|
||||
}
|
||||
}
|
||||
|
||||
/// A pickled version of an `InboundGroupSession`.
|
||||
///
|
||||
/// Holds all the information that needs to be stored in a database to restore
|
||||
/// an InboundGroupSession.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PickledInboundGroupSession {
|
||||
/// The pickle string holding the InboundGroupSession.
|
||||
pub pickle: InboundGroupSessionPickle,
|
||||
/// The public curve25519 key of the account that sent us the session
|
||||
pub sender_key: String,
|
||||
/// The public ed25519 key of the account that sent us the session.
|
||||
pub signing_key: BTreeMap<DeviceKeyAlgorithm, String>,
|
||||
/// The id of the room that the session is used in.
|
||||
pub room_id: RoomId,
|
||||
/// The list of claimed ed25519 that forwarded us this key. Will be None if
|
||||
/// we dirrectly received this session.
|
||||
pub forwarding_chains: Option<Vec<String>>,
|
||||
/// Flag remembering if the session was dirrectly sent to us by the sender
|
||||
/// or if it was imported.
|
||||
pub imported: bool,
|
||||
}
|
||||
|
||||
/// The typed representation of a base64 encoded string of the GroupSession pickle.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct InboundGroupSessionPickle(String);
|
||||
|
||||
impl From<String> for InboundGroupSessionPickle {
|
||||
fn from(pickle_string: String) -> Self {
|
||||
InboundGroupSessionPickle(pickle_string)
|
||||
}
|
||||
}
|
||||
|
||||
impl InboundGroupSessionPickle {
|
||||
/// Get the string representation of the pickle.
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ExportedRoomKey> for InboundGroupSession {
|
||||
type Error = OlmGroupSessionError;
|
||||
|
||||
fn try_from(key: ExportedRoomKey) -> Result<Self, Self::Error> {
|
||||
let session = OlmInboundGroupSession::import(&key.session_key.0)?;
|
||||
let first_known_index = session.first_known_index();
|
||||
|
||||
let forwarding_chains = if key.forwarding_curve25519_key_chain.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(key.forwarding_curve25519_key_chain)
|
||||
};
|
||||
|
||||
Ok(InboundGroupSession {
|
||||
inner: Arc::new(Mutex::new(session)),
|
||||
session_id: key.session_id.into(),
|
||||
sender_key: key.sender_key.into(),
|
||||
first_known_index,
|
||||
signing_key: Arc::new(key.sender_claimed_keys),
|
||||
room_id: Arc::new(key.room_id),
|
||||
forwarding_chains: Arc::new(Mutex::new(forwarding_chains)),
|
||||
imported: Arc::new(true),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
// 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.
|
||||
|
||||
use matrix_sdk_common::{
|
||||
events::forwarded_room_key::ForwardedRoomKeyToDeviceEventContent,
|
||||
identifiers::{DeviceKeyAlgorithm, EventEncryptionAlgorithm, RoomId},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{collections::BTreeMap, convert::TryInto};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
mod inbound;
|
||||
mod outbound;
|
||||
|
||||
pub use inbound::{InboundGroupSession, InboundGroupSessionPickle, PickledInboundGroupSession};
|
||||
pub use outbound::{EncryptionSettings, OutboundGroupSession};
|
||||
|
||||
/// The private session key of a group session.
|
||||
/// Can be used to create a new inbound group session.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
pub struct GroupSessionKey(pub String);
|
||||
|
||||
/// The exported version of an private session key of a group session.
|
||||
/// Can be used to create a new inbound group session.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
pub struct ExportedGroupSessionKey(pub String);
|
||||
|
||||
/// An exported version of a `InboundGroupSession`
|
||||
///
|
||||
/// This can be used to share the `InboundGroupSession` in an exported file.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct ExportedRoomKey {
|
||||
/// The encryption algorithm that the session uses.
|
||||
pub algorithm: EventEncryptionAlgorithm,
|
||||
|
||||
/// The room where the session is used.
|
||||
pub room_id: RoomId,
|
||||
|
||||
/// The Curve25519 key of the device which initiated the session originally.
|
||||
pub sender_key: String,
|
||||
|
||||
/// The ID of the session that the key is for.
|
||||
pub session_id: String,
|
||||
|
||||
/// The key for the session.
|
||||
pub session_key: ExportedGroupSessionKey,
|
||||
|
||||
/// The Ed25519 key of the device which initiated the session originally.
|
||||
pub sender_claimed_keys: BTreeMap<DeviceKeyAlgorithm, String>,
|
||||
|
||||
/// Chain of Curve25519 keys through which this session was forwarded, via
|
||||
/// m.forwarded_room_key events.
|
||||
pub forwarding_curve25519_key_chain: Vec<String>,
|
||||
}
|
||||
|
||||
impl TryInto<ForwardedRoomKeyToDeviceEventContent> for ExportedRoomKey {
|
||||
type Error = ();
|
||||
|
||||
/// Convert an exported room key into a content for a forwarded room key
|
||||
/// event.
|
||||
///
|
||||
/// This will fail if the exported room key has multiple sender claimed keys
|
||||
/// or if the algorithm of the claimed sender key isn't
|
||||
/// `DeviceKeyAlgorithm::Ed25519`.
|
||||
fn try_into(self) -> Result<ForwardedRoomKeyToDeviceEventContent, Self::Error> {
|
||||
if self.sender_claimed_keys.len() != 1 {
|
||||
Err(())
|
||||
} else {
|
||||
let (algorithm, claimed_key) = self.sender_claimed_keys.iter().next().ok_or(())?;
|
||||
|
||||
if algorithm != &DeviceKeyAlgorithm::Ed25519 {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
Ok(ForwardedRoomKeyToDeviceEventContent {
|
||||
algorithm: self.algorithm,
|
||||
room_id: self.room_id,
|
||||
sender_key: self.sender_key,
|
||||
session_id: self.session_id,
|
||||
session_key: self.session_key.0.clone(),
|
||||
sender_claimed_ed25519_key: claimed_key.to_owned(),
|
||||
forwarding_curve25519_key_chain: self.forwarding_curve25519_key_chain,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ForwardedRoomKeyToDeviceEventContent> for ExportedRoomKey {
|
||||
/// Convert the content of a forwarded room key into a exported room key.
|
||||
fn from(forwarded_key: ForwardedRoomKeyToDeviceEventContent) -> Self {
|
||||
let mut sender_claimed_keys: BTreeMap<DeviceKeyAlgorithm, String> = BTreeMap::new();
|
||||
sender_claimed_keys.insert(
|
||||
DeviceKeyAlgorithm::Ed25519,
|
||||
forwarded_key.sender_claimed_ed25519_key,
|
||||
);
|
||||
|
||||
Self {
|
||||
algorithm: forwarded_key.algorithm,
|
||||
room_id: forwarded_key.room_id,
|
||||
session_id: forwarded_key.session_id,
|
||||
forwarding_curve25519_key_chain: forwarded_key.forwarding_curve25519_key_chain,
|
||||
sender_claimed_keys,
|
||||
sender_key: forwarded_key.sender_key,
|
||||
session_key: ExportedGroupSessionKey(forwarded_key.session_key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
events::{
|
||||
room::message::{MessageEventContent, TextMessageEventContent},
|
||||
AnyMessageEventContent,
|
||||
},
|
||||
identifiers::{room_id, user_id},
|
||||
};
|
||||
|
||||
use super::EncryptionSettings;
|
||||
use crate::ReadOnlyAccount;
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn expiration() {
|
||||
let settings = EncryptionSettings {
|
||||
rotation_period_msgs: 1,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let account = ReadOnlyAccount::new(&user_id!("@alice:example.org"), "DEVICEID".into());
|
||||
let (session, _) = account
|
||||
.create_group_session_pair(&room_id!("!test_room:example.org"), settings)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!session.expired());
|
||||
let _ = session
|
||||
.encrypt(AnyMessageEventContent::RoomMessage(
|
||||
MessageEventContent::Text(TextMessageEventContent::plain("Test message")),
|
||||
))
|
||||
.await;
|
||||
assert!(session.expired());
|
||||
|
||||
let settings = EncryptionSettings {
|
||||
rotation_period: Duration::from_millis(100),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let (mut session, _) = account
|
||||
.create_group_session_pair(&room_id!("!test_room:example.org"), settings)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!session.expired());
|
||||
session.creation_time = Arc::new(Instant::now() - Duration::from_secs(60 * 60));
|
||||
assert!(session.expired());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
// 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.
|
||||
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use matrix_sdk_common::{api::r0::to_device::DeviceIdOrAllDevices, uuid::Uuid};
|
||||
use std::{
|
||||
cmp::min,
|
||||
fmt,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicU64, Ordering},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use tracing::debug;
|
||||
|
||||
use matrix_sdk_common::{
|
||||
events::{
|
||||
room::{encrypted::EncryptedEventContent, encryption::EncryptionEventContent},
|
||||
AnyMessageEventContent, EventContent,
|
||||
},
|
||||
identifiers::{DeviceId, DeviceIdBox, EventEncryptionAlgorithm, RoomId, UserId},
|
||||
instant::Instant,
|
||||
locks::Mutex,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use olm_rs::outbound_group_session::OlmOutboundGroupSession;
|
||||
pub use olm_rs::{
|
||||
account::IdentityKeys,
|
||||
session::{OlmMessage, PreKeyMessage},
|
||||
utility::OlmUtility,
|
||||
};
|
||||
|
||||
use crate::ToDeviceRequest;
|
||||
|
||||
use super::GroupSessionKey;
|
||||
|
||||
const ROTATION_PERIOD: Duration = Duration::from_millis(604800000);
|
||||
const ROTATION_MESSAGES: u64 = 100;
|
||||
|
||||
/// Settings for an encrypted room.
|
||||
///
|
||||
/// This determines the algorithm and rotation periods of a group session.
|
||||
#[derive(Debug)]
|
||||
pub struct EncryptionSettings {
|
||||
/// The encryption algorithm that should be used in the room.
|
||||
pub algorithm: EventEncryptionAlgorithm,
|
||||
/// How long the session should be used before changing it.
|
||||
pub rotation_period: Duration,
|
||||
/// How many messages should be sent before changing the session.
|
||||
pub rotation_period_msgs: u64,
|
||||
}
|
||||
|
||||
impl Default for EncryptionSettings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
algorithm: EventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
rotation_period: ROTATION_PERIOD,
|
||||
rotation_period_msgs: ROTATION_MESSAGES,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&EncryptionEventContent> for EncryptionSettings {
|
||||
fn from(content: &EncryptionEventContent) -> Self {
|
||||
let rotation_period: Duration = content
|
||||
.rotation_period_ms
|
||||
.map_or(ROTATION_PERIOD, |r| Duration::from_millis(r.into()));
|
||||
let rotation_period_msgs: u64 = content
|
||||
.rotation_period_msgs
|
||||
.map_or(ROTATION_MESSAGES, Into::into);
|
||||
|
||||
Self {
|
||||
algorithm: content.algorithm.clone(),
|
||||
rotation_period,
|
||||
rotation_period_msgs,
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Outbound group session.
|
||||
///
|
||||
/// Outbound group sessions are used to exchange room messages between a group
|
||||
/// of participants. Outbound group sessions are used to encrypt the room
|
||||
/// messages.
|
||||
#[derive(Clone)]
|
||||
pub struct OutboundGroupSession {
|
||||
inner: Arc<Mutex<OlmOutboundGroupSession>>,
|
||||
device_id: Arc<DeviceIdBox>,
|
||||
account_identity_keys: Arc<IdentityKeys>,
|
||||
session_id: Arc<str>,
|
||||
room_id: Arc<RoomId>,
|
||||
pub(crate) creation_time: Arc<Instant>,
|
||||
message_count: Arc<AtomicU64>,
|
||||
shared: Arc<AtomicBool>,
|
||||
invalidated: Arc<AtomicBool>,
|
||||
settings: Arc<EncryptionSettings>,
|
||||
shared_with_set: Arc<DashMap<UserId, DashSet<DeviceIdBox>>>,
|
||||
to_share_with_set: Arc<DashMap<Uuid, Arc<ToDeviceRequest>>>,
|
||||
}
|
||||
|
||||
impl OutboundGroupSession {
|
||||
/// Create a new outbound group session for the given room.
|
||||
///
|
||||
/// Outbound group sessions are used to encrypt room messages.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `device_id` - The id of the device that created this session.
|
||||
///
|
||||
/// * `identity_keys` - The identity keys of the account that created this
|
||||
/// session.
|
||||
///
|
||||
/// * `room_id` - The id of the room that the session is used in.
|
||||
///
|
||||
/// * `settings` - Settings determining the algorithm and rotation period of
|
||||
/// the outbound group session.
|
||||
pub fn new(
|
||||
device_id: Arc<DeviceIdBox>,
|
||||
identity_keys: Arc<IdentityKeys>,
|
||||
room_id: &RoomId,
|
||||
settings: EncryptionSettings,
|
||||
) -> Self {
|
||||
let session = OlmOutboundGroupSession::new();
|
||||
let session_id = session.session_id();
|
||||
|
||||
OutboundGroupSession {
|
||||
inner: Arc::new(Mutex::new(session)),
|
||||
room_id: Arc::new(room_id.to_owned()),
|
||||
device_id,
|
||||
account_identity_keys: identity_keys,
|
||||
session_id: session_id.into(),
|
||||
creation_time: Arc::new(Instant::now()),
|
||||
message_count: Arc::new(AtomicU64::new(0)),
|
||||
shared: Arc::new(AtomicBool::new(false)),
|
||||
invalidated: Arc::new(AtomicBool::new(false)),
|
||||
settings: Arc::new(settings),
|
||||
shared_with_set: Arc::new(DashMap::new()),
|
||||
to_share_with_set: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_request(&self, request_id: Uuid, request: Arc<ToDeviceRequest>) {
|
||||
self.to_share_with_set.insert(request_id, request);
|
||||
}
|
||||
|
||||
pub fn add_recipient(&self, user_id: &UserId) {
|
||||
self.shared_with_set
|
||||
.entry(user_id.to_owned())
|
||||
.or_insert_with(DashSet::new);
|
||||
}
|
||||
|
||||
pub fn contains_recipient(&self, user_id: &UserId) -> bool {
|
||||
self.shared_with_set.contains_key(user_id)
|
||||
}
|
||||
|
||||
/// Mark the request with the given request id as sent.
|
||||
///
|
||||
/// This removes the request from the queue and marks the set of
|
||||
/// users/devices that received the session.
|
||||
pub fn mark_request_as_sent(&self, request_id: &Uuid) {
|
||||
if let Some((_, r)) = self.to_share_with_set.remove(request_id) {
|
||||
let user_pairs = r.messages.iter().map(|(u, v)| {
|
||||
(
|
||||
u.clone(),
|
||||
v.keys().filter_map(|d| {
|
||||
if let DeviceIdOrAllDevices::DeviceId(d) = d {
|
||||
Some(d.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
user_pairs.for_each(|(u, d)| {
|
||||
self.shared_with_set
|
||||
.entry(u)
|
||||
.or_insert_with(DashSet::new)
|
||||
.extend(d);
|
||||
});
|
||||
|
||||
if self.to_share_with_set.is_empty() {
|
||||
debug!(
|
||||
"Marking session {} for room {} as shared.",
|
||||
self.session_id(),
|
||||
self.room_id
|
||||
);
|
||||
self.mark_as_shared();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Encrypt the given plaintext using this session.
|
||||
///
|
||||
/// Returns the encrypted ciphertext.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `plaintext` - The plaintext that should be encrypted.
|
||||
pub(crate) async fn encrypt_helper(&self, plaintext: String) -> String {
|
||||
let session = self.inner.lock().await;
|
||||
self.message_count.fetch_add(1, Ordering::SeqCst);
|
||||
session.encrypt(plaintext)
|
||||
}
|
||||
|
||||
/// Encrypt a room message for the given room.
|
||||
///
|
||||
/// Beware that a group session needs to be shared before this method can be
|
||||
/// called using the `share_group_session()` method.
|
||||
///
|
||||
/// Since group sessions can expire or become invalid if the room membership
|
||||
/// changes client authors should check with the
|
||||
/// `should_share_group_session()` method if a new group session needs to
|
||||
/// be shared.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `content` - The plaintext content of the message that should be
|
||||
/// encrypted.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the content can't be serialized.
|
||||
pub async fn encrypt(&self, content: AnyMessageEventContent) -> EncryptedEventContent {
|
||||
let json_content = json!({
|
||||
"content": content,
|
||||
"room_id": &*self.room_id,
|
||||
"type": content.event_type(),
|
||||
});
|
||||
|
||||
let plaintext = json_content.to_string();
|
||||
|
||||
let ciphertext = self.encrypt_helper(plaintext).await;
|
||||
|
||||
EncryptedEventContent::MegolmV1AesSha2(
|
||||
matrix_sdk_common::events::room::encrypted::MegolmV1AesSha2ContentInit {
|
||||
ciphertext,
|
||||
sender_key: self.account_identity_keys.curve25519().to_owned(),
|
||||
session_id: self.session_id().to_owned(),
|
||||
device_id: (&*self.device_id).to_owned(),
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Check if the session has expired and if it should be rotated.
|
||||
///
|
||||
/// A session will expire after some time or if enough messages have been
|
||||
/// encrypted using it.
|
||||
pub fn expired(&self) -> bool {
|
||||
let count = self.message_count.load(Ordering::SeqCst);
|
||||
|
||||
count >= self.settings.rotation_period_msgs
|
||||
|| self.creation_time.elapsed()
|
||||
// Since the encryption settings are provided by users and not
|
||||
// checked someone could set a really low rotation perdiod so
|
||||
// clamp it at a minute.
|
||||
>= min(self.settings.rotation_period, Duration::from_secs(3600))
|
||||
}
|
||||
|
||||
/// Has the session been invalidated.
|
||||
pub fn invalidated(&self) -> bool {
|
||||
self.invalidated.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Mark the session as shared.
|
||||
///
|
||||
/// Messages shouldn't be encrypted with the session before it has been
|
||||
/// shared.
|
||||
pub fn mark_as_shared(&self) {
|
||||
self.shared.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Check if the session has been marked as shared.
|
||||
pub fn shared(&self) -> bool {
|
||||
self.shared.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Get the session key of this session.
|
||||
///
|
||||
/// A session key can be used to to create an `InboundGroupSession`.
|
||||
pub async fn session_key(&self) -> GroupSessionKey {
|
||||
let session = self.inner.lock().await;
|
||||
GroupSessionKey(session.session_key())
|
||||
}
|
||||
|
||||
/// Get the room id of the room this session belongs to.
|
||||
pub fn room_id(&self) -> &RoomId {
|
||||
&self.room_id
|
||||
}
|
||||
|
||||
/// Returns the unique identifier for this session.
|
||||
pub fn session_id(&self) -> &str {
|
||||
&self.session_id
|
||||
}
|
||||
|
||||
/// Get the current message index for this session.
|
||||
///
|
||||
/// Each message is sent with an increasing index. This returns the
|
||||
/// message index that will be used for the next encrypted message.
|
||||
pub async fn message_index(&self) -> u32 {
|
||||
let session = self.inner.lock().await;
|
||||
session.session_message_index()
|
||||
}
|
||||
|
||||
/// Get the outbound group session key as a json value that can be sent as a
|
||||
/// m.room_key.
|
||||
pub async fn as_json(&self) -> Value {
|
||||
json!({
|
||||
"algorithm": EventEncryptionAlgorithm::MegolmV1AesSha2,
|
||||
"room_id": &*self.room_id,
|
||||
"session_id": &*self.session_id,
|
||||
"session_key": self.session_key().await,
|
||||
"chain_index": self.message_index().await,
|
||||
})
|
||||
}
|
||||
|
||||
/// Mark the session as invalid.
|
||||
///
|
||||
/// This should be called if an user/device deletes a device that received
|
||||
/// this session.
|
||||
pub fn invalidate_session(&self) {
|
||||
self.invalidated.store(true, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// Clear out the requests returning the request ids.
|
||||
pub fn clear_requests(&self) -> Vec<Uuid> {
|
||||
let request_ids = self
|
||||
.to_share_with_set
|
||||
.iter()
|
||||
.map(|item| *item.key())
|
||||
.collect();
|
||||
self.to_share_with_set.clear();
|
||||
request_ids
|
||||
}
|
||||
|
||||
/// Has or will the session be shared with the given user/device pair.
|
||||
pub(crate) fn is_shared_with(&self, user_id: &UserId, device_id: &DeviceId) -> bool {
|
||||
let shared_with = self
|
||||
.shared_with_set
|
||||
.get(user_id)
|
||||
.map(|d| d.contains(device_id))
|
||||
.unwrap_or(false);
|
||||
|
||||
let should_be_shared_with = if self.shared() {
|
||||
false
|
||||
} else {
|
||||
let device_id = DeviceIdOrAllDevices::DeviceId(device_id.into());
|
||||
|
||||
self.to_share_with_set.iter().any(|item| {
|
||||
if let Some(e) = item.value().messages.get(user_id) {
|
||||
e.contains_key(&device_id)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
shared_with || should_be_shared_with
|
||||
}
|
||||
|
||||
/// Mark that the session was shared with the given user/device pair.
|
||||
#[cfg(test)]
|
||||
pub fn mark_shared_with(&self, user_id: &UserId, device_id: &DeviceId) {
|
||||
self.shared_with_set
|
||||
.entry(user_id.to_owned())
|
||||
.or_insert_with(DashSet::new)
|
||||
.insert(device_id.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl std::fmt::Debug for OutboundGroupSession {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("OutboundGroupSession")
|
||||
.field("session_id", &self.session_id)
|
||||
.field("room_id", &self.room_id)
|
||||
.field("creation_time", &self.creation_time)
|
||||
.field("message_count", &self.message_count)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::time::Duration;
|
||||
|
||||
use matrix_sdk_common::{
|
||||
events::room::encryption::EncryptionEventContent, identifiers::EventEncryptionAlgorithm,
|
||||
uint,
|
||||
};
|
||||
|
||||
use super::{EncryptionSettings, ROTATION_MESSAGES, ROTATION_PERIOD};
|
||||
|
||||
#[test]
|
||||
fn encryption_settings_conversion() {
|
||||
let mut content = EncryptionEventContent::new(EventEncryptionAlgorithm::MegolmV1AesSha2);
|
||||
let settings = EncryptionSettings::from(&content);
|
||||
|
||||
assert_eq!(settings.rotation_period, ROTATION_PERIOD);
|
||||
assert_eq!(settings.rotation_period_msgs, ROTATION_MESSAGES);
|
||||
|
||||
content.rotation_period_ms = Some(uint!(3600));
|
||||
content.rotation_period_msgs = Some(uint!(500));
|
||||
|
||||
let settings = EncryptionSettings::from(&content);
|
||||
|
||||
assert_eq!(settings.rotation_period, Duration::from_millis(3600));
|
||||
assert_eq!(settings.rotation_period_msgs, 500);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
// 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.
|
||||
|
||||
//! The crypto specific Olm objects.
|
||||
//!
|
||||
//! Note: You'll only be interested in these if you are implementing a custom
|
||||
//! `CryptoStore`.
|
||||
|
||||
mod account;
|
||||
mod group_sessions;
|
||||
mod session;
|
||||
mod signing;
|
||||
mod utility;
|
||||
|
||||
pub(crate) use account::{Account, OlmDecryptionInfo, SessionType};
|
||||
pub use account::{AccountPickle, OlmMessageHash, PickledAccount, ReadOnlyAccount};
|
||||
pub use group_sessions::{
|
||||
EncryptionSettings, ExportedRoomKey, InboundGroupSession, InboundGroupSessionPickle,
|
||||
PickledInboundGroupSession,
|
||||
};
|
||||
pub(crate) use group_sessions::{GroupSessionKey, OutboundGroupSession};
|
||||
pub use olm_rs::{account::IdentityKeys, PicklingMode};
|
||||
pub use session::{PickledSession, Session, SessionPickle};
|
||||
pub use signing::{PickledCrossSigningIdentity, PrivateCrossSigningIdentity};
|
||||
pub(crate) use utility::Utility;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use crate::olm::{InboundGroupSession, ReadOnlyAccount, Session};
|
||||
use matrix_sdk_common::{
|
||||
api::r0::keys::SignedKey,
|
||||
events::forwarded_room_key::ForwardedRoomKeyToDeviceEventContent,
|
||||
identifiers::{room_id, user_id, DeviceId, UserId},
|
||||
};
|
||||
use olm_rs::session::OlmMessage;
|
||||
use std::{collections::BTreeMap, convert::TryInto};
|
||||
|
||||
fn alice_id() -> UserId {
|
||||
user_id!("@alice:example.org")
|
||||
}
|
||||
|
||||
fn alice_device_id() -> Box<DeviceId> {
|
||||
"ALICEDEVICE".into()
|
||||
}
|
||||
|
||||
fn bob_id() -> UserId {
|
||||
user_id!("@bob:example.org")
|
||||
}
|
||||
|
||||
fn bob_device_id() -> Box<DeviceId> {
|
||||
"BOBDEVICE".into()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_account_and_session() -> (ReadOnlyAccount, Session) {
|
||||
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
|
||||
let bob = ReadOnlyAccount::new(&bob_id(), &bob_device_id());
|
||||
|
||||
bob.generate_one_time_keys_helper(1).await;
|
||||
let one_time_key = bob
|
||||
.one_time_keys()
|
||||
.await
|
||||
.curve25519()
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.1
|
||||
.to_owned();
|
||||
let one_time_key = SignedKey {
|
||||
key: one_time_key,
|
||||
signatures: BTreeMap::new(),
|
||||
};
|
||||
let sender_key = bob.identity_keys().curve25519().to_owned();
|
||||
let session = alice
|
||||
.create_outbound_session_helper(&sender_key, &one_time_key)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
(alice, session)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn account_creation() {
|
||||
let account = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
|
||||
let identyty_keys = account.identity_keys();
|
||||
|
||||
assert!(!account.shared());
|
||||
assert!(!identyty_keys.ed25519().is_empty());
|
||||
assert_ne!(identyty_keys.values().len(), 0);
|
||||
assert_ne!(identyty_keys.keys().len(), 0);
|
||||
assert_ne!(identyty_keys.iter().len(), 0);
|
||||
assert!(identyty_keys.contains_key("ed25519"));
|
||||
assert_eq!(
|
||||
identyty_keys.ed25519(),
|
||||
identyty_keys.get("ed25519").unwrap()
|
||||
);
|
||||
assert!(!identyty_keys.curve25519().is_empty());
|
||||
|
||||
account.mark_as_shared();
|
||||
assert!(account.shared());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn one_time_keys_creation() {
|
||||
let account = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
|
||||
let one_time_keys = account.one_time_keys().await;
|
||||
|
||||
assert!(one_time_keys.curve25519().is_empty());
|
||||
assert_ne!(account.max_one_time_keys().await, 0);
|
||||
|
||||
account.generate_one_time_keys_helper(10).await;
|
||||
let one_time_keys = account.one_time_keys().await;
|
||||
|
||||
assert!(!one_time_keys.curve25519().is_empty());
|
||||
assert_ne!(one_time_keys.values().len(), 0);
|
||||
assert_ne!(one_time_keys.keys().len(), 0);
|
||||
assert_ne!(one_time_keys.iter().len(), 0);
|
||||
assert!(one_time_keys.contains_key("curve25519"));
|
||||
assert_eq!(one_time_keys.curve25519().keys().len(), 10);
|
||||
assert_eq!(
|
||||
one_time_keys.curve25519(),
|
||||
one_time_keys.get("curve25519").unwrap()
|
||||
);
|
||||
|
||||
account.mark_keys_as_published().await;
|
||||
let one_time_keys = account.one_time_keys().await;
|
||||
assert!(one_time_keys.curve25519().is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn session_creation() {
|
||||
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
|
||||
let bob = ReadOnlyAccount::new(&bob_id(), &bob_device_id());
|
||||
let alice_keys = alice.identity_keys();
|
||||
alice.generate_one_time_keys_helper(1).await;
|
||||
let one_time_keys = alice.one_time_keys().await;
|
||||
alice.mark_keys_as_published().await;
|
||||
|
||||
let one_time_key = one_time_keys
|
||||
.curve25519()
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.1
|
||||
.to_owned();
|
||||
|
||||
let one_time_key = SignedKey {
|
||||
key: one_time_key,
|
||||
signatures: BTreeMap::new(),
|
||||
};
|
||||
|
||||
let mut bob_session = bob
|
||||
.create_outbound_session_helper(alice_keys.curve25519(), &one_time_key)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let plaintext = "Hello world";
|
||||
|
||||
let message = bob_session.encrypt_helper(plaintext).await;
|
||||
|
||||
let prekey_message = match message.clone() {
|
||||
OlmMessage::PreKey(m) => m,
|
||||
OlmMessage::Message(_) => panic!("Incorrect message type"),
|
||||
};
|
||||
|
||||
let bob_keys = bob.identity_keys();
|
||||
let mut alice_session = alice
|
||||
.create_inbound_session(bob_keys.curve25519(), prekey_message.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(alice_session
|
||||
.matches(bob_keys.curve25519(), prekey_message)
|
||||
.await
|
||||
.unwrap());
|
||||
|
||||
assert_eq!(bob_session.session_id(), alice_session.session_id());
|
||||
|
||||
let decyrpted = alice_session.decrypt(message).await.unwrap();
|
||||
assert_eq!(plaintext, decyrpted);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn group_session_creation() {
|
||||
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
let (outbound, _) = alice
|
||||
.create_group_session_pair_with_defaults(&room_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(0, outbound.message_index().await);
|
||||
assert!(!outbound.shared());
|
||||
outbound.mark_as_shared();
|
||||
assert!(outbound.shared());
|
||||
|
||||
let inbound = InboundGroupSession::new(
|
||||
"test_key",
|
||||
"test_key",
|
||||
&room_id,
|
||||
outbound.session_key().await,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(0, inbound.first_known_index());
|
||||
|
||||
assert_eq!(outbound.session_id(), inbound.session_id());
|
||||
|
||||
let plaintext = "This is a secret to everybody".to_owned();
|
||||
let ciphertext = outbound.encrypt_helper(plaintext.clone()).await;
|
||||
|
||||
assert_eq!(
|
||||
plaintext,
|
||||
inbound.decrypt_helper(ciphertext).await.unwrap().0
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn group_session_export() {
|
||||
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
let (_, inbound) = alice
|
||||
.create_group_session_pair_with_defaults(&room_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let export = inbound.export().await;
|
||||
let export: ForwardedRoomKeyToDeviceEventContent = export.try_into().unwrap();
|
||||
|
||||
let imported = InboundGroupSession::from_export(export).unwrap();
|
||||
|
||||
assert_eq!(inbound.session_id(), imported.session_id());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
// 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.
|
||||
|
||||
use std::{collections::BTreeMap, fmt, sync::Arc};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
events::{
|
||||
room::encrypted::{CiphertextInfo, EncryptedEventContent, OlmV1Curve25519AesSha2Content},
|
||||
EventType,
|
||||
},
|
||||
identifiers::{DeviceId, DeviceKeyAlgorithm, UserId},
|
||||
instant::{Duration, Instant},
|
||||
locks::Mutex,
|
||||
};
|
||||
use olm_rs::{errors::OlmSessionError, session::OlmSession, PicklingMode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::IdentityKeys;
|
||||
use crate::{
|
||||
error::{EventError, OlmResult, SessionUnpicklingError},
|
||||
ReadOnlyDevice,
|
||||
};
|
||||
|
||||
pub use olm_rs::{
|
||||
session::{OlmMessage, PreKeyMessage},
|
||||
utility::OlmUtility,
|
||||
};
|
||||
|
||||
/// Cryptographic session that enables secure communication between two
|
||||
/// `Account`s
|
||||
#[derive(Clone)]
|
||||
pub struct Session {
|
||||
pub(crate) user_id: Arc<UserId>,
|
||||
pub(crate) device_id: Arc<Box<DeviceId>>,
|
||||
pub(crate) our_identity_keys: Arc<IdentityKeys>,
|
||||
pub(crate) inner: Arc<Mutex<OlmSession>>,
|
||||
pub(crate) session_id: Arc<str>,
|
||||
pub(crate) sender_key: Arc<str>,
|
||||
pub(crate) creation_time: Arc<Instant>,
|
||||
pub(crate) last_use_time: Arc<Instant>,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl fmt::Debug for Session {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Session")
|
||||
.field("session_id", &self.session_id())
|
||||
.field("sender_key", &self.sender_key)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Decrypt the given Olm message.
|
||||
///
|
||||
/// Returns the decrypted plaintext or an `OlmSessionError` if decryption
|
||||
/// failed.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `message` - The Olm message that should be decrypted.
|
||||
pub async fn decrypt(&mut self, message: OlmMessage) -> Result<String, OlmSessionError> {
|
||||
let plaintext = self.inner.lock().await.decrypt(message)?;
|
||||
self.last_use_time = Arc::new(Instant::now());
|
||||
Ok(plaintext)
|
||||
}
|
||||
|
||||
/// Encrypt the given plaintext as a OlmMessage.
|
||||
///
|
||||
/// Returns the encrypted Olm message.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `plaintext` - The plaintext that should be encrypted.
|
||||
pub(crate) async fn encrypt_helper(&mut self, plaintext: &str) -> OlmMessage {
|
||||
let message = self.inner.lock().await.encrypt(plaintext);
|
||||
self.last_use_time = Arc::new(Instant::now());
|
||||
message
|
||||
}
|
||||
|
||||
/// Encrypt the given event content content as an m.room.encrypted event
|
||||
/// content.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `recipient_device` - The device for which this message is going to be
|
||||
/// encrypted, this needs to be the device that was used to create this
|
||||
/// session with.
|
||||
///
|
||||
/// * `event_type` - The type of the event.
|
||||
///
|
||||
/// * `content` - The content of the event.
|
||||
pub async fn encrypt(
|
||||
&mut self,
|
||||
recipient_device: &ReadOnlyDevice,
|
||||
event_type: EventType,
|
||||
content: Value,
|
||||
) -> OlmResult<EncryptedEventContent> {
|
||||
let recipient_signing_key = recipient_device
|
||||
.get_key(DeviceKeyAlgorithm::Ed25519)
|
||||
.ok_or(EventError::MissingSigningKey)?;
|
||||
|
||||
let payload = json!({
|
||||
"sender": self.user_id.as_str(),
|
||||
"sender_device": self.device_id.as_ref(),
|
||||
"keys": {
|
||||
"ed25519": self.our_identity_keys.ed25519(),
|
||||
},
|
||||
"recipient": recipient_device.user_id(),
|
||||
"recipient_keys": {
|
||||
"ed25519": recipient_signing_key,
|
||||
},
|
||||
"type": event_type,
|
||||
"content": content,
|
||||
});
|
||||
|
||||
let plaintext = serde_json::to_string(&payload)?;
|
||||
let ciphertext = self.encrypt_helper(&plaintext).await.to_tuple();
|
||||
|
||||
let message_type = ciphertext.0;
|
||||
let ciphertext = CiphertextInfo::new(ciphertext.1, (message_type as u32).into());
|
||||
|
||||
let mut content = BTreeMap::new();
|
||||
content.insert((&*self.sender_key).to_owned(), ciphertext);
|
||||
|
||||
Ok(EncryptedEventContent::OlmV1Curve25519AesSha2(
|
||||
OlmV1Curve25519AesSha2Content::new(
|
||||
content,
|
||||
self.our_identity_keys.curve25519().to_string(),
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
/// Check if a pre-key Olm message was encrypted for this session.
|
||||
///
|
||||
/// Returns true if it matches, false if not and a OlmSessionError if there
|
||||
/// was an error checking if it matches.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `their_identity_key` - The identity/curve25519 key of the account
|
||||
/// that encrypted this Olm message.
|
||||
///
|
||||
/// * `message` - The pre-key Olm message that should be checked.
|
||||
pub async fn matches(
|
||||
&self,
|
||||
their_identity_key: &str,
|
||||
message: PreKeyMessage,
|
||||
) -> Result<bool, OlmSessionError> {
|
||||
self.inner
|
||||
.lock()
|
||||
.await
|
||||
.matches_inbound_session_from(their_identity_key, message)
|
||||
}
|
||||
|
||||
/// Returns the unique identifier for this session.
|
||||
pub fn session_id(&self) -> &str {
|
||||
&self.session_id
|
||||
}
|
||||
|
||||
/// Store the session as a base64 encoded string.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pickle_mode` - The mode that was used to pickle the session, either
|
||||
/// an unencrypted mode or an encrypted using passphrase.
|
||||
pub async fn pickle(&self, pickle_mode: PicklingMode) -> PickledSession {
|
||||
let pickle = self.inner.lock().await.pickle(pickle_mode);
|
||||
|
||||
PickledSession {
|
||||
pickle: SessionPickle::from(pickle),
|
||||
sender_key: self.sender_key.to_string(),
|
||||
// FIXME this should use the duration from the unix epoch.
|
||||
creation_time: self.creation_time.elapsed(),
|
||||
last_use_time: self.last_use_time.elapsed(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore a Session from a previously pickled string.
|
||||
///
|
||||
/// Returns the restored Olm Session or a `SessionUnpicklingError` if there
|
||||
/// was an error.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - Our own user id that the session belongs to.
|
||||
///
|
||||
/// * `device_id` - Our own device id that the session belongs to.
|
||||
///
|
||||
/// * `our_idenity_keys` - An clone of the Arc to our own identity keys.
|
||||
///
|
||||
/// * `pickle` - The pickled version of the `Session`.
|
||||
///
|
||||
/// * `pickle_mode` - The mode that was used to pickle the session, either
|
||||
/// an unencrypted mode or an encrypted using passphrase.
|
||||
pub fn from_pickle(
|
||||
user_id: Arc<UserId>,
|
||||
device_id: Arc<Box<DeviceId>>,
|
||||
our_identity_keys: Arc<IdentityKeys>,
|
||||
pickle: PickledSession,
|
||||
pickle_mode: PicklingMode,
|
||||
) -> Result<Self, SessionUnpicklingError> {
|
||||
let session = OlmSession::unpickle(pickle.pickle.0, pickle_mode)?;
|
||||
let session_id = session.session_id();
|
||||
|
||||
// FIXME this should use the UNIX epoch.
|
||||
let now = Instant::now();
|
||||
|
||||
let creation_time = now
|
||||
.checked_sub(pickle.creation_time)
|
||||
.ok_or(SessionUnpicklingError::SessionTimestampError)?;
|
||||
let last_use_time = now
|
||||
.checked_sub(pickle.last_use_time)
|
||||
.ok_or(SessionUnpicklingError::SessionTimestampError)?;
|
||||
|
||||
Ok(Session {
|
||||
user_id,
|
||||
device_id,
|
||||
our_identity_keys,
|
||||
inner: Arc::new(Mutex::new(session)),
|
||||
session_id: session_id.into(),
|
||||
sender_key: pickle.sender_key.into(),
|
||||
creation_time: Arc::new(creation_time),
|
||||
last_use_time: Arc::new(last_use_time),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Session {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.session_id() == other.session_id()
|
||||
}
|
||||
}
|
||||
|
||||
/// A pickled version of a `Session`.
|
||||
///
|
||||
/// Holds all the information that needs to be stored in a database to restore
|
||||
/// a Session.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PickledSession {
|
||||
/// The pickle string holding the Olm Session.
|
||||
pub pickle: SessionPickle,
|
||||
/// The curve25519 key of the other user that we share this session with.
|
||||
pub sender_key: String,
|
||||
/// The relative time elapsed since the session was created.
|
||||
pub creation_time: Duration,
|
||||
/// The relative time elapsed since the session was last used.
|
||||
pub last_use_time: Duration,
|
||||
}
|
||||
|
||||
/// The typed representation of a base64 encoded string of the Olm Session pickle.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionPickle(String);
|
||||
|
||||
impl From<String> for SessionPickle {
|
||||
fn from(picle_string: String) -> Self {
|
||||
SessionPickle(picle_string)
|
||||
}
|
||||
}
|
||||
|
||||
impl SessionPickle {
|
||||
/// Get the string representation of the pickle.
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,599 @@
|
||||
// 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.
|
||||
|
||||
mod pk_signing;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Error as JsonError;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
},
|
||||
};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
api::r0::keys::{upload_signatures::Request as SignatureUploadRequest, KeyUsage},
|
||||
encryption::DeviceKeys,
|
||||
identifiers::{DeviceKeyAlgorithm, DeviceKeyId, UserId},
|
||||
locks::Mutex,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::SignatureError, requests::UploadSigningKeysRequest, OwnUserIdentity, ReadOnlyAccount,
|
||||
ReadOnlyDevice, UserIdentity,
|
||||
};
|
||||
|
||||
use pk_signing::{MasterSigning, PickledSignings, SelfSigning, Signing, SigningError, UserSigning};
|
||||
|
||||
/// Private cross signing identity.
|
||||
///
|
||||
/// This object holds the private and public ed25519 key triplet that is used
|
||||
/// for cross signing.
|
||||
///
|
||||
/// The object might be comletely empty or have only some of the key pairs
|
||||
/// available.
|
||||
///
|
||||
/// It can be used to sign devices or other identities.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PrivateCrossSigningIdentity {
|
||||
user_id: Arc<UserId>,
|
||||
shared: Arc<AtomicBool>,
|
||||
pub(crate) master_key: Arc<Mutex<Option<MasterSigning>>>,
|
||||
pub(crate) user_signing_key: Arc<Mutex<Option<UserSigning>>>,
|
||||
pub(crate) self_signing_key: Arc<Mutex<Option<SelfSigning>>>,
|
||||
}
|
||||
|
||||
/// The pickled version of a `PrivateCrossSigningIdentity`.
|
||||
///
|
||||
/// Can be used to store the identity.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PickledCrossSigningIdentity {
|
||||
/// The user id of the identity owner.
|
||||
pub user_id: UserId,
|
||||
/// Have the public keys of the identity been shared.
|
||||
pub shared: bool,
|
||||
/// The encrypted pickle of the identity.
|
||||
pub pickle: String,
|
||||
}
|
||||
|
||||
impl PrivateCrossSigningIdentity {
|
||||
/// Get the user id that this identity belongs to.
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
&self.user_id
|
||||
}
|
||||
|
||||
/// Is the identity empty.
|
||||
///
|
||||
/// An empty identity doesn't contain any private keys.
|
||||
///
|
||||
/// It is usual for the identity not to contain the master key since the
|
||||
/// master key is only needed to sign the subkeys.
|
||||
///
|
||||
/// An empty identity indicates that either no identity was created for this
|
||||
/// use or that another device created it and hasn't shared it yet with us.
|
||||
pub async fn is_empty(&self) -> bool {
|
||||
let has_master = self.master_key.lock().await.is_some();
|
||||
let has_user = self.user_signing_key.lock().await.is_some();
|
||||
let has_self = self.self_signing_key.lock().await.is_some();
|
||||
|
||||
!(has_master && has_user && has_self)
|
||||
}
|
||||
|
||||
/// Create a new empty identity.
|
||||
pub(crate) fn empty(user_id: UserId) -> Self {
|
||||
Self {
|
||||
user_id: Arc::new(user_id),
|
||||
shared: Arc::new(AtomicBool::new(false)),
|
||||
master_key: Arc::new(Mutex::new(None)),
|
||||
self_signing_key: Arc::new(Mutex::new(None)),
|
||||
user_signing_key: Arc::new(Mutex::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn as_public_identity(&self) -> Result<OwnUserIdentity, SignatureError> {
|
||||
let master = self
|
||||
.master_key
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.ok_or(SignatureError::MissingSigningKey)?
|
||||
.public_key
|
||||
.clone();
|
||||
let self_signing = self
|
||||
.self_signing_key
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.ok_or(SignatureError::MissingSigningKey)?
|
||||
.public_key
|
||||
.clone();
|
||||
let user_signing = self
|
||||
.user_signing_key
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.ok_or(SignatureError::MissingSigningKey)?
|
||||
.public_key
|
||||
.clone();
|
||||
let identity = OwnUserIdentity::new(master, self_signing, user_signing)?;
|
||||
identity.mark_as_verified();
|
||||
|
||||
Ok(identity)
|
||||
}
|
||||
|
||||
/// Sign the given public user identity with this private identity.
|
||||
pub(crate) async fn sign_user(
|
||||
&self,
|
||||
user_identity: &UserIdentity,
|
||||
) -> Result<SignatureUploadRequest, SignatureError> {
|
||||
let signed_keys = self
|
||||
.user_signing_key
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.ok_or(SignatureError::MissingSigningKey)?
|
||||
.sign_user(&user_identity)
|
||||
.await?;
|
||||
|
||||
Ok(SignatureUploadRequest::new(signed_keys))
|
||||
}
|
||||
|
||||
/// Sign the given device keys with this identity.
|
||||
pub(crate) async fn sign_device(
|
||||
&self,
|
||||
device: &ReadOnlyDevice,
|
||||
) -> Result<SignatureUploadRequest, SignatureError> {
|
||||
let mut device_keys = device.as_device_keys();
|
||||
device_keys.signatures.clear();
|
||||
self.sign_device_keys(&mut device_keys).await
|
||||
}
|
||||
|
||||
/// Sign an Olm account with this private identity.
|
||||
pub(crate) async fn sign_account(
|
||||
&self,
|
||||
account: &ReadOnlyAccount,
|
||||
) -> Result<SignatureUploadRequest, SignatureError> {
|
||||
let mut device_keys = account.unsigned_device_keys();
|
||||
self.sign_device_keys(&mut device_keys).await
|
||||
}
|
||||
|
||||
pub(crate) async fn sign_device_keys(
|
||||
&self,
|
||||
mut device_keys: &mut DeviceKeys,
|
||||
) -> Result<SignatureUploadRequest, SignatureError> {
|
||||
self.self_signing_key
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.ok_or(SignatureError::MissingSigningKey)?
|
||||
.sign_device(&mut device_keys)
|
||||
.await?;
|
||||
|
||||
let mut signed_keys = BTreeMap::new();
|
||||
signed_keys
|
||||
.entry((&*self.user_id).to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(
|
||||
device_keys.device_id.to_string(),
|
||||
serde_json::to_value(device_keys)?,
|
||||
);
|
||||
|
||||
Ok(SignatureUploadRequest::new(signed_keys))
|
||||
}
|
||||
|
||||
/// Create a new identity for the given Olm Account.
|
||||
///
|
||||
/// Returns the new identity, the upload signing keys request and a
|
||||
/// signature upload request that contains the signature of the account
|
||||
/// signed by the self signing key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `account` - The Olm account that is creating the new identity. The
|
||||
/// account will sign the master key and the self signing key will sign the
|
||||
/// account.
|
||||
pub(crate) async fn new_with_account(
|
||||
account: &ReadOnlyAccount,
|
||||
) -> (Self, UploadSigningKeysRequest, SignatureUploadRequest) {
|
||||
let master = Signing::new();
|
||||
|
||||
let mut public_key =
|
||||
master.cross_signing_key(account.user_id().to_owned(), KeyUsage::Master);
|
||||
let signature = account
|
||||
.sign_json(
|
||||
serde_json::to_value(&public_key)
|
||||
.expect("Can't convert own public master key to json"),
|
||||
)
|
||||
.await;
|
||||
|
||||
public_key
|
||||
.signatures
|
||||
.entry(account.user_id().to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(
|
||||
DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, account.device_id())
|
||||
.to_string(),
|
||||
signature,
|
||||
);
|
||||
|
||||
let master = MasterSigning {
|
||||
inner: master,
|
||||
public_key: public_key.into(),
|
||||
};
|
||||
|
||||
let identity = Self::new_helper(account.user_id(), master).await;
|
||||
let signature_request = identity
|
||||
.sign_account(account)
|
||||
.await
|
||||
.expect("Can't sign own device with new cross signign keys");
|
||||
|
||||
let request = identity.as_upload_request().await;
|
||||
|
||||
(identity, request, signature_request)
|
||||
}
|
||||
|
||||
async fn new_helper(user_id: &UserId, master: MasterSigning) -> Self {
|
||||
let user = Signing::new();
|
||||
let mut public_key = user.cross_signing_key(user_id.to_owned(), KeyUsage::UserSigning);
|
||||
master.sign_subkey(&mut public_key).await;
|
||||
|
||||
let user = UserSigning {
|
||||
inner: user,
|
||||
public_key: public_key.into(),
|
||||
};
|
||||
|
||||
let self_signing = Signing::new();
|
||||
let mut public_key =
|
||||
self_signing.cross_signing_key(user_id.to_owned(), KeyUsage::SelfSigning);
|
||||
master.sign_subkey(&mut public_key).await;
|
||||
|
||||
let self_signing = SelfSigning {
|
||||
inner: self_signing,
|
||||
public_key: public_key.into(),
|
||||
};
|
||||
|
||||
Self {
|
||||
user_id: Arc::new(user_id.to_owned()),
|
||||
shared: Arc::new(AtomicBool::new(false)),
|
||||
master_key: Arc::new(Mutex::new(Some(master))),
|
||||
self_signing_key: Arc::new(Mutex::new(Some(self_signing))),
|
||||
user_signing_key: Arc::new(Mutex::new(Some(user))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new cross signing identity without signing the device that
|
||||
/// created it.
|
||||
#[cfg(test)]
|
||||
pub(crate) async fn new(user_id: UserId) -> Self {
|
||||
let master = Signing::new();
|
||||
|
||||
let public_key = master.cross_signing_key(user_id.clone(), KeyUsage::Master);
|
||||
let master = MasterSigning {
|
||||
inner: master,
|
||||
public_key: public_key.into(),
|
||||
};
|
||||
|
||||
Self::new_helper(&user_id, master).await
|
||||
}
|
||||
|
||||
/// Mark the identity as shared.
|
||||
pub fn mark_as_shared(&self) {
|
||||
self.shared.store(true, Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Has the identity been shared.
|
||||
///
|
||||
/// A shared identity here means that the public keys of the identity have
|
||||
/// been uploaded to the server.
|
||||
pub fn shared(&self) -> bool {
|
||||
self.shared.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Store the cross signing identity as a pickle.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pickle_key` - The key that should be used to encrypt the signing
|
||||
/// object, must be 32 bytes long.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This will panic if the provided pickle key isn't 32 bytes long.
|
||||
pub async fn pickle(
|
||||
&self,
|
||||
pickle_key: &[u8],
|
||||
) -> Result<PickledCrossSigningIdentity, JsonError> {
|
||||
let master_key = if let Some(m) = self.master_key.lock().await.as_ref() {
|
||||
Some(m.pickle(pickle_key).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let self_signing_key = if let Some(m) = self.self_signing_key.lock().await.as_ref() {
|
||||
Some(m.pickle(pickle_key).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let user_signing_key = if let Some(m) = self.user_signing_key.lock().await.as_ref() {
|
||||
Some(m.pickle(pickle_key).await)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let pickle = PickledSignings {
|
||||
master_key,
|
||||
user_signing_key,
|
||||
self_signing_key,
|
||||
};
|
||||
|
||||
let pickle = serde_json::to_string(&pickle)?;
|
||||
|
||||
Ok(PickledCrossSigningIdentity {
|
||||
user_id: self.user_id.as_ref().to_owned(),
|
||||
shared: self.shared(),
|
||||
pickle,
|
||||
})
|
||||
}
|
||||
|
||||
/// Restore the private cross signing identity from a pickle.
|
||||
///
|
||||
/// # Panic
|
||||
///
|
||||
/// Panics if the pickle_key isn't 32 bytes long.
|
||||
pub async fn from_pickle(
|
||||
pickle: PickledCrossSigningIdentity,
|
||||
pickle_key: &[u8],
|
||||
) -> Result<Self, SigningError> {
|
||||
let signings: PickledSignings = serde_json::from_str(&pickle.pickle)?;
|
||||
|
||||
let master = if let Some(m) = signings.master_key {
|
||||
Some(MasterSigning::from_pickle(m, pickle_key)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let self_signing = if let Some(s) = signings.self_signing_key {
|
||||
Some(SelfSigning::from_pickle(s, pickle_key)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let user_signing = if let Some(u) = signings.user_signing_key {
|
||||
Some(UserSigning::from_pickle(u, pickle_key)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
user_id: Arc::new(pickle.user_id),
|
||||
shared: Arc::new(AtomicBool::from(pickle.shared)),
|
||||
master_key: Arc::new(Mutex::new(master)),
|
||||
self_signing_key: Arc::new(Mutex::new(self_signing)),
|
||||
user_signing_key: Arc::new(Mutex::new(user_signing)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get the upload request that is needed to share the public keys of this
|
||||
/// identity.
|
||||
pub(crate) async fn as_upload_request(&self) -> UploadSigningKeysRequest {
|
||||
let master_key = self
|
||||
.master_key
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.map(|k| k.public_key.into());
|
||||
|
||||
let user_signing_key = self
|
||||
.user_signing_key
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.map(|k| k.public_key.into());
|
||||
|
||||
let self_signing_key = self
|
||||
.self_signing_key
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.map(|k| k.public_key.into());
|
||||
|
||||
UploadSigningKeysRequest {
|
||||
master_key,
|
||||
user_signing_key,
|
||||
self_signing_key,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
identities::{ReadOnlyDevice, UserIdentity},
|
||||
olm::ReadOnlyAccount,
|
||||
};
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use super::{PrivateCrossSigningIdentity, Signing};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
api::r0::keys::CrossSigningKey,
|
||||
identifiers::{user_id, UserId},
|
||||
};
|
||||
use matrix_sdk_test::async_test;
|
||||
|
||||
fn user_id() -> UserId {
|
||||
user_id!("@example:localhost")
|
||||
}
|
||||
|
||||
fn pickle_key() -> &'static [u8] {
|
||||
&[0u8; 32]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signing_creation() {
|
||||
let signing = Signing::new();
|
||||
assert!(!signing.public_key().as_str().is_empty());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn signature_verification() {
|
||||
let signing = Signing::new();
|
||||
|
||||
let message = "Hello world";
|
||||
|
||||
let signature = signing.sign(message).await;
|
||||
assert!(signing.verify(message, &signature).await.is_ok());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn pickling_signing() {
|
||||
let signing = Signing::new();
|
||||
let pickled = signing.pickle(pickle_key()).await;
|
||||
|
||||
let unpickled = Signing::from_pickle(pickled, pickle_key()).unwrap();
|
||||
|
||||
assert_eq!(signing.public_key(), unpickled.public_key());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn private_identity_creation() {
|
||||
let identity = PrivateCrossSigningIdentity::new(user_id()).await;
|
||||
|
||||
let master_key = identity.master_key.lock().await;
|
||||
let master_key = master_key.as_ref().unwrap();
|
||||
|
||||
assert!(master_key
|
||||
.public_key
|
||||
.verify_subkey(
|
||||
&identity
|
||||
.self_signing_key
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.public_key,
|
||||
)
|
||||
.is_ok());
|
||||
|
||||
assert!(master_key
|
||||
.public_key
|
||||
.verify_subkey(
|
||||
&identity
|
||||
.user_signing_key
|
||||
.lock()
|
||||
.await
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.public_key,
|
||||
)
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn identity_pickling() {
|
||||
let identity = PrivateCrossSigningIdentity::new(user_id()).await;
|
||||
|
||||
let pickled = identity.pickle(pickle_key()).await.unwrap();
|
||||
|
||||
let unpickled = PrivateCrossSigningIdentity::from_pickle(pickled, pickle_key())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(identity.user_id, unpickled.user_id);
|
||||
assert_eq!(
|
||||
&*identity.master_key.lock().await,
|
||||
&*unpickled.master_key.lock().await
|
||||
);
|
||||
assert_eq!(
|
||||
&*identity.user_signing_key.lock().await,
|
||||
&*unpickled.user_signing_key.lock().await
|
||||
);
|
||||
assert_eq!(
|
||||
&*identity.self_signing_key.lock().await,
|
||||
&*unpickled.self_signing_key.lock().await
|
||||
);
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn private_identity_signed_by_accound() {
|
||||
let account = ReadOnlyAccount::new(&user_id(), "DEVICEID".into());
|
||||
let (identity, _, _) = PrivateCrossSigningIdentity::new_with_account(&account).await;
|
||||
let master = identity.master_key.lock().await;
|
||||
let master = master.as_ref().unwrap();
|
||||
|
||||
assert!(!master.public_key.signatures().is_empty());
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn sign_device() {
|
||||
let account = ReadOnlyAccount::new(&user_id(), "DEVICEID".into());
|
||||
let (identity, _, _) = PrivateCrossSigningIdentity::new_with_account(&account).await;
|
||||
|
||||
let mut device = ReadOnlyDevice::from_account(&account).await;
|
||||
let self_signing = identity.self_signing_key.lock().await;
|
||||
let self_signing = self_signing.as_ref().unwrap();
|
||||
|
||||
let mut device_keys = device.as_device_keys();
|
||||
self_signing.sign_device(&mut device_keys).await.unwrap();
|
||||
device.signatures = Arc::new(device_keys.signatures);
|
||||
|
||||
let public_key = &self_signing.public_key;
|
||||
public_key.verify_device(&device).unwrap()
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn sign_user_identity() {
|
||||
let account = ReadOnlyAccount::new(&user_id(), "DEVICEID".into());
|
||||
let (identity, _, _) = PrivateCrossSigningIdentity::new_with_account(&account).await;
|
||||
|
||||
let bob_account = ReadOnlyAccount::new(&user_id!("@bob:localhost"), "DEVICEID".into());
|
||||
let (bob_private, _, _) = PrivateCrossSigningIdentity::new_with_account(&bob_account).await;
|
||||
let mut bob_public = UserIdentity::from_private(&bob_private).await;
|
||||
|
||||
let user_signing = identity.user_signing_key.lock().await;
|
||||
let user_signing = user_signing.as_ref().unwrap();
|
||||
|
||||
let signatures = user_signing.sign_user(&bob_public).await.unwrap();
|
||||
|
||||
let (key_id, signature) = signatures
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap()
|
||||
.1
|
||||
.iter()
|
||||
.next()
|
||||
.map(|(k, s)| (k.to_string(), serde_json::from_value(s.to_owned()).unwrap()))
|
||||
.unwrap();
|
||||
|
||||
let mut master: CrossSigningKey = bob_public.master_key.as_ref().clone();
|
||||
master
|
||||
.signatures
|
||||
.entry(identity.user_id().to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(key_id, signature);
|
||||
|
||||
bob_public.master_key = master.into();
|
||||
|
||||
user_signing
|
||||
.public_key
|
||||
.verify_master_key(bob_public.master_key())
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,416 @@
|
||||
// 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.
|
||||
|
||||
use aes_gcm::{
|
||||
aead::{generic_array::GenericArray, Aead, NewAead},
|
||||
Aes256Gcm,
|
||||
};
|
||||
use getrandom::getrandom;
|
||||
use matrix_sdk_common::{
|
||||
encryption::DeviceKeys,
|
||||
identifiers::{DeviceKeyAlgorithm, DeviceKeyId},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{json, Error as JsonError, Value};
|
||||
use std::{collections::BTreeMap, convert::TryInto, sync::Arc};
|
||||
use thiserror::Error;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use olm_rs::pk::OlmPkSigning;
|
||||
|
||||
#[cfg(test)]
|
||||
use olm_rs::{errors::OlmUtilityError, utility::OlmUtility};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
api::r0::keys::{CrossSigningKey, KeyUsage},
|
||||
identifiers::UserId,
|
||||
locks::Mutex,
|
||||
CanonicalJsonValue,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::SignatureError,
|
||||
identities::{MasterPubkey, SelfSigningPubkey, UserSigningPubkey},
|
||||
utilities::{decode_url_safe as decode, encode_url_safe as encode, DecodeError},
|
||||
UserIdentity,
|
||||
};
|
||||
|
||||
const NONCE_SIZE: usize = 12;
|
||||
|
||||
/// Error type reporting failures in the Signign operations.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SigningError {
|
||||
/// Error decoding the base64 encoded pickle data.
|
||||
#[error(transparent)]
|
||||
Decode(#[from] DecodeError),
|
||||
|
||||
/// Error decrypting the pickled signing seed
|
||||
#[error("Error decrypting the pickled signign seed")]
|
||||
Decryption(String),
|
||||
|
||||
/// Error deserializing the pickle data.
|
||||
#[error(transparent)]
|
||||
Json(#[from] JsonError),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Signing {
|
||||
inner: Arc<Mutex<OlmPkSigning>>,
|
||||
seed: Arc<Zeroizing<Vec<u8>>>,
|
||||
public_key: PublicSigningKey,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Signing {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Signing")
|
||||
.field("public_key", &self.public_key.as_str())
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Signing {
|
||||
fn eq(&self, other: &Signing) -> bool {
|
||||
self.seed == other.seed
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct InnerPickle {
|
||||
version: u8,
|
||||
nonce: String,
|
||||
ciphertext: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct MasterSigning {
|
||||
pub inner: Signing,
|
||||
pub public_key: MasterPubkey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PickledMasterSigning {
|
||||
pickle: PickledSigning,
|
||||
public_key: CrossSigningKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PickledUserSigning {
|
||||
pickle: PickledSigning,
|
||||
public_key: CrossSigningKey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct PickledSelfSigning {
|
||||
pickle: PickledSigning,
|
||||
public_key: CrossSigningKey,
|
||||
}
|
||||
|
||||
impl Signature {
|
||||
#[cfg(test)]
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl PickledSigning {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PublicSigningKey(Arc<str>);
|
||||
|
||||
impl PublicSigningKey {
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
|
||||
#[allow(clippy::inherent_to_string)]
|
||||
fn to_string(&self) -> String {
|
||||
self.0.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl MasterSigning {
|
||||
pub async fn pickle(&self, pickle_key: &[u8]) -> PickledMasterSigning {
|
||||
let pickle = self.inner.pickle(pickle_key).await;
|
||||
let public_key = self.public_key.clone().into();
|
||||
PickledMasterSigning { pickle, public_key }
|
||||
}
|
||||
|
||||
pub fn from_pickle(
|
||||
pickle: PickledMasterSigning,
|
||||
pickle_key: &[u8],
|
||||
) -> Result<Self, SigningError> {
|
||||
let inner = Signing::from_pickle(pickle.pickle, pickle_key)?;
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
public_key: pickle.public_key.into(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn sign_subkey<'a>(&self, subkey: &mut CrossSigningKey) {
|
||||
let subkey_wihtout_signatures = json!({
|
||||
"user_id": subkey.user_id.clone(),
|
||||
"keys": subkey.keys.clone(),
|
||||
"usage": subkey.usage.clone(),
|
||||
});
|
||||
|
||||
let message = serde_json::to_string(&subkey_wihtout_signatures)
|
||||
.expect("Can't serialize cross signing subkey");
|
||||
let signature = self.inner.sign(&message).await;
|
||||
|
||||
subkey
|
||||
.signatures
|
||||
.entry(self.public_key.user_id().to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(
|
||||
DeviceKeyId::from_parts(
|
||||
DeviceKeyAlgorithm::Ed25519,
|
||||
self.inner.public_key().as_str().into(),
|
||||
)
|
||||
.to_string(),
|
||||
signature.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl UserSigning {
|
||||
pub async fn pickle(&self, pickle_key: &[u8]) -> PickledUserSigning {
|
||||
let pickle = self.inner.pickle(pickle_key).await;
|
||||
let public_key = self.public_key.clone().into();
|
||||
PickledUserSigning { pickle, public_key }
|
||||
}
|
||||
|
||||
pub async fn sign_user(
|
||||
&self,
|
||||
user: &UserIdentity,
|
||||
) -> Result<BTreeMap<UserId, BTreeMap<String, Value>>, SignatureError> {
|
||||
let user_master: &CrossSigningKey = user.master_key().as_ref();
|
||||
let signature = self
|
||||
.inner
|
||||
.sign_json(serde_json::to_value(user_master)?)
|
||||
.await?;
|
||||
|
||||
let mut signatures = BTreeMap::new();
|
||||
|
||||
signatures
|
||||
.entry(self.public_key.user_id().to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(
|
||||
DeviceKeyId::from_parts(
|
||||
DeviceKeyAlgorithm::Ed25519,
|
||||
self.inner.public_key.as_str().into(),
|
||||
)
|
||||
.to_string(),
|
||||
serde_json::to_value(signature.0)?,
|
||||
);
|
||||
|
||||
Ok(signatures)
|
||||
}
|
||||
|
||||
pub fn from_pickle(
|
||||
pickle: PickledUserSigning,
|
||||
pickle_key: &[u8],
|
||||
) -> Result<Self, SigningError> {
|
||||
let inner = Signing::from_pickle(pickle.pickle, pickle_key)?;
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
public_key: pickle.public_key.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SelfSigning {
|
||||
pub async fn pickle(&self, pickle_key: &[u8]) -> PickledSelfSigning {
|
||||
let pickle = self.inner.pickle(pickle_key).await;
|
||||
let public_key = self.public_key.clone().into();
|
||||
PickledSelfSigning { pickle, public_key }
|
||||
}
|
||||
|
||||
pub async fn sign_device_helper(&self, value: Value) -> Result<Signature, SignatureError> {
|
||||
self.inner.sign_json(value).await
|
||||
}
|
||||
|
||||
pub async fn sign_device(&self, device_keys: &mut DeviceKeys) -> Result<(), SignatureError> {
|
||||
// Create a copy of the device keys containing only fields that will
|
||||
// get signed.
|
||||
let json_device = json!({
|
||||
"user_id": device_keys.user_id,
|
||||
"device_id": device_keys.device_id,
|
||||
"algorithms": device_keys.algorithms,
|
||||
"keys": device_keys.keys,
|
||||
});
|
||||
|
||||
let signature = self.sign_device_helper(json_device).await?;
|
||||
|
||||
device_keys
|
||||
.signatures
|
||||
.entry(self.public_key.user_id().to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(
|
||||
DeviceKeyId::from_parts(
|
||||
DeviceKeyAlgorithm::Ed25519,
|
||||
self.inner.public_key.as_str().into(),
|
||||
),
|
||||
signature.0,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn from_pickle(
|
||||
pickle: PickledSelfSigning,
|
||||
pickle_key: &[u8],
|
||||
) -> Result<Self, SigningError> {
|
||||
let inner = Signing::from_pickle(pickle.pickle, pickle_key)?;
|
||||
|
||||
Ok(Self {
|
||||
inner,
|
||||
public_key: pickle.public_key.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct SelfSigning {
|
||||
pub inner: Signing,
|
||||
pub public_key: SelfSigningPubkey,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct UserSigning {
|
||||
pub inner: Signing,
|
||||
pub public_key: UserSigningPubkey,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PickledSignings {
|
||||
pub master_key: Option<PickledMasterSigning>,
|
||||
pub user_signing_key: Option<PickledUserSigning>,
|
||||
pub self_signing_key: Option<PickledSelfSigning>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Signature(String);
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PickledSigning(String);
|
||||
|
||||
impl Signing {
|
||||
pub fn new() -> Self {
|
||||
let seed = OlmPkSigning::generate_seed();
|
||||
Self::from_seed(seed)
|
||||
}
|
||||
|
||||
pub fn from_seed(seed: Vec<u8>) -> Self {
|
||||
let inner = OlmPkSigning::new(seed.clone()).expect("Unable to create pk signing object");
|
||||
let public_key = PublicSigningKey(inner.public_key().into());
|
||||
|
||||
Signing {
|
||||
inner: Arc::new(Mutex::new(inner)),
|
||||
seed: Arc::new(Zeroizing::from(seed)),
|
||||
public_key,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_pickle(pickle: PickledSigning, pickle_key: &[u8]) -> Result<Self, SigningError> {
|
||||
let pickled: InnerPickle = serde_json::from_str(pickle.as_str())?;
|
||||
|
||||
let key = GenericArray::from_slice(pickle_key);
|
||||
let cipher = Aes256Gcm::new(key);
|
||||
|
||||
let nonce = decode(pickled.nonce)?;
|
||||
let nonce = GenericArray::from_slice(&nonce);
|
||||
let ciphertext = &decode(pickled.ciphertext)?;
|
||||
|
||||
let seed = cipher
|
||||
.decrypt(&nonce, ciphertext.as_slice())
|
||||
.map_err(|e| SigningError::Decryption(e.to_string()))?;
|
||||
|
||||
Ok(Self::from_seed(seed))
|
||||
}
|
||||
|
||||
pub async fn pickle(&self, pickle_key: &[u8]) -> PickledSigning {
|
||||
let key = GenericArray::from_slice(pickle_key);
|
||||
let cipher = Aes256Gcm::new(key);
|
||||
|
||||
let mut nonce = vec![0u8; NONCE_SIZE];
|
||||
getrandom(&mut nonce).expect("Can't generate nonce to pickle the signing object");
|
||||
let nonce = GenericArray::from_slice(nonce.as_slice());
|
||||
|
||||
let ciphertext = cipher
|
||||
.encrypt(nonce, self.seed.as_slice())
|
||||
.expect("Can't encrypt signing pickle");
|
||||
|
||||
let ciphertext = encode(ciphertext);
|
||||
|
||||
let pickle = InnerPickle {
|
||||
version: 1,
|
||||
nonce: encode(nonce.as_slice()),
|
||||
ciphertext,
|
||||
};
|
||||
|
||||
PickledSigning(serde_json::to_string(&pickle).expect("Can't encode pickled signing"))
|
||||
}
|
||||
|
||||
pub fn public_key(&self) -> &PublicSigningKey {
|
||||
&self.public_key
|
||||
}
|
||||
|
||||
pub fn cross_signing_key(&self, user_id: UserId, usage: KeyUsage) -> CrossSigningKey {
|
||||
let mut keys = BTreeMap::new();
|
||||
|
||||
keys.insert(
|
||||
DeviceKeyId::from_parts(
|
||||
DeviceKeyAlgorithm::Ed25519,
|
||||
self.public_key().as_str().into(),
|
||||
)
|
||||
.to_string(),
|
||||
self.public_key().to_string(),
|
||||
);
|
||||
|
||||
CrossSigningKey {
|
||||
user_id,
|
||||
usage: vec![usage],
|
||||
keys,
|
||||
signatures: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn verify(
|
||||
&self,
|
||||
message: &str,
|
||||
signature: &Signature,
|
||||
) -> Result<bool, OlmUtilityError> {
|
||||
let utility = OlmUtility::new();
|
||||
utility.ed25519_verify(self.public_key.as_str(), message, signature.as_str())
|
||||
}
|
||||
|
||||
pub async fn sign_json(&self, mut json: Value) -> Result<Signature, SignatureError> {
|
||||
let json_object = json.as_object_mut().ok_or(SignatureError::NotAnObject)?;
|
||||
let _ = json_object.remove("signatures");
|
||||
let canonical_json: CanonicalJsonValue =
|
||||
json.try_into().expect("Can't canonicalize the json value");
|
||||
Ok(self.sign(&canonical_json.to_string()).await)
|
||||
}
|
||||
|
||||
pub async fn sign(&self, message: &str) -> Signature {
|
||||
Signature(self.inner.lock().await.sign(message))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
// 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.
|
||||
|
||||
use olm_rs::utility::OlmUtility;
|
||||
use serde_json::Value;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use matrix_sdk_common::{
|
||||
identifiers::{DeviceKeyAlgorithm, DeviceKeyId, UserId},
|
||||
CanonicalJsonValue,
|
||||
};
|
||||
|
||||
use crate::error::SignatureError;
|
||||
|
||||
pub(crate) struct Utility {
|
||||
inner: OlmUtility,
|
||||
}
|
||||
|
||||
impl Utility {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inner: OlmUtility::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify a signed JSON object.
|
||||
///
|
||||
/// The object must have a signatures key associated with an object of the
|
||||
/// form `user_id: {key_id: signature}`.
|
||||
///
|
||||
/// Returns Ok if the signature was successfully verified, otherwise an
|
||||
/// SignatureError.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The user who signed the JSON object.
|
||||
///
|
||||
/// * `key_id` - The id of the key that signed the JSON object.
|
||||
///
|
||||
/// * `signing_key` - The public ed25519 key which was used to sign the JSON
|
||||
/// object.
|
||||
///
|
||||
/// * `json` - The JSON object that should be verified.
|
||||
pub(crate) fn verify_json(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
key_id: &DeviceKeyId,
|
||||
signing_key: &str,
|
||||
json: &mut Value,
|
||||
) -> Result<(), SignatureError> {
|
||||
if key_id.algorithm() != DeviceKeyAlgorithm::Ed25519 {
|
||||
return Err(SignatureError::UnsupportedAlgorithm);
|
||||
}
|
||||
|
||||
let json_object = json.as_object_mut().ok_or(SignatureError::NotAnObject)?;
|
||||
let unsigned = json_object.remove("unsigned");
|
||||
let signatures = json_object.remove("signatures");
|
||||
|
||||
let canonical_json: CanonicalJsonValue = json
|
||||
.clone()
|
||||
.try_into()
|
||||
.map_err(|_| SignatureError::NotAnObject)?;
|
||||
|
||||
let canonical_json: String = canonical_json.to_string();
|
||||
|
||||
let signatures = signatures.ok_or(SignatureError::NoSignatureFound)?;
|
||||
let signature_object = signatures
|
||||
.as_object()
|
||||
.ok_or(SignatureError::NoSignatureFound)?;
|
||||
let signature = signature_object
|
||||
.get(user_id.as_str())
|
||||
.ok_or(SignatureError::NoSignatureFound)?;
|
||||
let signature = signature
|
||||
.get(key_id.to_string())
|
||||
.ok_or(SignatureError::NoSignatureFound)?;
|
||||
let signature = signature.as_str().ok_or(SignatureError::NoSignatureFound)?;
|
||||
|
||||
let ret = match self
|
||||
.inner
|
||||
.ed25519_verify(signing_key, &canonical_json, signature)
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(SignatureError::VerificationError),
|
||||
};
|
||||
|
||||
let json_object = json.as_object_mut().ok_or(SignatureError::NotAnObject)?;
|
||||
|
||||
if let Some(u) = unsigned {
|
||||
json_object.insert("unsigned".to_string(), u);
|
||||
}
|
||||
|
||||
json_object.insert("signatures".to_string(), signatures);
|
||||
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::Utility;
|
||||
use matrix_sdk_common::identifiers::{user_id, DeviceKeyAlgorithm, DeviceKeyId};
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn signature_test() {
|
||||
let mut device_keys = json!({
|
||||
"device_id": "GBEWHQOYGS",
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2"
|
||||
],
|
||||
"keys": {
|
||||
"curve25519:GBEWHQOYGS": "F8QhZ0Z1rjtWrQOblMDgZtEX5x1UrG7sZ2Kk3xliNAU",
|
||||
"ed25519:GBEWHQOYGS": "n469gw7zm+KW+JsFIJKnFVvCKU14HwQyocggcCIQgZY"
|
||||
},
|
||||
"signatures": {
|
||||
"@example:localhost": {
|
||||
"ed25519:GBEWHQOYGS": "OlF2REsqjYdAfr04ONx8VS/5cB7KjrWYRlLF4eUm2foAiQL/RAfsjsa2JXZeoOHh6vEualZHbWlod49OewVqBg"
|
||||
}
|
||||
},
|
||||
"unsigned": {
|
||||
"device_display_name": "Weechat-Matrix-rs"
|
||||
},
|
||||
"user_id": "@example:localhost"
|
||||
});
|
||||
|
||||
let signing_key = "n469gw7zm+KW+JsFIJKnFVvCKU14HwQyocggcCIQgZY";
|
||||
|
||||
let utility = Utility::new();
|
||||
|
||||
utility
|
||||
.verify_json(
|
||||
&user_id!("@example:localhost"),
|
||||
&DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, "GBEWHQOYGS".into()),
|
||||
&signing_key,
|
||||
&mut device_keys,
|
||||
)
|
||||
.expect("Can't verify device keys");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
// 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.
|
||||
|
||||
use std::{collections::BTreeMap, sync::Arc, time::Duration};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
api::r0::{
|
||||
keys::{
|
||||
claim_keys::Response as KeysClaimResponse,
|
||||
get_keys::Response as KeysQueryResponse,
|
||||
upload_keys::{Request as KeysUploadRequest, Response as KeysUploadResponse},
|
||||
upload_signatures::{
|
||||
Request as SignatureUploadRequest, Response as SignatureUploadResponse,
|
||||
},
|
||||
upload_signing_keys::Response as SigningKeysUploadResponse,
|
||||
CrossSigningKey,
|
||||
},
|
||||
to_device::{send_event_to_device::Response as ToDeviceResponse, DeviceIdOrAllDevices},
|
||||
},
|
||||
events::EventType,
|
||||
identifiers::{DeviceIdBox, UserId},
|
||||
uuid::Uuid,
|
||||
};
|
||||
|
||||
use serde_json::value::RawValue as RawJsonValue;
|
||||
|
||||
/// Customized version of `ruma_client_api::r0::to_device::send_event_to_device::Request`, using a
|
||||
/// UUID for the transaction ID.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ToDeviceRequest {
|
||||
/// Type of event being sent to each device.
|
||||
pub event_type: EventType,
|
||||
|
||||
/// A request identifier unique to the access token used to send the request.
|
||||
pub txn_id: Uuid,
|
||||
|
||||
/// A map of users to devices to a content for a message event to be
|
||||
/// sent to the user's device. Individual message events can be sent
|
||||
/// to devices, but all events must be of the same type.
|
||||
/// The content's type for this field will be updated in a future
|
||||
/// release, until then you can create a value using
|
||||
/// `serde_json::value::to_raw_value`.
|
||||
pub messages: BTreeMap<UserId, BTreeMap<DeviceIdOrAllDevices, Box<RawJsonValue>>>,
|
||||
}
|
||||
|
||||
impl ToDeviceRequest {
|
||||
/// Gets the transaction ID as a string.
|
||||
pub fn txn_id_string(&self) -> String {
|
||||
self.txn_id.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Request that will publish a cross signing identity.
|
||||
///
|
||||
/// This uploads the public cross signing key triplet.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UploadSigningKeysRequest {
|
||||
/// The user's master key.
|
||||
pub master_key: Option<CrossSigningKey>,
|
||||
/// The user's self-signing key. Must be signed with the accompanied master, or by the
|
||||
/// user's most recently uploaded master key if no master key is included in the request.
|
||||
pub self_signing_key: Option<CrossSigningKey>,
|
||||
/// The user's user-signing key. Must be signed with the accompanied master, or by the
|
||||
/// user's most recently uploaded master key if no master key is included in the request.
|
||||
pub user_signing_key: Option<CrossSigningKey>,
|
||||
}
|
||||
|
||||
/// Customized version of `ruma_client_api::r0::keys::get_keys::Request`, without any references.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeysQueryRequest {
|
||||
/// The time (in milliseconds) to wait when downloading keys from remote
|
||||
/// servers. 10 seconds is the recommended default.
|
||||
pub timeout: Option<Duration>,
|
||||
|
||||
/// The keys to be downloaded. An empty list indicates all devices for
|
||||
/// the corresponding user.
|
||||
pub device_keys: BTreeMap<UserId, Vec<DeviceIdBox>>,
|
||||
|
||||
/// If the client is fetching keys as a result of a device update
|
||||
/// received in a sync request, this should be the 'since' token of that
|
||||
/// sync request, or any later sync token. This allows the server to
|
||||
/// ensure its response contains the keys advertised by the notification
|
||||
/// in that sync.
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
impl KeysQueryRequest {
|
||||
pub(crate) fn new(device_keys: BTreeMap<UserId, Vec<DeviceIdBox>>) -> Self {
|
||||
Self {
|
||||
timeout: None,
|
||||
device_keys,
|
||||
token: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum over the different outgoing requests we can have.
|
||||
#[derive(Debug)]
|
||||
pub enum OutgoingRequests {
|
||||
/// The keys upload request, uploading device and one-time keys.
|
||||
KeysUpload(KeysUploadRequest),
|
||||
/// The keys query request, fetching the device and cross singing keys of
|
||||
/// other users.
|
||||
KeysQuery(KeysQueryRequest),
|
||||
/// The to-device requests, this request is used for a couple of different
|
||||
/// things, the main use is key requests/forwards and interactive device
|
||||
/// verification.
|
||||
ToDeviceRequest(ToDeviceRequest),
|
||||
/// Signature upload request, this request is used after a successful device
|
||||
/// or user verification is done.
|
||||
SignatureUpload(SignatureUploadRequest),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl OutgoingRequests {
|
||||
pub fn to_device(&self) -> Option<&ToDeviceRequest> {
|
||||
match self {
|
||||
OutgoingRequests::ToDeviceRequest(r) => Some(r),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysQueryRequest> for OutgoingRequests {
|
||||
fn from(request: KeysQueryRequest) -> Self {
|
||||
OutgoingRequests::KeysQuery(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<KeysUploadRequest> for OutgoingRequests {
|
||||
fn from(request: KeysUploadRequest) -> Self {
|
||||
OutgoingRequests::KeysUpload(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ToDeviceRequest> for OutgoingRequests {
|
||||
fn from(request: ToDeviceRequest) -> Self {
|
||||
OutgoingRequests::ToDeviceRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SignatureUploadRequest> for OutgoingRequests {
|
||||
fn from(request: SignatureUploadRequest) -> Self {
|
||||
OutgoingRequests::SignatureUpload(request)
|
||||
}
|
||||
}
|
||||
|
||||
/// Enum over all the incoming responses we need to receive.
|
||||
#[derive(Debug)]
|
||||
pub enum IncomingResponse<'a> {
|
||||
/// The keys upload response, notifying us about the amount of uploaded
|
||||
/// one-time keys.
|
||||
KeysUpload(&'a KeysUploadResponse),
|
||||
/// The keys query response, giving us the device and cross singing keys of
|
||||
/// other users.
|
||||
KeysQuery(&'a KeysQueryResponse),
|
||||
/// The to-device response, an empty response.
|
||||
ToDevice(&'a ToDeviceResponse),
|
||||
/// The key claiming requests, giving us new one-time keys of other users so
|
||||
/// new Olm sessions can be created.
|
||||
KeysClaim(&'a KeysClaimResponse),
|
||||
/// The cross signing keys upload response, marking our private cross
|
||||
/// signing identity as shared.
|
||||
SigningKeysUpload(&'a SigningKeysUploadResponse),
|
||||
/// The cross signing keys upload response, marking our private cross
|
||||
/// signing identity as shared.
|
||||
SignatureUpload(&'a SignatureUploadResponse),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a KeysUploadResponse> for IncomingResponse<'a> {
|
||||
fn from(response: &'a KeysUploadResponse) -> Self {
|
||||
IncomingResponse::KeysUpload(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a KeysQueryResponse> for IncomingResponse<'a> {
|
||||
fn from(response: &'a KeysQueryResponse) -> Self {
|
||||
IncomingResponse::KeysQuery(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a ToDeviceResponse> for IncomingResponse<'a> {
|
||||
fn from(response: &'a ToDeviceResponse) -> Self {
|
||||
IncomingResponse::ToDevice(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a KeysClaimResponse> for IncomingResponse<'a> {
|
||||
fn from(response: &'a KeysClaimResponse) -> Self {
|
||||
IncomingResponse::KeysClaim(response)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a SignatureUploadResponse> for IncomingResponse<'a> {
|
||||
fn from(response: &'a SignatureUploadResponse) -> Self {
|
||||
IncomingResponse::SignatureUpload(response)
|
||||
}
|
||||
}
|
||||
|
||||
/// Outgoing request type, holds the unique ID of the request and the actual
|
||||
/// request.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OutgoingRequest {
|
||||
/// The unique id of a request, needs to be passed when receiving a
|
||||
/// response.
|
||||
pub(crate) request_id: Uuid,
|
||||
/// The underlying outgoing request.
|
||||
pub(crate) request: Arc<OutgoingRequests>,
|
||||
}
|
||||
|
||||
impl OutgoingRequest {
|
||||
/// Get the unique id of this request.
|
||||
pub fn request_id(&self) -> &Uuid {
|
||||
&self.request_id
|
||||
}
|
||||
|
||||
/// Get the underlying outgoing request.
|
||||
pub fn request(&self) -> &OutgoingRequests {
|
||||
&self.request
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
// 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.
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use dashmap::DashMap;
|
||||
use matrix_sdk_common::{
|
||||
api::r0::to_device::DeviceIdOrAllDevices,
|
||||
events::{room::encrypted::EncryptedEventContent, AnyMessageEventContent, EventType},
|
||||
identifiers::{RoomId, UserId},
|
||||
uuid::Uuid,
|
||||
};
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::{
|
||||
error::{EventError, MegolmResult, OlmResult},
|
||||
olm::{Account, InboundGroupSession, OutboundGroupSession},
|
||||
store::{Changes, Store},
|
||||
Device, EncryptionSettings, OlmError, ToDeviceRequest,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GroupSessionManager {
|
||||
account: Account,
|
||||
/// Store for the encryption keys.
|
||||
/// Persists all the encryption keys so a client can resume the session
|
||||
/// without the need to create new keys.
|
||||
store: Store,
|
||||
/// The currently active outbound group sessions.
|
||||
outbound_group_sessions: Arc<DashMap<RoomId, OutboundGroupSession>>,
|
||||
outbound_sessions_being_shared: Arc<DashMap<Uuid, OutboundGroupSession>>,
|
||||
}
|
||||
|
||||
impl GroupSessionManager {
|
||||
const MAX_TO_DEVICE_MESSAGES: usize = 20;
|
||||
|
||||
pub(crate) fn new(account: Account, store: Store) -> Self {
|
||||
Self {
|
||||
account,
|
||||
store,
|
||||
outbound_group_sessions: Arc::new(DashMap::new()),
|
||||
outbound_sessions_being_shared: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate_group_session(&self, room_id: &RoomId) -> bool {
|
||||
self.outbound_group_sessions.remove(room_id).is_some()
|
||||
}
|
||||
|
||||
pub fn mark_request_as_sent(&self, request_id: &Uuid) {
|
||||
if let Some((_, s)) = self.outbound_sessions_being_shared.remove(request_id) {
|
||||
s.mark_request_as_sent(request_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invalidate_sessions_new_devices(&self, users: &HashSet<&UserId>) {
|
||||
for session in self.outbound_group_sessions.iter() {
|
||||
if users.iter().any(|u| session.contains_recipient(u)) {
|
||||
info!(
|
||||
"Invalidating outobund session {} for room {}",
|
||||
session.session_id(),
|
||||
session.room_id()
|
||||
);
|
||||
session.invalidate_session();
|
||||
|
||||
if !session.shared() {
|
||||
for request_id in session.clear_requests() {
|
||||
self.outbound_sessions_being_shared.remove(&request_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get an outbound group session for a room, if one exists.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `room_id` - The id of the room for which we should get the outbound
|
||||
/// group session.
|
||||
pub fn get_outbound_group_session(&self, room_id: &RoomId) -> Option<OutboundGroupSession> {
|
||||
#[allow(clippy::map_clone)]
|
||||
self.outbound_group_sessions.get(room_id).map(|s| s.clone())
|
||||
}
|
||||
|
||||
pub async fn encrypt(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
content: AnyMessageEventContent,
|
||||
) -> MegolmResult<EncryptedEventContent> {
|
||||
let session = if let Some(s) = self.get_outbound_group_session(room_id) {
|
||||
s
|
||||
} else {
|
||||
panic!("Session wasn't created nor shared");
|
||||
};
|
||||
|
||||
if session.expired() {
|
||||
panic!("Session expired");
|
||||
}
|
||||
|
||||
Ok(session.encrypt(content).await)
|
||||
}
|
||||
|
||||
/// Should the client share a group session for the given room.
|
||||
///
|
||||
/// Returns true if a session needs to be shared before room messages can be
|
||||
/// encrypted, false if one is already shared and ready to encrypt room
|
||||
/// messages.
|
||||
///
|
||||
/// This should be called every time a new room message wants to be sent out
|
||||
/// since group sessions can expire at any time.
|
||||
pub fn should_share_group_session(&self, room_id: &RoomId) -> bool {
|
||||
let session = self.outbound_group_sessions.get(room_id);
|
||||
|
||||
match session {
|
||||
Some(s) => !s.shared() || s.expired() || s.invalidated(),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new outbound group session.
|
||||
///
|
||||
/// This also creates a matching inbound group session and saves that one in
|
||||
/// the store.
|
||||
pub async fn create_outbound_group_session(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
settings: EncryptionSettings,
|
||||
) -> OlmResult<(OutboundGroupSession, InboundGroupSession)> {
|
||||
let (outbound, inbound) = self
|
||||
.account
|
||||
.create_group_session_pair(room_id, settings)
|
||||
.await
|
||||
.map_err(|_| EventError::UnsupportedAlgorithm)?;
|
||||
|
||||
let _ = self
|
||||
.outbound_group_sessions
|
||||
.insert(room_id.to_owned(), outbound.clone());
|
||||
Ok((outbound, inbound))
|
||||
}
|
||||
|
||||
/// Get to-device requests to share a group session with users in a room.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// `room_id` - The room id of the room where the group session will be
|
||||
/// used.
|
||||
///
|
||||
/// `users` - The list of users that should receive the group session.
|
||||
pub async fn share_group_session(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
users: impl Iterator<Item = &UserId>,
|
||||
encryption_settings: impl Into<EncryptionSettings>,
|
||||
) -> OlmResult<Vec<Arc<ToDeviceRequest>>> {
|
||||
let mut changes = Changes::default();
|
||||
|
||||
let (session, inbound_session) = self
|
||||
.create_outbound_group_session(room_id, encryption_settings.into())
|
||||
.await?;
|
||||
changes.inbound_group_sessions.push(inbound_session);
|
||||
|
||||
let mut devices: Vec<Device> = Vec::new();
|
||||
|
||||
for user_id in users {
|
||||
session.add_recipient(user_id);
|
||||
let user_devices = self.store.get_user_devices(&user_id).await?;
|
||||
devices.extend(user_devices.devices().filter(|d| !d.is_blacklisted()));
|
||||
}
|
||||
|
||||
let mut requests = Vec::new();
|
||||
let key_content = session.as_json().await;
|
||||
|
||||
for device_map_chunk in devices.chunks(Self::MAX_TO_DEVICE_MESSAGES) {
|
||||
let mut messages = BTreeMap::new();
|
||||
|
||||
for device in device_map_chunk {
|
||||
let encrypted = device
|
||||
.encrypt(EventType::RoomKey, key_content.clone())
|
||||
.await;
|
||||
|
||||
let (used_session, encrypted) = match encrypted {
|
||||
Ok(c) => c,
|
||||
Err(OlmError::MissingSession)
|
||||
| Err(OlmError::EventError(EventError::MissingSenderKey)) => {
|
||||
continue;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
changes.sessions.push(used_session);
|
||||
|
||||
messages
|
||||
.entry(device.user_id().clone())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(
|
||||
DeviceIdOrAllDevices::DeviceId(device.device_id().into()),
|
||||
serde_json::value::to_raw_value(&encrypted)?,
|
||||
);
|
||||
}
|
||||
|
||||
let id = Uuid::new_v4();
|
||||
|
||||
let request = Arc::new(ToDeviceRequest {
|
||||
event_type: EventType::RoomEncrypted,
|
||||
txn_id: id,
|
||||
messages,
|
||||
});
|
||||
|
||||
session.add_request(id, request.clone());
|
||||
self.outbound_sessions_being_shared
|
||||
.insert(id, session.clone());
|
||||
requests.push(request);
|
||||
}
|
||||
|
||||
if requests.is_empty() {
|
||||
debug!(
|
||||
"Session {} for room {} doesn't need to be shared with anyone, marking as shared",
|
||||
session.session_id(),
|
||||
session.room_id()
|
||||
);
|
||||
session.mark_as_shared();
|
||||
}
|
||||
|
||||
self.store.save_changes(changes).await?;
|
||||
|
||||
Ok(requests)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.
|
||||
|
||||
mod group_sessions;
|
||||
mod sessions;
|
||||
|
||||
pub(crate) use group_sessions::GroupSessionManager;
|
||||
pub(crate) use sessions::SessionManager;
|
||||
@@ -0,0 +1,497 @@
|
||||
// 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.
|
||||
|
||||
use std::{collections::BTreeMap, sync::Arc, time::Duration};
|
||||
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use matrix_sdk_common::{
|
||||
api::r0::{
|
||||
keys::claim_keys::{Request as KeysClaimRequest, Response as KeysClaimResponse},
|
||||
to_device::DeviceIdOrAllDevices,
|
||||
},
|
||||
assign,
|
||||
events::EventType,
|
||||
identifiers::{DeviceId, DeviceIdBox, DeviceKeyAlgorithm, UserId},
|
||||
uuid::Uuid,
|
||||
};
|
||||
use serde_json::{json, value::to_raw_value};
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
use crate::{
|
||||
error::OlmResult,
|
||||
key_request::KeyRequestMachine,
|
||||
olm::Account,
|
||||
requests::{OutgoingRequest, ToDeviceRequest},
|
||||
store::{Changes, Result as StoreResult, Store},
|
||||
ReadOnlyDevice,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct SessionManager {
|
||||
account: Account,
|
||||
store: Store,
|
||||
/// A map of user/devices that we need to automatically claim keys for.
|
||||
/// Submodules can insert user/device pairs into this map and the
|
||||
/// user/device paris will be added to the list of users when
|
||||
/// [`get_missing_sessions`](#method.get_missing_sessions) is called.
|
||||
users_for_key_claim: Arc<DashMap<UserId, DashSet<DeviceIdBox>>>,
|
||||
wedged_devices: Arc<DashMap<UserId, DashSet<DeviceIdBox>>>,
|
||||
key_request_machine: KeyRequestMachine,
|
||||
outgoing_to_device_requests: Arc<DashMap<Uuid, OutgoingRequest>>,
|
||||
}
|
||||
|
||||
impl SessionManager {
|
||||
const KEY_CLAIM_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const UNWEDGING_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
pub fn new(
|
||||
account: Account,
|
||||
users_for_key_claim: Arc<DashMap<UserId, DashSet<DeviceIdBox>>>,
|
||||
key_request_machine: KeyRequestMachine,
|
||||
store: Store,
|
||||
) -> Self {
|
||||
Self {
|
||||
account,
|
||||
store,
|
||||
key_request_machine,
|
||||
users_for_key_claim,
|
||||
wedged_devices: Arc::new(DashMap::new()),
|
||||
outgoing_to_device_requests: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark the outgoing request as sent.
|
||||
pub fn mark_outgoing_request_as_sent(&self, id: &Uuid) {
|
||||
self.outgoing_to_device_requests.remove(id);
|
||||
}
|
||||
|
||||
pub async fn mark_device_as_wedged(&self, sender: &UserId, curve_key: &str) -> StoreResult<()> {
|
||||
if let Some(device) = self
|
||||
.store
|
||||
.get_device_from_curve_key(sender, curve_key)
|
||||
.await?
|
||||
{
|
||||
let sessions = device.get_sessions().await?;
|
||||
|
||||
if let Some(sessions) = sessions {
|
||||
let mut sessions = sessions.lock().await;
|
||||
sessions.sort_by_key(|s| s.creation_time.clone());
|
||||
|
||||
let session = sessions.get(0);
|
||||
|
||||
if let Some(session) = session {
|
||||
if session.creation_time.elapsed() > Self::UNWEDGING_INTERVAL {
|
||||
self.users_for_key_claim
|
||||
.entry(device.user_id().clone())
|
||||
.or_insert_with(DashSet::new)
|
||||
.insert(device.device_id().into());
|
||||
self.wedged_devices
|
||||
.entry(device.user_id().to_owned())
|
||||
.or_insert_with(DashSet::new)
|
||||
.insert(device.device_id().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn is_device_wedged(&self, device: &ReadOnlyDevice) -> bool {
|
||||
self.wedged_devices
|
||||
.get(device.user_id())
|
||||
.map(|d| d.contains(device.device_id()))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Check if the session was created to unwedge a Device.
|
||||
///
|
||||
/// If the device was wedged this will queue up a dummy to-device message.
|
||||
async fn check_if_unwedged(&self, user_id: &UserId, device_id: &DeviceId) -> OlmResult<()> {
|
||||
if self
|
||||
.wedged_devices
|
||||
.get(user_id)
|
||||
.map(|d| d.remove(device_id))
|
||||
.flatten()
|
||||
.is_some()
|
||||
{
|
||||
if let Some(device) = self.store.get_device(user_id, device_id).await? {
|
||||
let (_, content) = device.encrypt(EventType::Dummy, json!({})).await?;
|
||||
let id = Uuid::new_v4();
|
||||
let mut messages = BTreeMap::new();
|
||||
|
||||
messages
|
||||
.entry(device.user_id().to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(
|
||||
DeviceIdOrAllDevices::DeviceId(device.device_id().into()),
|
||||
to_raw_value(&content)?,
|
||||
);
|
||||
|
||||
let request = OutgoingRequest {
|
||||
request_id: id,
|
||||
request: Arc::new(
|
||||
ToDeviceRequest {
|
||||
event_type: EventType::RoomEncrypted,
|
||||
txn_id: id,
|
||||
messages,
|
||||
}
|
||||
.into(),
|
||||
),
|
||||
};
|
||||
|
||||
self.outgoing_to_device_requests.insert(id, request);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the a key claiming request for the user/device pairs that we are
|
||||
/// missing Olm sessions for.
|
||||
///
|
||||
/// Returns None if no key claiming request needs to be sent out.
|
||||
///
|
||||
/// Sessions need to be established between devices so group sessions for a
|
||||
/// room can be shared with them.
|
||||
///
|
||||
/// This should be called every time a group session needs to be shared as
|
||||
/// well as between sync calls. After a sync some devices may request room
|
||||
/// keys without us having a valid Olm session with them, making it
|
||||
/// impossible to server the room key request, thus it's necessary to check
|
||||
/// for missing sessions between sync as well.
|
||||
///
|
||||
/// **Note**: Care should be taken that only one such request at a time is
|
||||
/// in flight, e.g. using a lock.
|
||||
///
|
||||
/// The response of a successful key claiming requests needs to be passed to
|
||||
/// the `OlmMachine` with the [`receive_keys_claim_response`].
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// `users` - The list of users that we should check if we lack a session
|
||||
/// with one of their devices. This can be an empty iterator when calling
|
||||
/// this method between sync requests.
|
||||
///
|
||||
/// [`receive_keys_claim_response`]: #method.receive_keys_claim_response
|
||||
pub async fn get_missing_sessions(
|
||||
&self,
|
||||
users: &mut impl Iterator<Item = &UserId>,
|
||||
) -> OlmResult<Option<(Uuid, KeysClaimRequest)>> {
|
||||
let mut missing = BTreeMap::new();
|
||||
|
||||
// Add the list of devices that the user wishes to establish sessions
|
||||
// right now.
|
||||
for user_id in users {
|
||||
let user_devices = self.store.get_user_devices(user_id).await?;
|
||||
|
||||
for device in user_devices.devices() {
|
||||
let sender_key = if let Some(k) = device.get_key(DeviceKeyAlgorithm::Curve25519) {
|
||||
k
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let sessions = self.store.get_sessions(sender_key).await?;
|
||||
|
||||
let is_missing = if let Some(sessions) = sessions {
|
||||
sessions.lock().await.is_empty()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if is_missing {
|
||||
missing
|
||||
.entry(user_id.to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(
|
||||
device.device_id().into(),
|
||||
DeviceKeyAlgorithm::SignedCurve25519,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the list of sessions that for some reason automatically need to
|
||||
// create an Olm session.
|
||||
for item in self.users_for_key_claim.iter() {
|
||||
let user = item.key();
|
||||
|
||||
for device_id in item.value().iter() {
|
||||
missing
|
||||
.entry(user.to_owned())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(device_id.to_owned(), DeviceKeyAlgorithm::SignedCurve25519);
|
||||
}
|
||||
}
|
||||
|
||||
if missing.is_empty() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some((
|
||||
Uuid::new_v4(),
|
||||
assign!(KeysClaimRequest::new(missing), {
|
||||
timeout: Some(Self::KEY_CLAIM_TIMEOUT),
|
||||
}),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive a successful key claim response and create new Olm sessions with
|
||||
/// the claimed keys.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `response` - The response containing the claimed one-time keys.
|
||||
pub async fn receive_keys_claim_response(&self, response: &KeysClaimResponse) -> OlmResult<()> {
|
||||
// TODO log the failures here
|
||||
|
||||
let mut changes = Changes::default();
|
||||
|
||||
for (user_id, user_devices) in &response.one_time_keys {
|
||||
for (device_id, key_map) in user_devices {
|
||||
let device = match self.store.get_readonly_device(&user_id, device_id).await {
|
||||
Ok(Some(d)) => d,
|
||||
Ok(None) => {
|
||||
warn!(
|
||||
"Tried to create an Olm session for {} {}, but the device is unknown",
|
||||
user_id, device_id
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Tried to create an Olm session for {} {}, but \
|
||||
can't fetch the device from the store {:?}",
|
||||
user_id, device_id, e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
info!("Creating outbound Session for {} {}", user_id, device_id);
|
||||
|
||||
let session = match self.account.create_outbound_session(device, &key_map).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
warn!("Error creating new outbound session {:?}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
changes.sessions.push(session);
|
||||
|
||||
self.key_request_machine.retry_keyshare(&user_id, device_id);
|
||||
|
||||
if let Err(e) = self.check_if_unwedged(&user_id, device_id).await {
|
||||
error!(
|
||||
"Error while treating an unwedged device {} {} {:?}",
|
||||
user_id, device_id, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self.store.save_changes(changes).await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use dashmap::DashMap;
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
use std::{collections::BTreeMap, sync::Arc};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
api::r0::keys::claim_keys::Response as KeyClaimResponse,
|
||||
identifiers::{user_id, DeviceIdBox, UserId},
|
||||
};
|
||||
use matrix_sdk_test::async_test;
|
||||
|
||||
use super::SessionManager;
|
||||
use crate::{
|
||||
identities::ReadOnlyDevice,
|
||||
key_request::KeyRequestMachine,
|
||||
olm::{Account, PrivateCrossSigningIdentity, ReadOnlyAccount},
|
||||
store::{CryptoStore, MemoryStore, Store},
|
||||
verification::VerificationMachine,
|
||||
};
|
||||
|
||||
fn user_id() -> UserId {
|
||||
user_id!("@example:localhost")
|
||||
}
|
||||
|
||||
fn device_id() -> DeviceIdBox {
|
||||
"DEVICEID".into()
|
||||
}
|
||||
|
||||
fn bob_account() -> ReadOnlyAccount {
|
||||
ReadOnlyAccount::new(&user_id!("@bob:localhost"), "BOBDEVICE".into())
|
||||
}
|
||||
|
||||
async fn session_manager() -> SessionManager {
|
||||
let user_id = user_id();
|
||||
let device_id = device_id();
|
||||
|
||||
let outbound_sessions = Arc::new(DashMap::new());
|
||||
let users_for_key_claim = Arc::new(DashMap::new());
|
||||
let account = ReadOnlyAccount::new(&user_id, &device_id);
|
||||
let store: Arc<Box<dyn CryptoStore>> = Arc::new(Box::new(MemoryStore::new()));
|
||||
store.save_account(account.clone()).await.unwrap();
|
||||
let identity = Arc::new(Mutex::new(PrivateCrossSigningIdentity::empty(
|
||||
user_id.clone(),
|
||||
)));
|
||||
let verification =
|
||||
VerificationMachine::new(account.clone(), identity.clone(), store.clone());
|
||||
|
||||
let user_id = Arc::new(user_id);
|
||||
let device_id = Arc::new(device_id);
|
||||
|
||||
let store = Store::new(user_id.clone(), identity, store, verification);
|
||||
|
||||
let account = Account {
|
||||
inner: account,
|
||||
store: store.clone(),
|
||||
};
|
||||
|
||||
let key_request = KeyRequestMachine::new(
|
||||
user_id,
|
||||
device_id,
|
||||
store.clone(),
|
||||
outbound_sessions,
|
||||
users_for_key_claim.clone(),
|
||||
);
|
||||
|
||||
SessionManager::new(account, users_for_key_claim, key_request, store)
|
||||
}
|
||||
|
||||
#[async_test]
|
||||
async fn session_creation() {
|
||||
let manager = session_manager().await;
|
||||
let bob = bob_account();
|
||||
|
||||
let bob_device = ReadOnlyDevice::from_account(&bob).await;
|
||||
|
||||
manager.store.save_devices(&[bob_device]).await.unwrap();
|
||||
|
||||
let (_, request) = manager
|
||||
.get_missing_sessions(&mut [bob.user_id().clone()].iter())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(request.one_time_keys.contains_key(bob.user_id()));
|
||||
|
||||
bob.generate_one_time_keys_helper(1).await;
|
||||
let one_time = bob.signed_one_time_keys_helper().await.unwrap();
|
||||
bob.mark_keys_as_published().await;
|
||||
|
||||
let mut one_time_keys = BTreeMap::new();
|
||||
one_time_keys
|
||||
.entry(bob.user_id().clone())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(bob.device_id().into(), one_time);
|
||||
|
||||
let response = KeyClaimResponse::new(one_time_keys);
|
||||
|
||||
manager
|
||||
.receive_keys_claim_response(&response)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(manager
|
||||
.get_missing_sessions(&mut [bob.user_id().clone()].iter())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
|
||||
// This test doesn't run on macos because we're modifying the session
|
||||
// creation time so we can get around the UNWEDGING_INTERVAL.
|
||||
#[async_test]
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn session_unwedging() {
|
||||
use matrix_sdk_common::{
|
||||
identifiers::DeviceKeyAlgorithm,
|
||||
instant::{Duration, Instant},
|
||||
};
|
||||
|
||||
let manager = session_manager().await;
|
||||
let bob = bob_account();
|
||||
let (_, mut session) = bob.create_session_for(&manager.account).await;
|
||||
|
||||
let bob_device = ReadOnlyDevice::from_account(&bob).await;
|
||||
session.creation_time = Arc::new(Instant::now() - Duration::from_secs(3601));
|
||||
|
||||
manager
|
||||
.store
|
||||
.save_devices(&[bob_device.clone()])
|
||||
.await
|
||||
.unwrap();
|
||||
manager.store.save_sessions(&[session]).await.unwrap();
|
||||
|
||||
assert!(manager
|
||||
.get_missing_sessions(&mut [bob.user_id().clone()].iter())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
let curve_key = bob_device.get_key(DeviceKeyAlgorithm::Curve25519).unwrap();
|
||||
|
||||
assert!(!manager.users_for_key_claim.contains_key(bob.user_id()));
|
||||
assert!(!manager.is_device_wedged(&bob_device));
|
||||
manager
|
||||
.mark_device_as_wedged(bob_device.user_id(), &curve_key)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(manager.is_device_wedged(&bob_device));
|
||||
assert!(manager.users_for_key_claim.contains_key(bob.user_id()));
|
||||
|
||||
let (_, request) = manager
|
||||
.get_missing_sessions(&mut [bob.user_id().clone()].iter())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(request.one_time_keys.contains_key(bob.user_id()));
|
||||
|
||||
bob.generate_one_time_keys_helper(1).await;
|
||||
let one_time = bob.signed_one_time_keys_helper().await.unwrap();
|
||||
bob.mark_keys_as_published().await;
|
||||
|
||||
let mut one_time_keys = BTreeMap::new();
|
||||
one_time_keys
|
||||
.entry(bob.user_id().clone())
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(bob.device_id().into(), one_time);
|
||||
|
||||
let response = KeyClaimResponse::new(one_time_keys);
|
||||
|
||||
assert!(manager.outgoing_to_device_requests.is_empty());
|
||||
|
||||
manager
|
||||
.receive_keys_claim_response(&response)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(!manager.is_device_wedged(&bob_device));
|
||||
assert!(manager
|
||||
.get_missing_sessions(&mut [bob.user_id().clone()].iter())
|
||||
.await
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(!manager.outgoing_to_device_requests.is_empty())
|
||||
}
|
||||
}
|
||||
@@ -12,27 +12,35 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
//! Collection of small in-memory stores that can be used to cache Olm objects.
|
||||
//!
|
||||
//! Note: You'll only be interested in these if you are implementing a custom
|
||||
//! `CryptoStore`.
|
||||
|
||||
use dashmap::{DashMap, ReadOnlyView};
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use super::device::Device;
|
||||
use super::olm::{InboundGroupSession, Session};
|
||||
use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId};
|
||||
use dashmap::DashMap;
|
||||
use matrix_sdk_common::{
|
||||
identifiers::{DeviceId, DeviceIdBox, RoomId, UserId},
|
||||
locks::Mutex,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
identities::ReadOnlyDevice,
|
||||
olm::{InboundGroupSession, Session},
|
||||
};
|
||||
|
||||
/// In-memory store for Olm Sessions.
|
||||
#[derive(Debug, Default)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct SessionStore {
|
||||
entries: HashMap<String, Arc<Mutex<Vec<Session>>>>,
|
||||
entries: Arc<DashMap<String, Arc<Mutex<Vec<Session>>>>>,
|
||||
}
|
||||
|
||||
impl SessionStore {
|
||||
/// Create a new empty Session store.
|
||||
pub fn new() -> Self {
|
||||
SessionStore {
|
||||
entries: HashMap::new(),
|
||||
entries: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,17 +48,16 @@ impl SessionStore {
|
||||
///
|
||||
/// Returns true if the the session was added, false if the session was
|
||||
/// already in the store.
|
||||
pub async fn add(&mut self, session: Session) -> bool {
|
||||
if !self.entries.contains_key(&*session.sender_key) {
|
||||
self.entries.insert(
|
||||
session.sender_key.to_string(),
|
||||
Arc::new(Mutex::new(Vec::new())),
|
||||
);
|
||||
}
|
||||
let sessions = self.entries.get_mut(&*session.sender_key).unwrap();
|
||||
pub async fn add(&self, session: Session) -> bool {
|
||||
let sessions_lock = self
|
||||
.entries
|
||||
.entry(session.sender_key.to_string())
|
||||
.or_insert_with(|| Arc::new(Mutex::new(Vec::new())));
|
||||
|
||||
if !sessions.lock().await.contains(&session) {
|
||||
sessions.lock().await.push(session);
|
||||
let mut sessions = sessions_lock.lock().await;
|
||||
|
||||
if !sessions.contains(&session) {
|
||||
sessions.push(session);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@@ -59,51 +66,57 @@ impl SessionStore {
|
||||
|
||||
/// Get all the sessions that belong to the given sender key.
|
||||
pub fn get(&self, sender_key: &str) -> Option<Arc<Mutex<Vec<Session>>>> {
|
||||
self.entries.get(sender_key).cloned()
|
||||
#[allow(clippy::map_clone)]
|
||||
self.entries.get(sender_key).map(|s| s.clone())
|
||||
}
|
||||
|
||||
/// Add a list of sessions belonging to the sender key.
|
||||
pub fn set_for_sender(&mut self, sender_key: &str, sessions: Vec<Session>) {
|
||||
pub fn set_for_sender(&self, sender_key: &str, sessions: Vec<Session>) {
|
||||
self.entries
|
||||
.insert(sender_key.to_owned(), Arc::new(Mutex::new(sessions)));
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// In-memory store that houlds inbound group sessions.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
/// In-memory store that holds inbound group sessions.
|
||||
pub struct GroupSessionStore {
|
||||
entries: HashMap<RoomId, HashMap<String, HashMap<String, InboundGroupSession>>>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
entries: Arc<DashMap<RoomId, HashMap<String, HashMap<String, InboundGroupSession>>>>,
|
||||
}
|
||||
|
||||
impl GroupSessionStore {
|
||||
/// Create a new empty store.
|
||||
pub fn new() -> Self {
|
||||
GroupSessionStore {
|
||||
entries: HashMap::new(),
|
||||
entries: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Add a inbound group session to the store.
|
||||
/// Add an inbound group session to the store.
|
||||
///
|
||||
/// Returns true if the the session was added, false if the session was
|
||||
/// already in the store.
|
||||
pub fn add(&mut self, session: InboundGroupSession) -> bool {
|
||||
if !self.entries.contains_key(&session.room_id) {
|
||||
let room_id = &*session.room_id;
|
||||
self.entries.insert(room_id.clone(), HashMap::new());
|
||||
}
|
||||
pub fn add(&self, session: InboundGroupSession) -> bool {
|
||||
self.entries
|
||||
.entry((&*session.room_id).clone())
|
||||
.or_insert_with(HashMap::new)
|
||||
.entry(session.sender_key.to_string())
|
||||
.or_insert_with(HashMap::new)
|
||||
.insert(session.session_id().to_owned(), session)
|
||||
.is_none()
|
||||
}
|
||||
|
||||
let room_map = self.entries.get_mut(&session.room_id).unwrap();
|
||||
|
||||
if !room_map.contains_key(&*session.sender_key) {
|
||||
let sender_key = &*session.sender_key;
|
||||
room_map.insert(sender_key.to_owned(), HashMap::new());
|
||||
}
|
||||
|
||||
let sender_map = room_map.get_mut(&*session.sender_key).unwrap();
|
||||
let ret = sender_map.insert(session.session_id().to_owned(), session);
|
||||
|
||||
ret.is_none()
|
||||
/// Get all the group sessions the store knows about.
|
||||
pub fn get_all(&self) -> Vec<InboundGroupSession> {
|
||||
self.entries
|
||||
.iter()
|
||||
.flat_map(|d| {
|
||||
d.value()
|
||||
.values()
|
||||
.flat_map(|t| t.values().cloned().collect::<Vec<InboundGroupSession>>())
|
||||
.collect::<Vec<InboundGroupSession>>()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get a inbound group session from our store.
|
||||
@@ -129,30 +142,7 @@ impl GroupSessionStore {
|
||||
/// In-memory store holding the devices of users.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct DeviceStore {
|
||||
entries: Arc<DashMap<UserId, DashMap<String, Device>>>,
|
||||
}
|
||||
|
||||
/// A read only view over all devices belonging to a user.
|
||||
#[derive(Debug)]
|
||||
pub struct UserDevices {
|
||||
entries: ReadOnlyView<DeviceId, Device>,
|
||||
}
|
||||
|
||||
impl UserDevices {
|
||||
/// Get the specific device with the given device id.
|
||||
pub fn get(&self, device_id: &str) -> Option<Device> {
|
||||
self.entries.get(device_id).cloned()
|
||||
}
|
||||
|
||||
/// Iterator over all the device ids of the user devices.
|
||||
pub fn keys(&self) -> impl Iterator<Item = &DeviceId> {
|
||||
self.entries.keys()
|
||||
}
|
||||
|
||||
/// Iterator over all the devices of the user devices.
|
||||
pub fn devices(&self) -> impl Iterator<Item = &Device> {
|
||||
self.entries.values()
|
||||
}
|
||||
entries: Arc<DashMap<UserId, DashMap<Box<DeviceId>, ReadOnlyDevice>>>,
|
||||
}
|
||||
|
||||
impl DeviceStore {
|
||||
@@ -166,21 +156,17 @@ impl DeviceStore {
|
||||
/// Add a device to the store.
|
||||
///
|
||||
/// Returns true if the device was already in the store, false otherwise.
|
||||
pub fn add(&self, device: Device) -> bool {
|
||||
pub fn add(&self, device: ReadOnlyDevice) -> bool {
|
||||
let user_id = device.user_id();
|
||||
|
||||
if !self.entries.contains_key(&user_id) {
|
||||
self.entries.insert(user_id.clone(), DashMap::new());
|
||||
}
|
||||
let device_map = self.entries.get_mut(&user_id).unwrap();
|
||||
|
||||
device_map
|
||||
.insert(device.device_id().to_owned(), device)
|
||||
self.entries
|
||||
.entry(user_id.to_owned())
|
||||
.or_insert_with(DashMap::new)
|
||||
.insert(device.device_id().into(), device)
|
||||
.is_none()
|
||||
}
|
||||
|
||||
/// Get the device with the given device_id and belonging to the given user.
|
||||
pub fn get(&self, user_id: &UserId, device_id: &str) -> Option<Device> {
|
||||
pub fn get(&self, user_id: &UserId, device_id: &DeviceId) -> Option<ReadOnlyDevice> {
|
||||
self.entries
|
||||
.get(user_id)
|
||||
.and_then(|m| m.get(device_id).map(|d| d.value().clone()))
|
||||
@@ -189,7 +175,7 @@ impl DeviceStore {
|
||||
/// Remove the device with the given device_id and belonging to the given user.
|
||||
///
|
||||
/// Returns the device if it was removed, None if it wasn't in the store.
|
||||
pub fn remove(&self, user_id: &UserId, device_id: &str) -> Option<Device> {
|
||||
pub fn remove(&self, user_id: &UserId, device_id: &DeviceId) -> Option<ReadOnlyDevice> {
|
||||
self.entries
|
||||
.get(user_id)
|
||||
.and_then(|m| m.remove(device_id))
|
||||
@@ -197,31 +183,30 @@ impl DeviceStore {
|
||||
}
|
||||
|
||||
/// Get a read-only view over all devices of the given user.
|
||||
pub fn user_devices(&self, user_id: &UserId) -> UserDevices {
|
||||
if !self.entries.contains_key(user_id) {
|
||||
self.entries.insert(user_id.clone(), DashMap::new());
|
||||
}
|
||||
UserDevices {
|
||||
entries: self.entries.get(user_id).unwrap().clone().into_read_only(),
|
||||
}
|
||||
pub fn user_devices(&self, user_id: &UserId) -> HashMap<DeviceIdBox, ReadOnlyDevice> {
|
||||
self.entries
|
||||
.entry(user_id.clone())
|
||||
.or_insert_with(DashMap::new)
|
||||
.iter()
|
||||
.map(|i| (i.key().to_owned(), i.value().clone()))
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use crate::device::test::get_device;
|
||||
use crate::memory_stores::{DeviceStore, GroupSessionStore, SessionStore};
|
||||
use crate::olm::test::get_account_and_session;
|
||||
use crate::olm::{InboundGroupSession, OutboundGroupSession};
|
||||
use matrix_sdk_common::identifiers::RoomId;
|
||||
use crate::{
|
||||
identities::device::test::get_device,
|
||||
olm::{test::get_account_and_session, InboundGroupSession},
|
||||
store::caches::{DeviceStore, GroupSessionStore, SessionStore},
|
||||
};
|
||||
use matrix_sdk_common::identifiers::room_id;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_session_store() {
|
||||
let (_, session) = get_account_and_session().await;
|
||||
|
||||
let mut store = SessionStore::new();
|
||||
let store = SessionStore::new();
|
||||
|
||||
assert!(store.add(session.clone()).await);
|
||||
assert!(!store.add(session.clone()).await);
|
||||
@@ -238,7 +223,7 @@ mod test {
|
||||
async fn test_session_store_bulk_storing() {
|
||||
let (_, session) = get_account_and_session().await;
|
||||
|
||||
let mut store = SessionStore::new();
|
||||
let store = SessionStore::new();
|
||||
store.set_for_sender(&session.sender_key, vec![session.clone()]);
|
||||
|
||||
let sessions = store.get(&session.sender_key).unwrap();
|
||||
@@ -251,9 +236,13 @@ mod test {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_group_session_store() {
|
||||
let room_id = RoomId::try_from("!test:localhost").unwrap();
|
||||
let (account, _) = get_account_and_session().await;
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
let outbound = OutboundGroupSession::new(&room_id);
|
||||
let (outbound, _) = account
|
||||
.create_group_session_pair_with_defaults(&room_id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(0, outbound.message_index().await);
|
||||
assert!(!outbound.shared());
|
||||
@@ -268,7 +257,7 @@ mod test {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut store = GroupSessionStore::new();
|
||||
let store = GroupSessionStore::new();
|
||||
store.add(inbound.clone());
|
||||
|
||||
let loaded_session = store
|
||||
@@ -291,12 +280,12 @@ mod test {
|
||||
|
||||
let user_devices = store.user_devices(device.user_id());
|
||||
|
||||
assert_eq!(user_devices.keys().nth(0).unwrap(), device.device_id());
|
||||
assert_eq!(user_devices.devices().nth(0).unwrap(), &device);
|
||||
assert_eq!(&**user_devices.keys().next().unwrap(), device.device_id());
|
||||
assert_eq!(user_devices.values().next().unwrap(), &device);
|
||||
|
||||
let loaded_device = user_devices.get(device.device_id()).unwrap();
|
||||
|
||||
assert_eq!(device, loaded_device);
|
||||
assert_eq!(&device, loaded_device);
|
||||
|
||||
store.remove(device.user_id(), device.device_id());
|
||||
|
||||
@@ -12,66 +12,133 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use matrix_sdk_common::{
|
||||
async_trait,
|
||||
identifiers::{DeviceId, DeviceIdBox, RoomId, UserId},
|
||||
locks::Mutex,
|
||||
};
|
||||
|
||||
use super::{Account, CryptoStore, InboundGroupSession, Result, Session};
|
||||
use crate::device::Device;
|
||||
use crate::memory_stores::{DeviceStore, GroupSessionStore, SessionStore, UserDevices};
|
||||
use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId};
|
||||
use super::{
|
||||
caches::{DeviceStore, GroupSessionStore, SessionStore},
|
||||
Changes, CryptoStore, InboundGroupSession, ReadOnlyAccount, Result, Session,
|
||||
};
|
||||
use crate::{
|
||||
identities::{ReadOnlyDevice, UserIdentities},
|
||||
olm::PrivateCrossSigningIdentity,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// An in-memory only store that will forget all the E2EE key once it's dropped.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MemoryStore {
|
||||
sessions: SessionStore,
|
||||
inbound_group_sessions: GroupSessionStore,
|
||||
tracked_users: HashSet<UserId>,
|
||||
users_for_key_query: HashSet<UserId>,
|
||||
tracked_users: Arc<DashSet<UserId>>,
|
||||
users_for_key_query: Arc<DashSet<UserId>>,
|
||||
olm_hashes: Arc<DashMap<String, DashSet<String>>>,
|
||||
devices: DeviceStore,
|
||||
identities: Arc<DashMap<UserId, UserIdentities>>,
|
||||
values: Arc<DashMap<String, String>>,
|
||||
}
|
||||
|
||||
impl MemoryStore {
|
||||
pub fn new() -> Self {
|
||||
impl Default for MemoryStore {
|
||||
fn default() -> Self {
|
||||
MemoryStore {
|
||||
sessions: SessionStore::new(),
|
||||
inbound_group_sessions: GroupSessionStore::new(),
|
||||
tracked_users: HashSet::new(),
|
||||
users_for_key_query: HashSet::new(),
|
||||
tracked_users: Arc::new(DashSet::new()),
|
||||
users_for_key_query: Arc::new(DashSet::new()),
|
||||
olm_hashes: Arc::new(DashMap::new()),
|
||||
devices: DeviceStore::new(),
|
||||
identities: Arc::new(DashMap::new()),
|
||||
values: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MemoryStore {
|
||||
/// Create a new empty `MemoryStore`.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
pub(crate) async fn save_devices(&self, mut devices: Vec<ReadOnlyDevice>) {
|
||||
for device in devices.drain(..) {
|
||||
let _ = self.devices.add(device);
|
||||
}
|
||||
}
|
||||
|
||||
async fn delete_devices(&self, mut devices: Vec<ReadOnlyDevice>) {
|
||||
for device in devices.drain(..) {
|
||||
let _ = self.devices.remove(device.user_id(), device.device_id());
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_sessions(&self, mut sessions: Vec<Session>) {
|
||||
for session in sessions.drain(..) {
|
||||
let _ = self.sessions.add(session.clone()).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn save_inbound_group_sessions(&self, mut sessions: Vec<InboundGroupSession>) {
|
||||
for session in sessions.drain(..) {
|
||||
self.inbound_group_sessions.add(session);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl CryptoStore for MemoryStore {
|
||||
async fn load_account(&mut self) -> Result<Option<Account>> {
|
||||
async fn load_account(&self) -> Result<Option<ReadOnlyAccount>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn save_account(&mut self, _: Account) -> Result<()> {
|
||||
async fn save_account(&self, _: ReadOnlyAccount) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_sessions(&mut self, sessions: &[Session]) -> Result<()> {
|
||||
for session in sessions {
|
||||
let _ = self.sessions.add(session.clone()).await;
|
||||
async fn save_changes(&self, mut changes: Changes) -> Result<()> {
|
||||
self.save_sessions(changes.sessions).await;
|
||||
self.save_inbound_group_sessions(changes.inbound_group_sessions)
|
||||
.await;
|
||||
|
||||
self.save_devices(changes.devices.new).await;
|
||||
self.save_devices(changes.devices.changed).await;
|
||||
self.delete_devices(changes.devices.deleted).await;
|
||||
|
||||
for identity in changes
|
||||
.identities
|
||||
.new
|
||||
.drain(..)
|
||||
.chain(changes.identities.changed)
|
||||
{
|
||||
let _ = self
|
||||
.identities
|
||||
.insert(identity.user_id().to_owned(), identity.clone());
|
||||
}
|
||||
|
||||
for hash in changes.message_hashes {
|
||||
self.olm_hashes
|
||||
.entry(hash.sender_key.to_owned())
|
||||
.or_insert_with(DashSet::new)
|
||||
.insert(hash.hash.clone());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_sessions(&mut self, sender_key: &str) -> Result<Option<Arc<Mutex<Vec<Session>>>>> {
|
||||
async fn get_sessions(&self, sender_key: &str) -> Result<Option<Arc<Mutex<Vec<Session>>>>> {
|
||||
Ok(self.sessions.get(sender_key))
|
||||
}
|
||||
|
||||
async fn save_inbound_group_session(&mut self, session: InboundGroupSession) -> Result<bool> {
|
||||
Ok(self.inbound_group_sessions.add(session))
|
||||
}
|
||||
|
||||
async fn get_inbound_group_session(
|
||||
&mut self,
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
sender_key: &str,
|
||||
session_id: &str,
|
||||
@@ -81,15 +148,36 @@ impl CryptoStore for MemoryStore {
|
||||
.get(room_id, sender_key, session_id))
|
||||
}
|
||||
|
||||
fn tracked_users(&self) -> &HashSet<UserId> {
|
||||
&self.tracked_users
|
||||
async fn get_inbound_group_sessions(&self) -> Result<Vec<InboundGroupSession>> {
|
||||
Ok(self.inbound_group_sessions.get_all())
|
||||
}
|
||||
|
||||
fn users_for_key_query(&self) -> &HashSet<UserId> {
|
||||
&self.users_for_key_query
|
||||
fn users_for_key_query(&self) -> HashSet<UserId> {
|
||||
#[allow(clippy::map_clone)]
|
||||
self.users_for_key_query.iter().map(|u| u.clone()).collect()
|
||||
}
|
||||
|
||||
async fn update_tracked_user(&mut self, user: &UserId, dirty: bool) -> Result<bool> {
|
||||
fn is_user_tracked(&self, user_id: &UserId) -> bool {
|
||||
self.tracked_users.contains(user_id)
|
||||
}
|
||||
|
||||
fn has_users_for_key_query(&self) -> bool {
|
||||
!self.users_for_key_query.is_empty()
|
||||
}
|
||||
|
||||
async fn update_tracked_user(&self, user: &UserId, dirty: bool) -> Result<bool> {
|
||||
// TODO to prevent a race between the sync and a key query in flight we
|
||||
// need to have an additional state to mention that the user changed.
|
||||
//
|
||||
// A simple counter could be used for this or enum with two states, e.g.
|
||||
// The counter would work as follows:
|
||||
// * 0 -> User is synced, no need for a key query.
|
||||
// * 1 -> A sync has marked the user as dirty.
|
||||
// * 2 -> A sync has marked the user again as dirty, before we got a
|
||||
// successful key query response.
|
||||
//
|
||||
// The counter would top out at 2 since there won't be a race between 3
|
||||
// different key queries syncs.
|
||||
if dirty {
|
||||
self.users_for_key_query.insert(user.clone());
|
||||
} else {
|
||||
@@ -99,49 +187,71 @@ impl CryptoStore for MemoryStore {
|
||||
Ok(self.tracked_users.insert(user.clone()))
|
||||
}
|
||||
|
||||
#[allow(clippy::ptr_arg)]
|
||||
async fn get_device(&self, user_id: &UserId, device_id: &DeviceId) -> Result<Option<Device>> {
|
||||
async fn get_device(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
device_id: &DeviceId,
|
||||
) -> Result<Option<ReadOnlyDevice>> {
|
||||
Ok(self.devices.get(user_id, device_id))
|
||||
}
|
||||
|
||||
async fn delete_device(&self, device: Device) -> Result<()> {
|
||||
let _ = self.devices.remove(device.user_id(), device.device_id());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_user_devices(&self, user_id: &UserId) -> Result<UserDevices> {
|
||||
async fn get_user_devices(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
) -> Result<HashMap<DeviceIdBox, ReadOnlyDevice>> {
|
||||
Ok(self.devices.user_devices(user_id))
|
||||
}
|
||||
|
||||
async fn save_devices(&self, devices: &[Device]) -> Result<()> {
|
||||
for device in devices {
|
||||
let _ = self.devices.add(device.clone());
|
||||
}
|
||||
async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<UserIdentities>> {
|
||||
#[allow(clippy::map_clone)]
|
||||
Ok(self.identities.get(user_id).map(|i| i.clone()))
|
||||
}
|
||||
|
||||
async fn save_value(&self, key: String, value: String) -> Result<()> {
|
||||
self.values.insert(key, value);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_value(&self, key: &str) -> Result<()> {
|
||||
self.values.remove(key);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_value(&self, key: &str) -> Result<Option<String>> {
|
||||
Ok(self.values.get(key).map(|v| v.to_owned()))
|
||||
}
|
||||
|
||||
async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
async fn is_message_known(&self, message_hash: &crate::olm::OlmMessageHash) -> Result<bool> {
|
||||
Ok(self
|
||||
.olm_hashes
|
||||
.entry(message_hash.sender_key.to_owned())
|
||||
.or_insert_with(DashSet::new)
|
||||
.contains(&message_hash.hash))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use crate::device::test::get_device;
|
||||
use crate::olm::test::get_account_and_session;
|
||||
use crate::olm::{InboundGroupSession, OutboundGroupSession};
|
||||
use crate::store::memorystore::MemoryStore;
|
||||
use crate::store::CryptoStore;
|
||||
use matrix_sdk_common::identifiers::RoomId;
|
||||
use crate::{
|
||||
identities::device::test::get_device,
|
||||
olm::{test::get_account_and_session, InboundGroupSession, OlmMessageHash},
|
||||
store::{memorystore::MemoryStore, Changes, CryptoStore},
|
||||
};
|
||||
use matrix_sdk_common::identifiers::room_id;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_session_store() {
|
||||
let (account, session) = get_account_and_session().await;
|
||||
let mut store = MemoryStore::new();
|
||||
let store = MemoryStore::new();
|
||||
|
||||
assert!(store.load_account().await.unwrap().is_none());
|
||||
store.save_account(account).await.unwrap();
|
||||
|
||||
store.save_sessions(&[session.clone()]).await.unwrap();
|
||||
store.save_sessions(vec![session.clone()]).await;
|
||||
|
||||
let sessions = store
|
||||
.get_sessions(&session.sender_key)
|
||||
@@ -157,9 +267,13 @@ mod test {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_group_session_store() {
|
||||
let room_id = RoomId::try_from("!test:localhost").unwrap();
|
||||
let (account, _) = get_account_and_session().await;
|
||||
let room_id = room_id!("!test:localhost");
|
||||
|
||||
let outbound = OutboundGroupSession::new(&room_id);
|
||||
let (outbound, _) = account
|
||||
.create_group_session_pair_with_defaults(&room_id)
|
||||
.await
|
||||
.unwrap();
|
||||
let inbound = InboundGroupSession::new(
|
||||
"test_key",
|
||||
"test_key",
|
||||
@@ -168,11 +282,10 @@ mod test {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut store = MemoryStore::new();
|
||||
let store = MemoryStore::new();
|
||||
let _ = store
|
||||
.save_inbound_group_session(inbound.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
.save_inbound_group_sessions(vec![inbound.clone()])
|
||||
.await;
|
||||
|
||||
let loaded_session = store
|
||||
.get_inbound_group_session(&room_id, "test_key", outbound.session_id())
|
||||
@@ -187,7 +300,7 @@ mod test {
|
||||
let device = get_device();
|
||||
let store = MemoryStore::new();
|
||||
|
||||
store.save_devices(&[device.clone()]).await.unwrap();
|
||||
store.save_devices(vec![device.clone()]).await;
|
||||
|
||||
let loaded_device = store
|
||||
.get_device(device.user_id(), device.device_id())
|
||||
@@ -199,14 +312,14 @@ mod test {
|
||||
|
||||
let user_devices = store.get_user_devices(device.user_id()).await.unwrap();
|
||||
|
||||
assert_eq!(user_devices.keys().nth(0).unwrap(), device.device_id());
|
||||
assert_eq!(user_devices.devices().nth(0).unwrap(), &device);
|
||||
assert_eq!(&**user_devices.keys().next().unwrap(), device.device_id());
|
||||
assert_eq!(user_devices.values().next().unwrap(), &device);
|
||||
|
||||
let loaded_device = user_devices.get(device.device_id()).unwrap();
|
||||
|
||||
assert_eq!(device, loaded_device);
|
||||
assert_eq!(&device, loaded_device);
|
||||
|
||||
store.delete_device(device.clone()).await.unwrap();
|
||||
store.delete_devices(vec![device.clone()]).await;
|
||||
assert!(store
|
||||
.get_device(device.user_id(), device.device_id())
|
||||
.await
|
||||
@@ -217,7 +330,7 @@ mod test {
|
||||
#[tokio::test]
|
||||
async fn test_tracked_users() {
|
||||
let device = get_device();
|
||||
let mut store = MemoryStore::new();
|
||||
let store = MemoryStore::new();
|
||||
|
||||
assert!(store
|
||||
.update_tracked_user(device.user_id(), false)
|
||||
@@ -228,8 +341,23 @@ mod test {
|
||||
.await
|
||||
.unwrap());
|
||||
|
||||
let tracked_users = store.tracked_users();
|
||||
assert!(store.is_user_tracked(device.user_id()));
|
||||
}
|
||||
|
||||
let _ = tracked_users.contains(device.user_id());
|
||||
#[tokio::test]
|
||||
async fn test_message_hash() {
|
||||
let store = MemoryStore::new();
|
||||
|
||||
let hash = OlmMessageHash {
|
||||
sender_key: "test_sender".to_owned(),
|
||||
hash: "test_hash".to_owned(),
|
||||
};
|
||||
|
||||
let mut changes = Changes::default();
|
||||
changes.message_hashes.push(hash.clone());
|
||||
|
||||
assert!(!store.is_message_known(&hash).await.unwrap());
|
||||
store.save_changes(changes).await.unwrap();
|
||||
assert!(store.is_message_known(&hash).await.unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,30 +12,275 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use core::fmt::Debug;
|
||||
use std::collections::HashSet;
|
||||
use std::io::Error as IoError;
|
||||
use std::sync::Arc;
|
||||
use url::ParseError;
|
||||
//! Types and traits to implement the storage layer for the [`OlmMachine`]
|
||||
//!
|
||||
//! The storage layer for the [`OlmMachine`] can be customized using a trait.
|
||||
//! Implementing your own [`CryptoStore`]
|
||||
//!
|
||||
//! An in-memory only store is provided as well as a SQLite based one, depending
|
||||
//! on your needs and targets a custom store may be implemented, e.g. for
|
||||
//! `wasm-unknown-unknown` an indexeddb store would be needed
|
||||
//!
|
||||
//! ```
|
||||
//! # use matrix_sdk_crypto::{
|
||||
//! # OlmMachine,
|
||||
//! # store::MemoryStore,
|
||||
//! # };
|
||||
//! # use matrix_sdk_common::identifiers::{user_id, DeviceIdBox};
|
||||
//! # let user_id = user_id!("@example:localhost");
|
||||
//! # let device_id: DeviceIdBox = "TEST".into();
|
||||
//! let store = Box::new(MemoryStore::new());
|
||||
//!
|
||||
//! let machine = OlmMachine::new_with_store(user_id, device_id, store);
|
||||
//! ```
|
||||
//!
|
||||
//! [`OlmMachine`]: /matrix_sdk_crypto/struct.OlmMachine.html
|
||||
//! [`CryptoStore`]: trait.Cryptostore.html
|
||||
|
||||
use async_trait::async_trait;
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
pub mod caches;
|
||||
mod memorystore;
|
||||
mod pickle_key;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(feature = "sqlite_cryptostore")]
|
||||
pub(crate) mod sqlite;
|
||||
|
||||
pub use memorystore::MemoryStore;
|
||||
pub use pickle_key::{EncryptedPickleKey, PickleKey};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(feature = "sqlite_cryptostore")]
|
||||
pub use sqlite::SqliteStore;
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
fmt::Debug,
|
||||
io::Error as IoError,
|
||||
ops::Deref,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use olm_rs::errors::{OlmAccountError, OlmGroupSessionError, OlmSessionError};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Error as SerdeError;
|
||||
use thiserror::Error;
|
||||
|
||||
use super::device::Device;
|
||||
use super::memory_stores::UserDevices;
|
||||
use super::olm::{Account, InboundGroupSession, Session};
|
||||
use matrix_sdk_common::identifiers::{DeviceId, RoomId, UserId};
|
||||
use olm_rs::errors::{OlmAccountError, OlmGroupSessionError, OlmSessionError};
|
||||
|
||||
pub mod memorystore;
|
||||
#[cfg(feature = "sqlite-cryptostore")]
|
||||
pub mod sqlite;
|
||||
|
||||
#[cfg(feature = "sqlite-cryptostore")]
|
||||
#[cfg_attr(feature = "docs", doc(cfg(r#sqlite_cryptostore)))]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[cfg(feature = "sqlite_cryptostore")]
|
||||
use sqlx::Error as SqlxError;
|
||||
|
||||
use matrix_sdk_common::{
|
||||
async_trait,
|
||||
identifiers::{
|
||||
DeviceId, DeviceIdBox, DeviceKeyAlgorithm, Error as IdentifierValidationError, RoomId,
|
||||
UserId,
|
||||
},
|
||||
locks::Mutex,
|
||||
AsyncTraitDeps,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::SessionUnpicklingError,
|
||||
identities::{Device, ReadOnlyDevice, UserDevices, UserIdentities},
|
||||
olm::{
|
||||
InboundGroupSession, OlmMessageHash, PrivateCrossSigningIdentity, ReadOnlyAccount, Session,
|
||||
},
|
||||
verification::VerificationMachine,
|
||||
};
|
||||
|
||||
/// A `CryptoStore` specific result type.
|
||||
pub type Result<T> = std::result::Result<T, CryptoStoreError>;
|
||||
|
||||
/// A wrapper for our CryptoStore trait object.
|
||||
///
|
||||
/// This is needed because we want to have a generic interface so we can
|
||||
/// store/restore objects that we can serialize. Since trait objects and
|
||||
/// generics don't mix let the CryptoStore store strings and this wrapper
|
||||
/// adds the generic interface on top.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct Store {
|
||||
user_id: Arc<UserId>,
|
||||
identity: Arc<Mutex<PrivateCrossSigningIdentity>>,
|
||||
inner: Arc<Box<dyn CryptoStore>>,
|
||||
verification_machine: VerificationMachine,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct Changes {
|
||||
pub account: Option<ReadOnlyAccount>,
|
||||
pub private_identity: Option<PrivateCrossSigningIdentity>,
|
||||
pub sessions: Vec<Session>,
|
||||
pub message_hashes: Vec<OlmMessageHash>,
|
||||
pub inbound_group_sessions: Vec<InboundGroupSession>,
|
||||
pub identities: IdentityChanges,
|
||||
pub devices: DeviceChanges,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct IdentityChanges {
|
||||
pub new: Vec<UserIdentities>,
|
||||
pub changed: Vec<UserIdentities>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct DeviceChanges {
|
||||
pub new: Vec<ReadOnlyDevice>,
|
||||
pub changed: Vec<ReadOnlyDevice>,
|
||||
pub deleted: Vec<ReadOnlyDevice>,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub fn new(
|
||||
user_id: Arc<UserId>,
|
||||
identity: Arc<Mutex<PrivateCrossSigningIdentity>>,
|
||||
store: Arc<Box<dyn CryptoStore>>,
|
||||
verification_machine: VerificationMachine,
|
||||
) -> Self {
|
||||
Self {
|
||||
user_id,
|
||||
identity,
|
||||
inner: store,
|
||||
verification_machine,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_readonly_device(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
device_id: &DeviceId,
|
||||
) -> Result<Option<ReadOnlyDevice>> {
|
||||
self.inner.get_device(user_id, device_id).await
|
||||
}
|
||||
|
||||
pub async fn save_sessions(&self, sessions: &[Session]) -> Result<()> {
|
||||
let changes = Changes {
|
||||
sessions: sessions.to_vec(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
self.save_changes(changes).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn save_devices(&self, devices: &[ReadOnlyDevice]) -> Result<()> {
|
||||
let changes = Changes {
|
||||
devices: DeviceChanges {
|
||||
changed: devices.to_vec(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
self.save_changes(changes).await
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub async fn save_inbound_group_sessions(
|
||||
&self,
|
||||
sessions: &[InboundGroupSession],
|
||||
) -> Result<()> {
|
||||
let changes = Changes {
|
||||
inbound_group_sessions: sessions.to_vec(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
self.save_changes(changes).await
|
||||
}
|
||||
|
||||
pub async fn get_readonly_devices(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
) -> Result<HashMap<DeviceIdBox, ReadOnlyDevice>> {
|
||||
self.inner.get_user_devices(user_id).await
|
||||
}
|
||||
|
||||
pub async fn get_device_from_curve_key(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
curve_key: &str,
|
||||
) -> Result<Option<Device>> {
|
||||
self.get_user_devices(user_id).await.map(|d| {
|
||||
d.devices().find(|d| {
|
||||
d.get_key(DeviceKeyAlgorithm::Curve25519)
|
||||
.map_or(false, |k| k == curve_key)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_user_devices(&self, user_id: &UserId) -> Result<UserDevices> {
|
||||
let devices = self.inner.get_user_devices(user_id).await?;
|
||||
|
||||
let own_identity = self
|
||||
.inner
|
||||
.get_user_identity(&self.user_id)
|
||||
.await?
|
||||
.map(|i| i.own().cloned())
|
||||
.flatten();
|
||||
let device_owner_identity = self.inner.get_user_identity(user_id).await.ok().flatten();
|
||||
|
||||
Ok(UserDevices {
|
||||
inner: devices,
|
||||
private_identity: self.identity.clone(),
|
||||
verification_machine: self.verification_machine.clone(),
|
||||
own_identity,
|
||||
device_owner_identity,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_device(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
device_id: &DeviceId,
|
||||
) -> Result<Option<Device>> {
|
||||
let own_identity = self
|
||||
.get_user_identity(&self.user_id)
|
||||
.await?
|
||||
.map(|i| i.own().cloned())
|
||||
.flatten();
|
||||
let device_owner_identity = self.get_user_identity(user_id).await?;
|
||||
|
||||
Ok(self
|
||||
.inner
|
||||
.get_device(user_id, device_id)
|
||||
.await?
|
||||
.map(|d| Device {
|
||||
inner: d,
|
||||
private_identity: self.identity.clone(),
|
||||
verification_machine: self.verification_machine.clone(),
|
||||
own_identity,
|
||||
device_owner_identity,
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_object<V: for<'b> Deserialize<'b>>(&self, key: &str) -> Result<Option<V>> {
|
||||
if let Some(value) = self.get_value(key).await? {
|
||||
Ok(Some(serde_json::from_str(&value)?))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn save_object(&self, key: &str, value: &impl Serialize) -> Result<()> {
|
||||
let value = serde_json::to_string(value)?;
|
||||
self.save_value(key.to_owned(), value).await
|
||||
}
|
||||
|
||||
pub async fn delete_object(&self, key: &str) -> Result<()> {
|
||||
self.inner.remove_value(key).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Store {
|
||||
type Target = dyn CryptoStore;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&**self.inner
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
/// The crypto store's error type.
|
||||
pub enum CryptoStoreError {
|
||||
@@ -47,7 +292,7 @@ pub enum CryptoStoreError {
|
||||
/// SQL error occurred.
|
||||
// TODO flatten the SqlxError to make it easier for other store
|
||||
// implementations.
|
||||
#[cfg(feature = "sqlite-cryptostore")]
|
||||
#[cfg(feature = "sqlite_cryptostore")]
|
||||
#[error(transparent)]
|
||||
DatabaseError(#[from] SqlxError),
|
||||
|
||||
@@ -68,57 +313,53 @@ pub enum CryptoStoreError {
|
||||
OlmGroupSession(#[from] OlmGroupSessionError),
|
||||
|
||||
/// A session time-stamp couldn't be loaded.
|
||||
#[error("can't load session timestamps")]
|
||||
SessionTimestampError,
|
||||
#[error(transparent)]
|
||||
SessionUnpickling(#[from] SessionUnpicklingError),
|
||||
|
||||
/// Failed to decrypt an pickled object.
|
||||
#[error("An object failed to be decrypted while unpickling")]
|
||||
UnpicklingError,
|
||||
|
||||
/// A Matirx identifier failed to be validated.
|
||||
#[error(transparent)]
|
||||
IdentifierValidation(#[from] IdentifierValidationError),
|
||||
|
||||
/// The store failed to (de)serialize a data type.
|
||||
#[error(transparent)]
|
||||
Serialization(#[from] SerdeError),
|
||||
|
||||
/// An error occurred while parsing an URL.
|
||||
#[error(transparent)]
|
||||
UrlParse(#[from] ParseError),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, CryptoStoreError>;
|
||||
|
||||
#[async_trait]
|
||||
/// Trait abstracting a store that the `OlmMachine` uses to store cryptographic
|
||||
/// keys.
|
||||
pub trait CryptoStore: Debug + Send + Sync {
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait CryptoStore: AsyncTraitDeps {
|
||||
/// Load an account that was previously stored.
|
||||
async fn load_account(&mut self) -> Result<Option<Account>>;
|
||||
async fn load_account(&self) -> Result<Option<ReadOnlyAccount>>;
|
||||
|
||||
/// Save the given account in the store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `account` - The account that should be stored.
|
||||
async fn save_account(&mut self, account: Account) -> Result<()>;
|
||||
async fn save_account(&self, account: ReadOnlyAccount) -> Result<()>;
|
||||
|
||||
/// Save the given sessions in the store.
|
||||
/// Try to load a private cross signing identity, if one is stored.
|
||||
async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>>;
|
||||
|
||||
/// Save the set of changes to the store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `session` - The sessions that should be stored.
|
||||
async fn save_sessions(&mut self, session: &[Session]) -> Result<()>;
|
||||
/// * `changes` - The set of changes that should be stored.
|
||||
async fn save_changes(&self, changes: Changes) -> Result<()>;
|
||||
|
||||
/// Get all the sessions that belong to the given sender key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `sender_key` - The sender key that was used to establish the sessions.
|
||||
async fn get_sessions(&mut self, sender_key: &str) -> Result<Option<Arc<Mutex<Vec<Session>>>>>;
|
||||
|
||||
/// Save the given inbound group session in the store.
|
||||
///
|
||||
/// If the session wasn't already in the store true is returned, false
|
||||
/// otherwise.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `session` - The session that should be stored.
|
||||
async fn save_inbound_group_session(&mut self, session: InboundGroupSession) -> Result<bool>;
|
||||
async fn get_sessions(&self, sender_key: &str) -> Result<Option<Arc<Mutex<Vec<Session>>>>>;
|
||||
|
||||
/// Get the inbound group session from our store.
|
||||
///
|
||||
@@ -129,18 +370,24 @@ pub trait CryptoStore: Debug + Send + Sync {
|
||||
///
|
||||
/// * `session_id` - The unique id of the session.
|
||||
async fn get_inbound_group_session(
|
||||
&mut self,
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
sender_key: &str,
|
||||
session_id: &str,
|
||||
) -> Result<Option<InboundGroupSession>>;
|
||||
|
||||
/// Get the set of tracked users.
|
||||
fn tracked_users(&self) -> &HashSet<UserId>;
|
||||
/// Get all the inbound group sessions we have stored.
|
||||
async fn get_inbound_group_sessions(&self) -> Result<Vec<InboundGroupSession>>;
|
||||
|
||||
/// Is the given user already tracked.
|
||||
fn is_user_tracked(&self, user_id: &UserId) -> bool;
|
||||
|
||||
/// Are there any tracked users that are marked as dirty.
|
||||
fn has_users_for_key_query(&self) -> bool;
|
||||
|
||||
/// Set of users that we need to query keys for. This is a subset of
|
||||
/// the tracked users.
|
||||
fn users_for_key_query(&self) -> &HashSet<UserId>;
|
||||
fn users_for_key_query(&self) -> HashSet<UserId>;
|
||||
|
||||
/// Add an user for tracking.
|
||||
///
|
||||
@@ -151,21 +398,7 @@ pub trait CryptoStore: Debug + Send + Sync {
|
||||
/// * `user` - The user that should be marked as tracked.
|
||||
///
|
||||
/// * `dirty` - Should the user be also marked for a key query.
|
||||
async fn update_tracked_user(&mut self, user: &UserId, dirty: bool) -> Result<bool>;
|
||||
|
||||
/// Save the given devices in the store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `device` - The device that should be stored.
|
||||
async fn save_devices(&self, devices: &[Device]) -> Result<()>;
|
||||
|
||||
/// Delete the given device from the store.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `device` - The device that should be stored.
|
||||
async fn delete_device(&self, device: Device) -> Result<()>;
|
||||
async fn update_tracked_user(&self, user: &UserId, dirty: bool) -> Result<bool>;
|
||||
|
||||
/// Get the device for the given user with the given device id.
|
||||
///
|
||||
@@ -174,14 +407,38 @@ pub trait CryptoStore: Debug + Send + Sync {
|
||||
/// * `user_id` - The user that the device belongs to.
|
||||
///
|
||||
/// * `device_id` - The unique id of the device.
|
||||
#[allow(clippy::ptr_arg)]
|
||||
async fn get_device(&self, user_id: &UserId, device_id: &DeviceId) -> Result<Option<Device>>;
|
||||
async fn get_device(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
device_id: &DeviceId,
|
||||
) -> Result<Option<ReadOnlyDevice>>;
|
||||
|
||||
/// Get all the devices of the given user.
|
||||
///
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The user for which we should get all the devices.
|
||||
async fn get_user_devices(&self, user_id: &UserId) -> Result<UserDevices>;
|
||||
async fn get_user_devices(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
) -> Result<HashMap<DeviceIdBox, ReadOnlyDevice>>;
|
||||
|
||||
/// Get the user identity that is attached to the given user id.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `user_id` - The user for which we should get the identity.
|
||||
async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<UserIdentities>>;
|
||||
|
||||
/// Save a serializeable object in the store.
|
||||
async fn save_value(&self, key: String, value: String) -> Result<()>;
|
||||
|
||||
/// Remove a value from the store.
|
||||
async fn remove_value(&self, key: &str) -> Result<()>;
|
||||
|
||||
/// Load a serializeable object from the store.
|
||||
async fn get_value(&self, key: &str) -> Result<Option<String>>;
|
||||
|
||||
/// Check if a hash for an Olm message stored in the database.
|
||||
async fn is_message_known(&self, message_hash: &OlmMessageHash) -> Result<bool>;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
// 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.
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use aes_gcm::{
|
||||
aead::{generic_array::GenericArray, Aead, NewAead},
|
||||
Aes256Gcm, Error as DecryptionError,
|
||||
};
|
||||
use getrandom::getrandom;
|
||||
use hmac::Hmac;
|
||||
use olm_rs::PicklingMode;
|
||||
use pbkdf2::pbkdf2;
|
||||
use sha2::Sha256;
|
||||
use zeroize::{Zeroize, Zeroizing};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
const KEY_SIZE: usize = 32;
|
||||
const NONCE_SIZE: usize = 12;
|
||||
const KDF_SALT_SIZE: usize = 32;
|
||||
const KDF_ROUNDS: u32 = 10000;
|
||||
|
||||
/// Version specific info for the key derivation method that is used.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum KdfInfo {
|
||||
Pbkdf2 {
|
||||
/// The number of PBKDF rounds that were used when deriving the AES key.
|
||||
rounds: u32,
|
||||
},
|
||||
}
|
||||
|
||||
/// Version specific info for encryption method that is used to encrypt our
|
||||
/// pickle key.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub enum CipherTextInfo {
|
||||
Aes256Gcm {
|
||||
/// The nonce that was used to encrypt the ciphertext.
|
||||
nonce: Vec<u8>,
|
||||
/// The encrypted pickle key.
|
||||
ciphertext: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
/// An encrypted version of our pickle key, this can be safely stored in a
|
||||
/// database.
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct EncryptedPickleKey {
|
||||
/// Info about the key derivation method that was used to expand the
|
||||
/// passphrase into an encryption key.
|
||||
pub kdf_info: KdfInfo,
|
||||
/// The ciphertext with it's accompanying additional data that is needed to
|
||||
/// decrypt the pickle key.
|
||||
pub ciphertext_info: CipherTextInfo,
|
||||
/// The salt that was used when the passphrase was expanded into a AES key.
|
||||
kdf_salt: Vec<u8>,
|
||||
}
|
||||
|
||||
/// A pickle key that will be used to encrypt all the private keys for Olm.
|
||||
///
|
||||
/// Olm uses AES256 to encrypt accounts, sessions, inbound group sessions. We
|
||||
/// also implement our own pickling for the cross-signing types using
|
||||
/// AES256-GCM so the key sizes match.
|
||||
#[derive(Debug, Zeroize, PartialEq)]
|
||||
pub struct PickleKey {
|
||||
aes256_key: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Default for PickleKey {
|
||||
fn default() -> Self {
|
||||
let mut key = vec![0u8; KEY_SIZE];
|
||||
getrandom(&mut key).expect("Can't generate new pickle key");
|
||||
|
||||
Self { aes256_key: key }
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Vec<u8>> for PickleKey {
|
||||
type Error = ();
|
||||
fn try_from(value: Vec<u8>) -> Result<Self, Self::Error> {
|
||||
if value.len() != KEY_SIZE {
|
||||
Err(())
|
||||
} else {
|
||||
Ok(Self { aes256_key: value })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickleKey {
|
||||
/// Generate a new random pickle key.
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn expand_key(passphrase: &str, salt: &[u8], rounds: u32) -> Zeroizing<Vec<u8>> {
|
||||
let mut key = Zeroizing::from(vec![0u8; KEY_SIZE]);
|
||||
pbkdf2::<Hmac<Sha256>>(passphrase.as_bytes(), &salt, rounds, &mut *key);
|
||||
key
|
||||
}
|
||||
|
||||
/// Get a `PicklingMode` version of this pickle key.
|
||||
pub fn pickle_mode(&self) -> PicklingMode {
|
||||
PicklingMode::Encrypted {
|
||||
key: self.aes256_key.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the raw AES256 key.
|
||||
pub fn key(&self) -> &[u8] {
|
||||
&self.aes256_key
|
||||
}
|
||||
|
||||
/// Encrypt and export our pickle key using the given passphrase.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `passphrase` - The passphrase that should be used to encrypt the
|
||||
/// pickle key.
|
||||
pub fn encrypt(&self, passphrase: &str) -> EncryptedPickleKey {
|
||||
let mut salt = vec![0u8; KDF_SALT_SIZE];
|
||||
getrandom(&mut salt).expect("Can't generate new random pickle key");
|
||||
|
||||
let key = PickleKey::expand_key(passphrase, &salt, KDF_ROUNDS);
|
||||
let key = GenericArray::from_slice(key.as_ref());
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
|
||||
let mut nonce = vec![0u8; NONCE_SIZE];
|
||||
getrandom(&mut nonce).expect("Can't generate new random nonce for the pickle key");
|
||||
|
||||
let ciphertext = cipher
|
||||
.encrypt(
|
||||
&GenericArray::from_slice(nonce.as_ref()),
|
||||
self.aes256_key.as_slice(),
|
||||
)
|
||||
.expect("Can't encrypt pickle key");
|
||||
|
||||
EncryptedPickleKey {
|
||||
kdf_info: KdfInfo::Pbkdf2 { rounds: KDF_ROUNDS },
|
||||
kdf_salt: salt,
|
||||
ciphertext_info: CipherTextInfo::Aes256Gcm { nonce, ciphertext },
|
||||
}
|
||||
}
|
||||
|
||||
/// Restore a pickle key from an encrypted export.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `passphrase` - The passphrase that should be used to encrypt the
|
||||
/// pickle key.
|
||||
///
|
||||
/// * `encrypted` - The exported and encrypted version of the pickle key.
|
||||
pub fn from_encrypted(
|
||||
passphrase: &str,
|
||||
encrypted: EncryptedPickleKey,
|
||||
) -> Result<Self, DecryptionError> {
|
||||
let key = match encrypted.kdf_info {
|
||||
KdfInfo::Pbkdf2 { rounds } => Self::expand_key(passphrase, &encrypted.kdf_salt, rounds),
|
||||
};
|
||||
|
||||
let key = GenericArray::from_slice(key.as_ref());
|
||||
|
||||
let decrypted = match encrypted.ciphertext_info {
|
||||
CipherTextInfo::Aes256Gcm { nonce, ciphertext } => {
|
||||
let cipher = Aes256Gcm::new(&key);
|
||||
let nonce = GenericArray::from_slice(&nonce);
|
||||
cipher.decrypt(nonce, ciphertext.as_ref())?
|
||||
}
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
aes256_key: decrypted,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::PickleKey;
|
||||
|
||||
#[test]
|
||||
fn generating() {
|
||||
PickleKey::new();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encrypting() {
|
||||
let passphrase = "it's a secret to everybody";
|
||||
let pickle_key = PickleKey::new();
|
||||
|
||||
let encrypted = pickle_key.encrypt(passphrase);
|
||||
let decrypted = PickleKey::from_encrypted(passphrase, encrypted).unwrap();
|
||||
|
||||
assert_eq!(pickle_key, decrypted);
|
||||
}
|
||||
}
|
||||
+1696
-400
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
// 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.
|
||||
|
||||
pub use base64::DecodeError;
|
||||
use base64::{decode_config, encode_config, STANDARD_NO_PAD, URL_SAFE_NO_PAD};
|
||||
|
||||
/// Decode the input as base64 with no padding.
|
||||
pub fn decode(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
|
||||
decode_config(input, STANDARD_NO_PAD)
|
||||
}
|
||||
|
||||
/// Decode the input as URL safe base64 with no padding.
|
||||
pub fn decode_url_safe(input: impl AsRef<[u8]>) -> Result<Vec<u8>, DecodeError> {
|
||||
decode_config(input, URL_SAFE_NO_PAD)
|
||||
}
|
||||
|
||||
/// Encode the input as base64 with no padding.
|
||||
pub fn encode(input: impl AsRef<[u8]>) -> String {
|
||||
encode_config(input, STANDARD_NO_PAD)
|
||||
}
|
||||
|
||||
/// Encode the input as URL safe base64 with no padding.
|
||||
pub fn encode_url_safe(input: impl AsRef<[u8]>) -> String {
|
||||
encode_config(input, URL_SAFE_NO_PAD)
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
// 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.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use dashmap::DashMap;
|
||||
|
||||
use matrix_sdk_common::locks::Mutex;
|
||||
use tracing::{trace, warn};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
events::{AnyToDeviceEvent, AnyToDeviceEventContent},
|
||||
identifiers::{DeviceId, UserId},
|
||||
uuid::Uuid,
|
||||
};
|
||||
|
||||
use super::sas::{content_to_request, Sas, VerificationResult};
|
||||
use crate::{
|
||||
olm::PrivateCrossSigningIdentity,
|
||||
requests::{OutgoingRequest, ToDeviceRequest},
|
||||
store::{CryptoStore, CryptoStoreError},
|
||||
ReadOnlyAccount, ReadOnlyDevice,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct VerificationMachine {
|
||||
account: ReadOnlyAccount,
|
||||
private_identity: Arc<Mutex<PrivateCrossSigningIdentity>>,
|
||||
pub(crate) store: Arc<Box<dyn CryptoStore>>,
|
||||
verifications: Arc<DashMap<String, Sas>>,
|
||||
outgoing_to_device_messages: Arc<DashMap<Uuid, OutgoingRequest>>,
|
||||
}
|
||||
|
||||
impl VerificationMachine {
|
||||
pub(crate) fn new(
|
||||
account: ReadOnlyAccount,
|
||||
identity: Arc<Mutex<PrivateCrossSigningIdentity>>,
|
||||
store: Arc<Box<dyn CryptoStore>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
account,
|
||||
private_identity: identity,
|
||||
store,
|
||||
verifications: Arc::new(DashMap::new()),
|
||||
outgoing_to_device_messages: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn start_sas(
|
||||
&self,
|
||||
device: ReadOnlyDevice,
|
||||
) -> Result<(Sas, ToDeviceRequest), CryptoStoreError> {
|
||||
let identity = self.store.get_user_identity(device.user_id()).await?;
|
||||
let private_identity = self.private_identity.lock().await.clone();
|
||||
|
||||
let (sas, content) = Sas::start(
|
||||
self.account.clone(),
|
||||
private_identity,
|
||||
device.clone(),
|
||||
self.store.clone(),
|
||||
identity,
|
||||
);
|
||||
|
||||
let request = content_to_request(
|
||||
device.user_id(),
|
||||
device.device_id(),
|
||||
AnyToDeviceEventContent::KeyVerificationStart(content),
|
||||
);
|
||||
|
||||
self.verifications
|
||||
.insert(sas.flow_id().to_owned(), sas.clone());
|
||||
|
||||
Ok((sas, request))
|
||||
}
|
||||
|
||||
pub fn get_sas(&self, transaction_id: &str) -> Option<Sas> {
|
||||
#[allow(clippy::map_clone)]
|
||||
self.verifications.get(transaction_id).map(|s| s.clone())
|
||||
}
|
||||
|
||||
fn queue_up_content(
|
||||
&self,
|
||||
recipient: &UserId,
|
||||
recipient_device: &DeviceId,
|
||||
content: AnyToDeviceEventContent,
|
||||
) {
|
||||
let request = content_to_request(recipient, recipient_device, content);
|
||||
let request_id = request.txn_id;
|
||||
|
||||
let request = OutgoingRequest {
|
||||
request_id,
|
||||
request: Arc::new(request.into()),
|
||||
};
|
||||
|
||||
self.outgoing_to_device_messages.insert(request_id, request);
|
||||
}
|
||||
|
||||
fn receive_event_helper(&self, sas: &Sas, event: &mut AnyToDeviceEvent) {
|
||||
if let Some(c) = sas.receive_event(event) {
|
||||
self.queue_up_content(sas.other_user_id(), sas.other_device_id(), c);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_request_as_sent(&self, uuid: &Uuid) {
|
||||
self.outgoing_to_device_messages.remove(uuid);
|
||||
}
|
||||
|
||||
pub fn outgoing_to_device_requests(&self) -> Vec<OutgoingRequest> {
|
||||
#[allow(clippy::map_clone)]
|
||||
self.outgoing_to_device_messages
|
||||
.iter()
|
||||
.map(|r| (*r).clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn garbage_collect(&self) {
|
||||
self.verifications
|
||||
.retain(|_, s| !(s.is_done() || s.is_canceled()));
|
||||
|
||||
for sas in self.verifications.iter() {
|
||||
if let Some(r) = sas.cancel_if_timed_out() {
|
||||
self.outgoing_to_device_messages.insert(
|
||||
r.txn_id,
|
||||
OutgoingRequest {
|
||||
request_id: r.txn_id,
|
||||
request: Arc::new(r.into()),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn receive_event(
|
||||
&self,
|
||||
event: &mut AnyToDeviceEvent,
|
||||
) -> Result<(), CryptoStoreError> {
|
||||
trace!("Received a key verification event {:?}", event);
|
||||
|
||||
match event {
|
||||
AnyToDeviceEvent::KeyVerificationStart(e) => {
|
||||
trace!(
|
||||
"Received a m.key.verification start event from {} {}",
|
||||
e.sender,
|
||||
e.content.from_device
|
||||
);
|
||||
|
||||
if let Some(d) = self
|
||||
.store
|
||||
.get_device(&e.sender, &e.content.from_device)
|
||||
.await?
|
||||
{
|
||||
let private_identity = self.private_identity.lock().await.clone();
|
||||
match Sas::from_start_event(
|
||||
self.account.clone(),
|
||||
private_identity,
|
||||
d,
|
||||
self.store.clone(),
|
||||
e,
|
||||
self.store.get_user_identity(&e.sender).await?,
|
||||
) {
|
||||
Ok(s) => {
|
||||
self.verifications
|
||||
.insert(e.content.transaction_id.clone(), s);
|
||||
}
|
||||
Err(c) => {
|
||||
warn!(
|
||||
"Can't start key verification with {} {}, canceling: {:?}",
|
||||
e.sender, e.content.from_device, c
|
||||
);
|
||||
self.queue_up_content(&e.sender, &e.content.from_device, c)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"Received a key verification start event from an unknown device {} {}",
|
||||
e.sender, e.content.from_device
|
||||
);
|
||||
}
|
||||
}
|
||||
AnyToDeviceEvent::KeyVerificationCancel(e) => {
|
||||
self.verifications.remove(&e.content.transaction_id);
|
||||
}
|
||||
AnyToDeviceEvent::KeyVerificationAccept(e) => {
|
||||
if let Some(s) = self.get_sas(&e.content.transaction_id) {
|
||||
self.receive_event_helper(&s, event)
|
||||
};
|
||||
}
|
||||
AnyToDeviceEvent::KeyVerificationKey(e) => {
|
||||
if let Some(s) = self.get_sas(&e.content.transaction_id) {
|
||||
self.receive_event_helper(&s, event)
|
||||
};
|
||||
}
|
||||
AnyToDeviceEvent::KeyVerificationMac(e) => {
|
||||
if let Some(s) = self.get_sas(&e.content.transaction_id) {
|
||||
self.receive_event_helper(&s, event);
|
||||
|
||||
if s.is_done() {
|
||||
match s.mark_as_done().await? {
|
||||
VerificationResult::Ok => (),
|
||||
VerificationResult::Cancel(r) => {
|
||||
self.outgoing_to_device_messages.insert(
|
||||
r.txn_id,
|
||||
OutgoingRequest {
|
||||
request_id: r.txn_id,
|
||||
request: Arc::new(r.into()),
|
||||
},
|
||||
);
|
||||
}
|
||||
VerificationResult::SignatureUpload(r) => {
|
||||
let request_id = Uuid::new_v4();
|
||||
|
||||
self.outgoing_to_device_messages.insert(
|
||||
request_id,
|
||||
OutgoingRequest {
|
||||
request_id,
|
||||
request: Arc::new(r.into()),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
events::AnyToDeviceEventContent,
|
||||
identifiers::{DeviceId, UserId},
|
||||
locks::Mutex,
|
||||
};
|
||||
|
||||
use super::{Sas, VerificationMachine};
|
||||
use crate::{
|
||||
olm::PrivateCrossSigningIdentity,
|
||||
requests::OutgoingRequests,
|
||||
store::{CryptoStore, MemoryStore},
|
||||
verification::test::{get_content_from_request, wrap_any_to_device_content},
|
||||
ReadOnlyAccount, ReadOnlyDevice,
|
||||
};
|
||||
|
||||
fn alice_id() -> UserId {
|
||||
UserId::try_from("@alice:example.org").unwrap()
|
||||
}
|
||||
|
||||
fn alice_device_id() -> Box<DeviceId> {
|
||||
"JLAFKJWSCS".into()
|
||||
}
|
||||
|
||||
fn bob_id() -> UserId {
|
||||
UserId::try_from("@bob:example.org").unwrap()
|
||||
}
|
||||
|
||||
fn bob_device_id() -> Box<DeviceId> {
|
||||
"BOBDEVCIE".into()
|
||||
}
|
||||
|
||||
async fn setup_verification_machine() -> (VerificationMachine, Sas) {
|
||||
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
|
||||
let bob = ReadOnlyAccount::new(&bob_id(), &bob_device_id());
|
||||
let store = MemoryStore::new();
|
||||
let bob_store = MemoryStore::new();
|
||||
|
||||
let bob_device = ReadOnlyDevice::from_account(&bob).await;
|
||||
let alice_device = ReadOnlyDevice::from_account(&alice).await;
|
||||
|
||||
store.save_devices(vec![bob_device]).await;
|
||||
bob_store.save_devices(vec![alice_device.clone()]).await;
|
||||
|
||||
let bob_store: Arc<Box<dyn CryptoStore>> = Arc::new(Box::new(bob_store));
|
||||
let identity = Arc::new(Mutex::new(PrivateCrossSigningIdentity::empty(alice_id())));
|
||||
let machine = VerificationMachine::new(alice, identity, Arc::new(Box::new(store)));
|
||||
let (bob_sas, start_content) = Sas::start(
|
||||
bob,
|
||||
PrivateCrossSigningIdentity::empty(bob_id()),
|
||||
alice_device,
|
||||
bob_store,
|
||||
None,
|
||||
);
|
||||
machine
|
||||
.receive_event(&mut wrap_any_to_device_content(
|
||||
bob_sas.user_id(),
|
||||
AnyToDeviceEventContent::KeyVerificationStart(start_content),
|
||||
))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
(machine, bob_sas)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create() {
|
||||
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
|
||||
let identity = Arc::new(Mutex::new(PrivateCrossSigningIdentity::empty(alice_id())));
|
||||
let store = MemoryStore::new();
|
||||
let _ = VerificationMachine::new(alice, identity, Arc::new(Box::new(store)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn full_flow() {
|
||||
let (alice_machine, bob) = setup_verification_machine().await;
|
||||
|
||||
let alice = alice_machine.get_sas(bob.flow_id()).unwrap();
|
||||
|
||||
let mut event = alice
|
||||
.accept()
|
||||
.map(|c| wrap_any_to_device_content(alice.user_id(), get_content_from_request(&c)))
|
||||
.unwrap();
|
||||
|
||||
let mut event = bob
|
||||
.receive_event(&mut event)
|
||||
.map(|c| wrap_any_to_device_content(bob.user_id(), c))
|
||||
.unwrap();
|
||||
|
||||
assert!(alice_machine.outgoing_to_device_messages.is_empty());
|
||||
alice_machine.receive_event(&mut event).await.unwrap();
|
||||
assert!(!alice_machine.outgoing_to_device_messages.is_empty());
|
||||
|
||||
let request = alice_machine
|
||||
.outgoing_to_device_messages
|
||||
.iter()
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
let txn_id = *request.request_id();
|
||||
|
||||
let r = if let OutgoingRequests::ToDeviceRequest(r) = request.request() {
|
||||
r
|
||||
} else {
|
||||
panic!("Invalid request type");
|
||||
};
|
||||
|
||||
let mut event = wrap_any_to_device_content(alice.user_id(), get_content_from_request(r));
|
||||
drop(request);
|
||||
alice_machine.mark_request_as_sent(&txn_id);
|
||||
|
||||
assert!(bob.receive_event(&mut event).is_none());
|
||||
|
||||
assert!(alice.emoji().is_some());
|
||||
assert!(bob.emoji().is_some());
|
||||
|
||||
assert_eq!(alice.emoji(), bob.emoji());
|
||||
|
||||
let mut event = wrap_any_to_device_content(
|
||||
alice.user_id(),
|
||||
get_content_from_request(&alice.confirm().await.unwrap().0.unwrap()),
|
||||
);
|
||||
bob.receive_event(&mut event);
|
||||
|
||||
let mut event = wrap_any_to_device_content(
|
||||
bob.user_id(),
|
||||
get_content_from_request(&bob.confirm().await.unwrap().0.unwrap()),
|
||||
);
|
||||
alice.receive_event(&mut event);
|
||||
|
||||
assert!(alice.is_done());
|
||||
assert!(bob.is_done());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[tokio::test]
|
||||
async fn timing_out() {
|
||||
let (alice_machine, bob) = setup_verification_machine().await;
|
||||
let alice = alice_machine.get_sas(bob.flow_id()).unwrap();
|
||||
|
||||
assert!(!alice.timed_out());
|
||||
assert!(alice_machine.outgoing_to_device_messages.is_empty());
|
||||
|
||||
// This line panics on macOS, so we're disabled for now.
|
||||
alice.set_creation_time(Instant::now() - Duration::from_secs(60 * 15));
|
||||
assert!(alice.timed_out());
|
||||
assert!(alice_machine.outgoing_to_device_messages.is_empty());
|
||||
alice_machine.garbage_collect();
|
||||
assert!(!alice_machine.outgoing_to_device_messages.is_empty());
|
||||
alice_machine.garbage_collect();
|
||||
assert!(alice_machine.verifications.is_empty());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// 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.
|
||||
|
||||
mod machine;
|
||||
mod sas;
|
||||
|
||||
pub use machine::VerificationMachine;
|
||||
pub use sas::{Sas, VerificationResult};
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod test {
|
||||
use crate::requests::{OutgoingRequest, OutgoingRequests, ToDeviceRequest};
|
||||
use serde_json::Value;
|
||||
|
||||
use matrix_sdk_common::{
|
||||
events::{AnyToDeviceEvent, AnyToDeviceEventContent, EventType, ToDeviceEvent},
|
||||
identifiers::UserId,
|
||||
};
|
||||
|
||||
pub(crate) fn request_to_event(sender: &UserId, request: &ToDeviceRequest) -> AnyToDeviceEvent {
|
||||
let content = get_content_from_request(request);
|
||||
wrap_any_to_device_content(sender, content)
|
||||
}
|
||||
|
||||
pub(crate) fn outgoing_request_to_event(
|
||||
sender: &UserId,
|
||||
request: &OutgoingRequest,
|
||||
) -> AnyToDeviceEvent {
|
||||
match request.request() {
|
||||
OutgoingRequests::ToDeviceRequest(r) => request_to_event(sender, r),
|
||||
_ => panic!("Unsupported outgoing request"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wrap_any_to_device_content(
|
||||
sender: &UserId,
|
||||
content: AnyToDeviceEventContent,
|
||||
) -> AnyToDeviceEvent {
|
||||
match content {
|
||||
AnyToDeviceEventContent::KeyVerificationKey(c) => {
|
||||
AnyToDeviceEvent::KeyVerificationKey(ToDeviceEvent {
|
||||
sender: sender.clone(),
|
||||
content: c,
|
||||
})
|
||||
}
|
||||
AnyToDeviceEventContent::KeyVerificationStart(c) => {
|
||||
AnyToDeviceEvent::KeyVerificationStart(ToDeviceEvent {
|
||||
sender: sender.clone(),
|
||||
content: c,
|
||||
})
|
||||
}
|
||||
AnyToDeviceEventContent::KeyVerificationAccept(c) => {
|
||||
AnyToDeviceEvent::KeyVerificationAccept(ToDeviceEvent {
|
||||
sender: sender.clone(),
|
||||
content: c,
|
||||
})
|
||||
}
|
||||
AnyToDeviceEventContent::KeyVerificationMac(c) => {
|
||||
AnyToDeviceEvent::KeyVerificationMac(ToDeviceEvent {
|
||||
sender: sender.clone(),
|
||||
content: c,
|
||||
})
|
||||
}
|
||||
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_content_from_request(request: &ToDeviceRequest) -> AnyToDeviceEventContent {
|
||||
let json: Value = serde_json::from_str(
|
||||
request
|
||||
.messages
|
||||
.values()
|
||||
.next()
|
||||
.unwrap()
|
||||
.values()
|
||||
.next()
|
||||
.unwrap()
|
||||
.get(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
match request.event_type {
|
||||
EventType::KeyVerificationStart => {
|
||||
AnyToDeviceEventContent::KeyVerificationStart(serde_json::from_value(json).unwrap())
|
||||
}
|
||||
EventType::KeyVerificationKey => {
|
||||
AnyToDeviceEventContent::KeyVerificationKey(serde_json::from_value(json).unwrap())
|
||||
}
|
||||
EventType::KeyVerificationAccept => AnyToDeviceEventContent::KeyVerificationAccept(
|
||||
serde_json::from_value(json).unwrap(),
|
||||
),
|
||||
EventType::KeyVerificationMac => {
|
||||
AnyToDeviceEventContent::KeyVerificationMac(serde_json::from_value(json).unwrap())
|
||||
}
|
||||
EventType::KeyVerificationCancel => AnyToDeviceEventContent::KeyVerificationCancel(
|
||||
serde_json::from_value(json).unwrap(),
|
||||
),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
// 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.
|
||||
|
||||
use std::{collections::BTreeMap, convert::TryInto};
|
||||
|
||||
use sha2::{Digest, Sha256};
|
||||
use tracing::{trace, warn};
|
||||
|
||||
use olm_rs::sas::OlmSas;
|
||||
|
||||
use matrix_sdk_common::{
|
||||
api::r0::to_device::DeviceIdOrAllDevices,
|
||||
events::{
|
||||
key::verification::{
|
||||
cancel::CancelCode, mac::MacToDeviceEventContent, start::StartToDeviceEventContent,
|
||||
},
|
||||
AnyToDeviceEventContent, EventType, ToDeviceEvent,
|
||||
},
|
||||
identifiers::{DeviceId, DeviceKeyAlgorithm, DeviceKeyId, UserId},
|
||||
uuid::Uuid,
|
||||
CanonicalJsonValue,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
identities::{ReadOnlyDevice, UserIdentities},
|
||||
utilities::encode,
|
||||
ReadOnlyAccount, ToDeviceRequest,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SasIds {
|
||||
pub account: ReadOnlyAccount,
|
||||
pub other_device: ReadOnlyDevice,
|
||||
pub other_identity: Option<UserIdentities>,
|
||||
}
|
||||
|
||||
/// Calculate the commitment for a accept event from the public key and the
|
||||
/// start event.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `public_key` - Our own ephemeral public key that is used for the
|
||||
/// interactive verification.
|
||||
///
|
||||
/// * `content` - The `m.key.verification.start` event content that started the
|
||||
/// interactive verification process.
|
||||
pub fn calculate_commitment(public_key: &str, content: &StartToDeviceEventContent) -> String {
|
||||
let json_content: CanonicalJsonValue = serde_json::to_value(content)
|
||||
.expect("Can't serialize content")
|
||||
.try_into()
|
||||
.expect("Can't canonicalize content");
|
||||
|
||||
encode(
|
||||
Sha256::new()
|
||||
.chain(&format!("{}{}", public_key, json_content))
|
||||
.finalize(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Get a tuple of an emoji and a description of the emoji using a number.
|
||||
///
|
||||
/// This is taken directly from the [spec]
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// The spec defines 64 unique emojis, this function panics if the index is
|
||||
/// bigger than 63.
|
||||
///
|
||||
/// [spec]: https://matrix.org/docs/spec/client_server/latest#sas-method-emoji
|
||||
fn emoji_from_index(index: u8) -> (&'static str, &'static str) {
|
||||
match index {
|
||||
0 => ("🐶", "Dog"),
|
||||
1 => ("🐱", "Cat"),
|
||||
2 => ("🦁", "Lion"),
|
||||
3 => ("🐎", "Horse"),
|
||||
4 => ("🦄", "Unicorn"),
|
||||
5 => ("🐷", "Pig"),
|
||||
6 => ("🐘", "Elephant"),
|
||||
7 => ("🐰", "Rabbit"),
|
||||
8 => ("🐼", "Panda"),
|
||||
9 => ("🐓", "Rooster"),
|
||||
10 => ("🐧", "Penguin"),
|
||||
11 => ("🐢", "Turtle"),
|
||||
12 => ("🐟", "Fish"),
|
||||
13 => ("🐙", "Octopus"),
|
||||
14 => ("🦋", "Butterfly"),
|
||||
15 => ("🌷", "Flower"),
|
||||
16 => ("🌳", "Tree"),
|
||||
17 => ("🌵", "Cactus"),
|
||||
18 => ("🍄", "Mushroom"),
|
||||
19 => ("🌏", "Globe"),
|
||||
20 => ("🌙", "Moon"),
|
||||
21 => ("☁️", "Cloud"),
|
||||
22 => ("🔥", "Fire"),
|
||||
23 => ("🍌", "Banana"),
|
||||
24 => ("🍎", "Apple"),
|
||||
25 => ("🍓", "Strawberry"),
|
||||
26 => ("🌽", "Corn"),
|
||||
27 => ("🍕", "Pizza"),
|
||||
28 => ("🎂", "Cake"),
|
||||
29 => ("❤️", "Heart"),
|
||||
30 => ("😀", "Smiley"),
|
||||
31 => ("🤖", "Robot"),
|
||||
32 => ("🎩", "Hat"),
|
||||
33 => ("👓", "Glasses"),
|
||||
34 => ("🔧", "Spanner"),
|
||||
35 => ("🎅", "Santa"),
|
||||
36 => ("👍", "Thumbs up"),
|
||||
37 => ("☂️", "Umbrella"),
|
||||
38 => ("⌛", "Hourglass"),
|
||||
39 => ("⏰", "Clock"),
|
||||
40 => ("🎁", "Gift"),
|
||||
41 => ("💡", "Light Bulb"),
|
||||
42 => ("📕", "Book"),
|
||||
43 => ("✏️", "Pencil"),
|
||||
44 => ("📎", "Paperclip"),
|
||||
45 => ("✂️", "Scissors"),
|
||||
46 => ("🔒", "Lock"),
|
||||
47 => ("🔑", "Key"),
|
||||
48 => ("🔨", "Hammer"),
|
||||
49 => ("☎️", "Telephone"),
|
||||
50 => ("🏁", "Flag"),
|
||||
51 => ("🚂", "Train"),
|
||||
52 => ("🚲", "Bicycle"),
|
||||
53 => ("✈️", "Airplane"),
|
||||
54 => ("🚀", "Rocket"),
|
||||
55 => ("🏆", "Trophy"),
|
||||
56 => ("⚽", "Ball"),
|
||||
57 => ("🎸", "Guitar"),
|
||||
58 => ("🎺", "Trumpet"),
|
||||
59 => ("🔔", "Bell"),
|
||||
60 => ("⚓", "Anchor"),
|
||||
61 => ("🎧", "Headphones"),
|
||||
62 => ("📁", "Folder"),
|
||||
63 => ("📌", "Pin"),
|
||||
_ => panic!("Trying to fetch an emoji outside the allowed range"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the extra info that will be used when we check the MAC of a
|
||||
/// m.key.verification.key event.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `ids` - The ids that are used for this SAS authentication flow.
|
||||
///
|
||||
/// * `flow_id` - The unique id that identifies this SAS verification process.
|
||||
fn extra_mac_info_receive(ids: &SasIds, flow_id: &str) -> String {
|
||||
format!(
|
||||
"MATRIX_KEY_VERIFICATION_MAC{first_user}{first_device}\
|
||||
{second_user}{second_device}{transaction_id}",
|
||||
first_user = ids.other_device.user_id(),
|
||||
first_device = ids.other_device.device_id(),
|
||||
second_user = ids.account.user_id(),
|
||||
second_device = ids.account.device_id(),
|
||||
transaction_id = flow_id,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the content for a m.key.verification.mac event.
|
||||
///
|
||||
/// Returns a tuple that contains the list of verified devices and the list of
|
||||
/// verified master keys.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `sas` - The Olm SAS object that can be used to MACs
|
||||
///
|
||||
/// * `ids` - The ids that are used for this SAS authentication flow.
|
||||
///
|
||||
/// * `flow_id` - The unique id that identifies this SAS verification process.
|
||||
///
|
||||
/// * `event` - The m.key.verification.mac event that was sent to us by
|
||||
/// the other side.
|
||||
pub fn receive_mac_event(
|
||||
sas: &OlmSas,
|
||||
ids: &SasIds,
|
||||
flow_id: &str,
|
||||
event: &ToDeviceEvent<MacToDeviceEventContent>,
|
||||
) -> Result<(Vec<ReadOnlyDevice>, Vec<UserIdentities>), CancelCode> {
|
||||
let mut verified_devices = Vec::new();
|
||||
let mut verified_identities = Vec::new();
|
||||
|
||||
let info = extra_mac_info_receive(&ids, flow_id);
|
||||
|
||||
trace!(
|
||||
"Received a key.verification.mac event from {} {}",
|
||||
event.sender,
|
||||
ids.other_device.device_id()
|
||||
);
|
||||
|
||||
let mut keys = event.content.mac.keys().cloned().collect::<Vec<String>>();
|
||||
keys.sort();
|
||||
let keys = sas
|
||||
.calculate_mac(&keys.join(","), &format!("{}KEY_IDS", &info))
|
||||
.expect("Can't calculate SAS MAC");
|
||||
|
||||
if keys != event.content.keys {
|
||||
return Err(CancelCode::KeyMismatch);
|
||||
}
|
||||
|
||||
for (key_id, key_mac) in &event.content.mac {
|
||||
trace!(
|
||||
"Checking MAC for the key id {} from {} {}",
|
||||
key_id,
|
||||
event.sender,
|
||||
ids.other_device.device_id()
|
||||
);
|
||||
let key_id: DeviceKeyId = match key_id.as_str().try_into() {
|
||||
Ok(id) => id,
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
||||
if let Some(key) = ids.other_device.keys().get(&key_id) {
|
||||
if key_mac
|
||||
== &sas
|
||||
.calculate_mac(key, &format!("{}{}", info, key_id))
|
||||
.expect("Can't calculate SAS MAC")
|
||||
{
|
||||
verified_devices.push(ids.other_device.clone());
|
||||
} else {
|
||||
return Err(CancelCode::KeyMismatch);
|
||||
}
|
||||
} else if let Some(identity) = &ids.other_identity {
|
||||
if let Some(key) = identity.master_key().get_key(&key_id) {
|
||||
// TODO we should check that the master key signs the device,
|
||||
// this way we know the master key also trusts the device
|
||||
if key_mac
|
||||
== &sas
|
||||
.calculate_mac(key, &format!("{}{}", info, key_id))
|
||||
.expect("Can't calculate SAS MAC")
|
||||
{
|
||||
trace!(
|
||||
"Successfully verified the master key {} from {}",
|
||||
key_id,
|
||||
event.sender
|
||||
);
|
||||
verified_identities.push(identity.clone())
|
||||
} else {
|
||||
return Err(CancelCode::KeyMismatch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"Key ID {} in MAC event from {} {} doesn't belong to any device \
|
||||
or user identity",
|
||||
key_id,
|
||||
event.sender,
|
||||
ids.other_device.device_id()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((verified_devices, verified_identities))
|
||||
}
|
||||
|
||||
/// Get the extra info that will be used when we generate a MAC and need to send
|
||||
/// it out
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `ids` - The ids that are used for this SAS authentication flow.
|
||||
///
|
||||
/// * `flow_id` - The unique id that identifies this SAS verification process.
|
||||
fn extra_mac_info_send(ids: &SasIds, flow_id: &str) -> String {
|
||||
format!(
|
||||
"MATRIX_KEY_VERIFICATION_MAC{first_user}{first_device}\
|
||||
{second_user}{second_device}{transaction_id}",
|
||||
first_user = ids.account.user_id(),
|
||||
first_device = ids.account.device_id(),
|
||||
second_user = ids.other_device.user_id(),
|
||||
second_device = ids.other_device.device_id(),
|
||||
transaction_id = flow_id,
|
||||
)
|
||||
}
|
||||
|
||||
/// Get the content for a m.key.verification.mac event.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `sas` - The Olm SAS object that can be used to generate the MAC
|
||||
///
|
||||
/// * `ids` - The ids that are used for this SAS authentication flow.
|
||||
///
|
||||
/// * `flow_id` - The unique id that identifies this SAS verification process.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This will panic if the public key of the other side wasn't set.
|
||||
pub fn get_mac_content(sas: &OlmSas, ids: &SasIds, flow_id: &str) -> MacToDeviceEventContent {
|
||||
let mut mac: BTreeMap<String, String> = BTreeMap::new();
|
||||
|
||||
let key_id = DeviceKeyId::from_parts(DeviceKeyAlgorithm::Ed25519, ids.account.device_id());
|
||||
let key = ids.account.identity_keys().ed25519();
|
||||
let info = extra_mac_info_send(ids, flow_id);
|
||||
|
||||
mac.insert(
|
||||
key_id.to_string(),
|
||||
sas.calculate_mac(key, &format!("{}{}", info, key_id))
|
||||
.expect("Can't calculate SAS MAC"),
|
||||
);
|
||||
|
||||
// TODO Add the cross signing master key here if we trust/have it.
|
||||
|
||||
let mut keys = mac.keys().cloned().collect::<Vec<String>>();
|
||||
keys.sort();
|
||||
let keys = sas
|
||||
.calculate_mac(&keys.join(","), &format!("{}KEY_IDS", &info))
|
||||
.expect("Can't calculate SAS MAC");
|
||||
|
||||
MacToDeviceEventContent {
|
||||
transaction_id: flow_id.to_owned(),
|
||||
keys,
|
||||
mac,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the extra info that will be used when we generate bytes for the short
|
||||
/// auth string.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `ids` - The ids that are used for this SAS authentication flow.
|
||||
///
|
||||
/// * `flow_id` - The unique id that identifies this SAS verification process.
|
||||
///
|
||||
/// * `we_started` - Flag signaling if the SAS process was started on our side.
|
||||
fn extra_info_sas(
|
||||
ids: &SasIds,
|
||||
own_pubkey: &str,
|
||||
their_pubkey: &str,
|
||||
flow_id: &str,
|
||||
we_started: bool,
|
||||
) -> String {
|
||||
let our_info = format!(
|
||||
"{}|{}|{}",
|
||||
ids.account.user_id(),
|
||||
ids.account.device_id(),
|
||||
own_pubkey
|
||||
);
|
||||
let their_info = format!(
|
||||
"{}|{}|{}",
|
||||
ids.other_device.user_id(),
|
||||
ids.other_device.device_id(),
|
||||
their_pubkey
|
||||
);
|
||||
|
||||
let (first_info, second_info) = if we_started {
|
||||
(our_info, their_info)
|
||||
} else {
|
||||
(their_info, our_info)
|
||||
};
|
||||
|
||||
let info = format!(
|
||||
"MATRIX_KEY_VERIFICATION_SAS|{first_info}|{second_info}|{flow_id}",
|
||||
first_info = first_info,
|
||||
second_info = second_info,
|
||||
flow_id = flow_id,
|
||||
);
|
||||
|
||||
trace!("Generated a SAS extra info: {}", info);
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
/// Get the emoji version of the short authentication string.
|
||||
///
|
||||
/// Returns a vector of tuples where the first element is the emoji and the
|
||||
/// second element the English description of the emoji.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `sas` - The Olm SAS object that can be used to generate bytes using the
|
||||
/// shared secret.
|
||||
///
|
||||
/// * `ids` - The ids that are used for this SAS authentication flow.
|
||||
///
|
||||
/// * `flow_id` - The unique id that identifies this SAS verification process.
|
||||
///
|
||||
/// * `we_started` - Flag signaling if the SAS process was started on our side.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This will panic if the public key of the other side wasn't set.
|
||||
pub fn get_emoji(
|
||||
sas: &OlmSas,
|
||||
ids: &SasIds,
|
||||
their_pubkey: &str,
|
||||
flow_id: &str,
|
||||
we_started: bool,
|
||||
) -> Vec<(&'static str, &'static str)> {
|
||||
let bytes = sas
|
||||
.generate_bytes(
|
||||
&extra_info_sas(&ids, &sas.public_key(), their_pubkey, &flow_id, we_started),
|
||||
6,
|
||||
)
|
||||
.expect("Can't generate bytes");
|
||||
|
||||
bytes_to_emoji(bytes)
|
||||
}
|
||||
|
||||
fn bytes_to_emoji_index(bytes: Vec<u8>) -> Vec<u8> {
|
||||
let bytes: Vec<u64> = bytes.iter().map(|b| *b as u64).collect();
|
||||
// Join the 6 bytes into one 64 bit unsigned int. This u64 will contain 48
|
||||
// bits from our 6 bytes.
|
||||
let mut num: u64 = bytes[0] << 40;
|
||||
num += bytes[1] << 32;
|
||||
num += bytes[2] << 24;
|
||||
num += bytes[3] << 16;
|
||||
num += bytes[4] << 8;
|
||||
num += bytes[5];
|
||||
|
||||
// Take the top 42 bits of our 48 bits from the u64 and convert each 6 bits
|
||||
// into a 6 bit number.
|
||||
vec![
|
||||
((num >> 42) & 63) as u8,
|
||||
((num >> 36) & 63) as u8,
|
||||
((num >> 30) & 63) as u8,
|
||||
((num >> 24) & 63) as u8,
|
||||
((num >> 18) & 63) as u8,
|
||||
((num >> 12) & 63) as u8,
|
||||
((num >> 6) & 63) as u8,
|
||||
]
|
||||
}
|
||||
|
||||
fn bytes_to_emoji(bytes: Vec<u8>) -> Vec<(&'static str, &'static str)> {
|
||||
let numbers = bytes_to_emoji_index(bytes);
|
||||
|
||||
// Convert the 6 bit number into a emoji/description tuple.
|
||||
numbers.into_iter().map(emoji_from_index).collect()
|
||||
}
|
||||
|
||||
/// Get the decimal version of the short authentication string.
|
||||
///
|
||||
/// Returns a tuple containing three 4 digit integer numbers that represent
|
||||
/// the short auth string.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `sas` - The Olm SAS object that can be used to generate bytes using the
|
||||
/// shared secret.
|
||||
///
|
||||
/// * `ids` - The ids that are used for this SAS authentication flow.
|
||||
///
|
||||
/// * `flow_id` - The unique id that identifies this SAS verification process.
|
||||
///
|
||||
/// * `we_started` - Flag signaling if the SAS process was started on our side.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This will panic if the public key of the other side wasn't set.
|
||||
pub fn get_decimal(
|
||||
sas: &OlmSas,
|
||||
ids: &SasIds,
|
||||
their_pubkey: &str,
|
||||
flow_id: &str,
|
||||
we_started: bool,
|
||||
) -> (u16, u16, u16) {
|
||||
let bytes = sas
|
||||
.generate_bytes(
|
||||
&extra_info_sas(&ids, &sas.public_key(), their_pubkey, &flow_id, we_started),
|
||||
5,
|
||||
)
|
||||
.expect("Can't generate bytes");
|
||||
|
||||
bytes_to_decimal(bytes)
|
||||
}
|
||||
|
||||
fn bytes_to_decimal(bytes: Vec<u8>) -> (u16, u16, u16) {
|
||||
let bytes: Vec<u16> = bytes.into_iter().map(|b| b as u16).collect();
|
||||
|
||||
// This bitwise operation is taken from the [spec]
|
||||
// [spec]: https://matrix.org/docs/spec/client_server/latest#sas-method-decimal
|
||||
let first = bytes[0] << 5 | bytes[1] >> 3;
|
||||
let second = (bytes[1] & 0x7) << 10 | bytes[2] << 2 | bytes[3] >> 6;
|
||||
let third = (bytes[3] & 0x3F) << 7 | bytes[4] >> 1;
|
||||
|
||||
(first + 1000, second + 1000, third + 1000)
|
||||
}
|
||||
|
||||
pub fn content_to_request(
|
||||
recipient: &UserId,
|
||||
recipient_device: &DeviceId,
|
||||
content: AnyToDeviceEventContent,
|
||||
) -> ToDeviceRequest {
|
||||
let mut messages = BTreeMap::new();
|
||||
let mut user_messages = BTreeMap::new();
|
||||
|
||||
user_messages.insert(
|
||||
DeviceIdOrAllDevices::DeviceId(recipient_device.into()),
|
||||
serde_json::value::to_raw_value(&content).expect("Can't serialize to-device content"),
|
||||
);
|
||||
messages.insert(recipient.clone(), user_messages);
|
||||
|
||||
let event_type = match content {
|
||||
AnyToDeviceEventContent::KeyVerificationAccept(_) => EventType::KeyVerificationAccept,
|
||||
AnyToDeviceEventContent::KeyVerificationStart(_) => EventType::KeyVerificationStart,
|
||||
AnyToDeviceEventContent::KeyVerificationKey(_) => EventType::KeyVerificationKey,
|
||||
AnyToDeviceEventContent::KeyVerificationMac(_) => EventType::KeyVerificationMac,
|
||||
AnyToDeviceEventContent::KeyVerificationCancel(_) => EventType::KeyVerificationCancel,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
ToDeviceRequest {
|
||||
txn_id: Uuid::new_v4(),
|
||||
event_type,
|
||||
messages,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use matrix_sdk_common::events::key::verification::start::StartToDeviceEventContent;
|
||||
use proptest::prelude::*;
|
||||
use serde_json::json;
|
||||
|
||||
use super::{
|
||||
bytes_to_decimal, bytes_to_emoji, bytes_to_emoji_index, calculate_commitment,
|
||||
emoji_from_index,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn commitment_calculation() {
|
||||
let commitment = "CCQmB4JCdB0FW21FdAnHj/Hu8+W9+Nb0vgwPEnZZQ4g";
|
||||
|
||||
let public_key = "Q/NmNFEUS1fS+YeEmiZkjjblKTitrKOAk7cPEumcMlg";
|
||||
let content = json!({
|
||||
"from_device":"XOWLHHFSWM",
|
||||
"transaction_id":"bYxBsirjUJO9osar6ST4i2M2NjrYLA7l",
|
||||
"method":"m.sas.v1",
|
||||
"key_agreement_protocols":["curve25519-hkdf-sha256","curve25519"],
|
||||
"hashes":["sha256"],
|
||||
"message_authentication_codes":["hkdf-hmac-sha256","hmac-sha256"],
|
||||
"short_authentication_string":["decimal","emoji"]
|
||||
});
|
||||
|
||||
let content: StartToDeviceEventContent = serde_json::from_value(content).unwrap();
|
||||
let calculated_commitment = calculate_commitment(public_key, &content);
|
||||
|
||||
assert_eq!(commitment, &calculated_commitment);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emoji_generation() {
|
||||
let bytes = vec![0, 0, 0, 0, 0, 0];
|
||||
let index: Vec<(&'static str, &'static str)> = vec![0, 0, 0, 0, 0, 0, 0]
|
||||
.into_iter()
|
||||
.map(emoji_from_index)
|
||||
.collect();
|
||||
assert_eq!(bytes_to_emoji(bytes), index);
|
||||
|
||||
let bytes = vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
|
||||
|
||||
let index: Vec<(&'static str, &'static str)> = vec![63, 63, 63, 63, 63, 63, 63]
|
||||
.into_iter()
|
||||
.map(emoji_from_index)
|
||||
.collect();
|
||||
assert_eq!(bytes_to_emoji(bytes), index);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decimal_generation() {
|
||||
let bytes = vec![0, 0, 0, 0, 0];
|
||||
let result = bytes_to_decimal(bytes);
|
||||
|
||||
assert_eq!(result, (1000, 1000, 1000));
|
||||
|
||||
let bytes = vec![0xFF, 0xFF, 0xFF, 0xFF, 0xFF];
|
||||
let result = bytes_to_decimal(bytes);
|
||||
assert_eq!(result, (9191, 9191, 9191));
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn proptest_emoji(bytes in prop::array::uniform6(0u8..)) {
|
||||
let numbers = bytes_to_emoji_index(bytes.to_vec());
|
||||
|
||||
for number in numbers {
|
||||
prop_assert!(number < 64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn proptest_decimals(bytes in prop::array::uniform5(0u8..)) {
|
||||
let (first, second, third) = bytes_to_decimal(bytes.to_vec());
|
||||
|
||||
prop_assert!((1000..=9191).contains(&first));
|
||||
prop_assert!((1000..=9191).contains(&second));
|
||||
prop_assert!((1000..=9191).contains(&third));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,969 @@
|
||||
// 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.
|
||||
|
||||
mod helpers;
|
||||
mod sas_state;
|
||||
|
||||
#[cfg(test)]
|
||||
use std::time::Instant;
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tracing::{error, info, trace, warn};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
api::r0::keys::upload_signatures::Request as SignatureUploadRequest,
|
||||
events::{
|
||||
key::verification::{
|
||||
accept::AcceptToDeviceEventContent, cancel::CancelCode, mac::MacToDeviceEventContent,
|
||||
start::StartToDeviceEventContent,
|
||||
},
|
||||
AnyToDeviceEvent, AnyToDeviceEventContent, ToDeviceEvent,
|
||||
},
|
||||
identifiers::{DeviceId, UserId},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
error::SignatureError,
|
||||
identities::{LocalTrust, ReadOnlyDevice, UserIdentities},
|
||||
olm::PrivateCrossSigningIdentity,
|
||||
store::{Changes, CryptoStore, CryptoStoreError, DeviceChanges},
|
||||
ReadOnlyAccount, ToDeviceRequest,
|
||||
};
|
||||
|
||||
pub use helpers::content_to_request;
|
||||
use sas_state::{
|
||||
Accepted, Canceled, Confirmed, Created, Done, KeyReceived, MacReceived, SasState, Started,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A result of a verification flow.
|
||||
pub enum VerificationResult {
|
||||
/// The verification succeeded, nothing needs to be done.
|
||||
Ok,
|
||||
/// The verification was canceled.
|
||||
Cancel(ToDeviceRequest),
|
||||
/// The verification is done and has signatures that need to be uploaded.
|
||||
SignatureUpload(SignatureUploadRequest),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
/// Short authentication string object.
|
||||
pub struct Sas {
|
||||
inner: Arc<Mutex<InnerSas>>,
|
||||
store: Arc<Box<dyn CryptoStore>>,
|
||||
account: ReadOnlyAccount,
|
||||
private_identity: PrivateCrossSigningIdentity,
|
||||
other_device: ReadOnlyDevice,
|
||||
other_identity: Option<UserIdentities>,
|
||||
flow_id: Arc<str>,
|
||||
}
|
||||
|
||||
impl Sas {
|
||||
/// Get our own user id.
|
||||
pub fn user_id(&self) -> &UserId {
|
||||
self.account.user_id()
|
||||
}
|
||||
|
||||
/// Get our own device id.
|
||||
pub fn device_id(&self) -> &DeviceId {
|
||||
self.account.device_id()
|
||||
}
|
||||
|
||||
/// Get the user id of the other side.
|
||||
pub fn other_user_id(&self) -> &UserId {
|
||||
self.other_device.user_id()
|
||||
}
|
||||
|
||||
/// Get the device id of the other side.
|
||||
pub fn other_device_id(&self) -> &DeviceId {
|
||||
self.other_device.device_id()
|
||||
}
|
||||
|
||||
/// Get the device of the other user.
|
||||
pub fn other_device(&self) -> ReadOnlyDevice {
|
||||
self.other_device.clone()
|
||||
}
|
||||
|
||||
/// Get the unique ID that identifies this SAS verification flow.
|
||||
pub fn flow_id(&self) -> &str {
|
||||
&self.flow_id
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn set_creation_time(&self, time: Instant) {
|
||||
self.inner.lock().unwrap().set_creation_time(time)
|
||||
}
|
||||
|
||||
/// Start a new SAS auth flow with the given device.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `account` - Our own account.
|
||||
///
|
||||
/// * `other_device` - The other device which we are going to verify.
|
||||
///
|
||||
/// Returns the new `Sas` object and a `StartEventContent` that needs to be
|
||||
/// sent out through the server to the other device.
|
||||
pub(crate) fn start(
|
||||
account: ReadOnlyAccount,
|
||||
private_identity: PrivateCrossSigningIdentity,
|
||||
other_device: ReadOnlyDevice,
|
||||
store: Arc<Box<dyn CryptoStore>>,
|
||||
other_identity: Option<UserIdentities>,
|
||||
) -> (Sas, StartToDeviceEventContent) {
|
||||
let (inner, content) = InnerSas::start(
|
||||
account.clone(),
|
||||
other_device.clone(),
|
||||
other_identity.clone(),
|
||||
);
|
||||
let flow_id = inner.verification_flow_id();
|
||||
|
||||
let sas = Sas {
|
||||
inner: Arc::new(Mutex::new(inner)),
|
||||
account,
|
||||
private_identity,
|
||||
store,
|
||||
other_device,
|
||||
flow_id,
|
||||
other_identity,
|
||||
};
|
||||
|
||||
(sas, content)
|
||||
}
|
||||
|
||||
/// Create a new Sas object from a m.key.verification.start request.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `account` - Our own account.
|
||||
///
|
||||
/// * `other_device` - The other device which we are going to verify.
|
||||
///
|
||||
/// * `event` - The m.key.verification.start event that was sent to us by
|
||||
/// the other side.
|
||||
pub(crate) fn from_start_event(
|
||||
account: ReadOnlyAccount,
|
||||
private_identity: PrivateCrossSigningIdentity,
|
||||
other_device: ReadOnlyDevice,
|
||||
store: Arc<Box<dyn CryptoStore>>,
|
||||
event: &ToDeviceEvent<StartToDeviceEventContent>,
|
||||
other_identity: Option<UserIdentities>,
|
||||
) -> Result<Sas, AnyToDeviceEventContent> {
|
||||
let inner = InnerSas::from_start_event(
|
||||
account.clone(),
|
||||
other_device.clone(),
|
||||
event,
|
||||
other_identity.clone(),
|
||||
)?;
|
||||
let flow_id = inner.verification_flow_id();
|
||||
|
||||
Ok(Sas {
|
||||
inner: Arc::new(Mutex::new(inner)),
|
||||
account,
|
||||
private_identity,
|
||||
other_device,
|
||||
other_identity,
|
||||
store,
|
||||
flow_id,
|
||||
})
|
||||
}
|
||||
|
||||
/// Accept the SAS verification.
|
||||
///
|
||||
/// This does nothing if the verification was already accepted, otherwise it
|
||||
/// returns an `AcceptEventContent` that needs to be sent out.
|
||||
pub fn accept(&self) -> Option<ToDeviceRequest> {
|
||||
self.inner.lock().unwrap().accept().map(|c| {
|
||||
let content = AnyToDeviceEventContent::KeyVerificationAccept(c);
|
||||
self.content_to_request(content)
|
||||
})
|
||||
}
|
||||
|
||||
/// Confirm the Sas verification.
|
||||
///
|
||||
/// This confirms that the short auth strings match on both sides.
|
||||
///
|
||||
/// Does nothing if we're not in a state where we can confirm the short auth
|
||||
/// string, otherwise returns a `MacEventContent` that needs to be sent to
|
||||
/// the server.
|
||||
pub async fn confirm(
|
||||
&self,
|
||||
) -> Result<(Option<ToDeviceRequest>, Option<SignatureUploadRequest>), CryptoStoreError> {
|
||||
let (content, done) = {
|
||||
let mut guard = self.inner.lock().unwrap();
|
||||
let sas: InnerSas = (*guard).clone();
|
||||
let (sas, content) = sas.confirm();
|
||||
|
||||
*guard = sas;
|
||||
(content, guard.is_done())
|
||||
};
|
||||
|
||||
let mac_request = content
|
||||
.map(|c| self.content_to_request(AnyToDeviceEventContent::KeyVerificationMac(c)));
|
||||
|
||||
if done {
|
||||
match self.mark_as_done().await? {
|
||||
VerificationResult::Cancel(r) => Ok((Some(r), None)),
|
||||
VerificationResult::Ok => Ok((mac_request, None)),
|
||||
VerificationResult::SignatureUpload(r) => Ok((mac_request, Some(r))),
|
||||
}
|
||||
} else {
|
||||
Ok((mac_request, None))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn mark_as_done(&self) -> Result<VerificationResult, CryptoStoreError> {
|
||||
if let Some(device) = self.mark_device_as_verified().await? {
|
||||
let identity = self.mark_identity_as_verified().await?;
|
||||
|
||||
// We only sign devices of our own user here.
|
||||
let signature_request = if device.user_id() == self.user_id() {
|
||||
match self.private_identity.sign_device(&device).await {
|
||||
Ok(r) => Some(r),
|
||||
Err(SignatureError::MissingSigningKey) => {
|
||||
warn!(
|
||||
"Can't sign the device keys for {} {}, \
|
||||
no private user signing key found",
|
||||
device.user_id(),
|
||||
device.device_id(),
|
||||
);
|
||||
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Error signing device keys for {} {} {:?}",
|
||||
device.user_id(),
|
||||
device.device_id(),
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let mut changes = Changes {
|
||||
devices: DeviceChanges {
|
||||
changed: vec![device],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let identity_signature_request = if let Some(i) = identity {
|
||||
// We only sign other users here.
|
||||
let request = if let Some(i) = i.other() {
|
||||
// Signing can fail if the user signing key is missing.
|
||||
match self.private_identity.sign_user(&i).await {
|
||||
Ok(r) => Some(r),
|
||||
Err(SignatureError::MissingSigningKey) => {
|
||||
warn!(
|
||||
"Can't sign the public cross signing keys for {}, \
|
||||
no private user signing key found",
|
||||
i.user_id()
|
||||
);
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Error signing the public cross signing keys for {} {:?}",
|
||||
i.user_id(),
|
||||
e
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
changes.identities.changed.push(i);
|
||||
|
||||
request
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// If there are two signature upload requests, merge them. Otherwise
|
||||
// use the one we have or None.
|
||||
//
|
||||
// Realistically at most one reuqest will be used but let's make
|
||||
// this future proof.
|
||||
let merged_request = if let Some(mut r) = signature_request {
|
||||
if let Some(user_request) = identity_signature_request {
|
||||
r.signed_keys.extend(user_request.signed_keys);
|
||||
Some(r)
|
||||
} else {
|
||||
Some(r)
|
||||
}
|
||||
} else if let Some(r) = identity_signature_request {
|
||||
Some(r)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
// TODO store the request as well.
|
||||
self.store.save_changes(changes).await?;
|
||||
Ok(merged_request
|
||||
.map(VerificationResult::SignatureUpload)
|
||||
.unwrap_or(VerificationResult::Ok))
|
||||
} else {
|
||||
Ok(self
|
||||
.cancel()
|
||||
.map(VerificationResult::Cancel)
|
||||
.unwrap_or(VerificationResult::Ok))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn mark_identity_as_verified(
|
||||
&self,
|
||||
) -> Result<Option<UserIdentities>, CryptoStoreError> {
|
||||
// If there wasn't an identity available during the verification flow
|
||||
// return early as there's nothing to do.
|
||||
if self.other_identity.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// TODO signal an error, e.g. when the identity got deleted so we don't
|
||||
// verify/save the device either.
|
||||
let identity = self.store.get_user_identity(self.other_user_id()).await?;
|
||||
|
||||
if let Some(identity) = identity {
|
||||
if self
|
||||
.other_identity
|
||||
.as_ref()
|
||||
.map_or(false, |i| i.master_key() == identity.master_key())
|
||||
{
|
||||
if self
|
||||
.verified_identities()
|
||||
.map_or(false, |i| i.contains(&identity))
|
||||
{
|
||||
trace!(
|
||||
"Marking user identity of {} as verified.",
|
||||
identity.user_id(),
|
||||
);
|
||||
|
||||
if let UserIdentities::Own(i) = &identity {
|
||||
i.mark_as_verified();
|
||||
}
|
||||
|
||||
Ok(Some(identity))
|
||||
} else {
|
||||
info!(
|
||||
"The interactive verification process didn't contain a \
|
||||
MAC for the user identity of {} {:?}",
|
||||
identity.user_id(),
|
||||
self.verified_identities(),
|
||||
);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"The master keys of {} have changed while an interactive \
|
||||
verification was going on, not marking the identity as verified.",
|
||||
identity.user_id(),
|
||||
);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
"The identity for {} was deleted while an interactive \
|
||||
verification was going on.",
|
||||
self.other_user_id(),
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn mark_device_as_verified(
|
||||
&self,
|
||||
) -> Result<Option<ReadOnlyDevice>, CryptoStoreError> {
|
||||
let device = self
|
||||
.store
|
||||
.get_device(self.other_user_id(), self.other_device_id())
|
||||
.await?;
|
||||
|
||||
if let Some(device) = device {
|
||||
if device.keys() == self.other_device.keys() {
|
||||
if self
|
||||
.verified_devices()
|
||||
.map_or(false, |v| v.contains(&device))
|
||||
{
|
||||
trace!(
|
||||
"Marking device {} {} as verified.",
|
||||
device.user_id(),
|
||||
device.device_id()
|
||||
);
|
||||
|
||||
device.set_trust_state(LocalTrust::Verified);
|
||||
|
||||
Ok(Some(device))
|
||||
} else {
|
||||
info!(
|
||||
"The interactive verification process didn't contain a \
|
||||
MAC for the device {} {}",
|
||||
device.user_id(),
|
||||
device.device_id()
|
||||
);
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"The device keys of {} {} have changed while an interactive \
|
||||
verification was going on, not marking the device as verified.",
|
||||
device.user_id(),
|
||||
device.device_id()
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
} else {
|
||||
let device = self.other_device();
|
||||
|
||||
info!(
|
||||
"The device {} {} was deleted while an interactive \
|
||||
verification was going on.",
|
||||
device.user_id(),
|
||||
device.device_id()
|
||||
);
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Cancel the verification.
|
||||
///
|
||||
/// This cancels the verification with the `CancelCode::User`.
|
||||
///
|
||||
/// Returns None if the `Sas` object is already in a canceled state,
|
||||
/// otherwise it returns a request that needs to be sent out.
|
||||
pub fn cancel(&self) -> Option<ToDeviceRequest> {
|
||||
let mut guard = self.inner.lock().unwrap();
|
||||
let sas: InnerSas = (*guard).clone();
|
||||
let (sas, content) = sas.cancel(CancelCode::User);
|
||||
*guard = sas;
|
||||
|
||||
content.map(|c| self.content_to_request(c))
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_if_timed_out(&self) -> Option<ToDeviceRequest> {
|
||||
if self.is_canceled() || self.is_done() {
|
||||
None
|
||||
} else if self.timed_out() {
|
||||
let mut guard = self.inner.lock().unwrap();
|
||||
let sas: InnerSas = (*guard).clone();
|
||||
let (sas, content) = sas.cancel(CancelCode::Timeout);
|
||||
*guard = sas;
|
||||
content.map(|c| self.content_to_request(c))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Has the SAS verification flow timed out.
|
||||
pub fn timed_out(&self) -> bool {
|
||||
self.inner.lock().unwrap().timed_out()
|
||||
}
|
||||
|
||||
/// Are we in a state where we can show the short auth string.
|
||||
pub fn can_be_presented(&self) -> bool {
|
||||
self.inner.lock().unwrap().can_be_presented()
|
||||
}
|
||||
|
||||
/// Is the SAS flow done.
|
||||
pub fn is_done(&self) -> bool {
|
||||
self.inner.lock().unwrap().is_done()
|
||||
}
|
||||
|
||||
/// Is the SAS flow canceled.
|
||||
pub fn is_canceled(&self) -> bool {
|
||||
self.inner.lock().unwrap().is_canceled()
|
||||
}
|
||||
|
||||
/// Get the emoji version of the short auth string.
|
||||
///
|
||||
/// Returns None if we can't yet present the short auth string, otherwise a
|
||||
/// Vec of tuples with the emoji and description.
|
||||
pub fn emoji(&self) -> Option<Vec<(&'static str, &'static str)>> {
|
||||
self.inner.lock().unwrap().emoji()
|
||||
}
|
||||
|
||||
/// Get the decimal version of the short auth string.
|
||||
///
|
||||
/// Returns None if we can't yet present the short auth string, otherwise a
|
||||
/// tuple containing three 4-digit integers that represent the short auth
|
||||
/// string.
|
||||
pub fn decimals(&self) -> Option<(u16, u16, u16)> {
|
||||
self.inner.lock().unwrap().decimals()
|
||||
}
|
||||
|
||||
pub(crate) fn receive_event(
|
||||
&self,
|
||||
event: &mut AnyToDeviceEvent,
|
||||
) -> Option<AnyToDeviceEventContent> {
|
||||
let mut guard = self.inner.lock().unwrap();
|
||||
let sas: InnerSas = (*guard).clone();
|
||||
let (sas, content) = sas.receive_event(event);
|
||||
*guard = sas;
|
||||
|
||||
content
|
||||
}
|
||||
|
||||
pub(crate) fn verified_devices(&self) -> Option<Arc<[ReadOnlyDevice]>> {
|
||||
self.inner.lock().unwrap().verified_devices()
|
||||
}
|
||||
|
||||
pub(crate) fn verified_identities(&self) -> Option<Arc<[UserIdentities]>> {
|
||||
self.inner.lock().unwrap().verified_identities()
|
||||
}
|
||||
|
||||
pub(crate) fn content_to_request(&self, content: AnyToDeviceEventContent) -> ToDeviceRequest {
|
||||
content_to_request(self.other_user_id(), self.other_device_id(), content)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum InnerSas {
|
||||
Created(SasState<Created>),
|
||||
Started(SasState<Started>),
|
||||
Accepted(SasState<Accepted>),
|
||||
KeyRecieved(SasState<KeyReceived>),
|
||||
Confirmed(SasState<Confirmed>),
|
||||
MacReceived(SasState<MacReceived>),
|
||||
Done(SasState<Done>),
|
||||
Canceled(SasState<Canceled>),
|
||||
}
|
||||
|
||||
impl InnerSas {
|
||||
fn start(
|
||||
account: ReadOnlyAccount,
|
||||
other_device: ReadOnlyDevice,
|
||||
other_identity: Option<UserIdentities>,
|
||||
) -> (InnerSas, StartToDeviceEventContent) {
|
||||
let sas = SasState::<Created>::new(account, other_device, other_identity);
|
||||
let content = sas.as_content();
|
||||
(InnerSas::Created(sas), content)
|
||||
}
|
||||
|
||||
fn from_start_event(
|
||||
account: ReadOnlyAccount,
|
||||
other_device: ReadOnlyDevice,
|
||||
event: &ToDeviceEvent<StartToDeviceEventContent>,
|
||||
other_identity: Option<UserIdentities>,
|
||||
) -> Result<InnerSas, AnyToDeviceEventContent> {
|
||||
match SasState::<Started>::from_start_event(account, other_device, event, other_identity) {
|
||||
Ok(s) => Ok(InnerSas::Started(s)),
|
||||
Err(s) => Err(s.as_content()),
|
||||
}
|
||||
}
|
||||
|
||||
fn accept(&self) -> Option<AcceptToDeviceEventContent> {
|
||||
if let InnerSas::Started(s) = self {
|
||||
Some(s.as_content())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
fn set_creation_time(&mut self, time: Instant) {
|
||||
match self {
|
||||
InnerSas::Created(s) => s.set_creation_time(time),
|
||||
InnerSas::Started(s) => s.set_creation_time(time),
|
||||
InnerSas::Canceled(s) => s.set_creation_time(time),
|
||||
InnerSas::Accepted(s) => s.set_creation_time(time),
|
||||
InnerSas::KeyRecieved(s) => s.set_creation_time(time),
|
||||
InnerSas::Confirmed(s) => s.set_creation_time(time),
|
||||
InnerSas::MacReceived(s) => s.set_creation_time(time),
|
||||
InnerSas::Done(s) => s.set_creation_time(time),
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(self, code: CancelCode) -> (InnerSas, Option<AnyToDeviceEventContent>) {
|
||||
let sas = match self {
|
||||
InnerSas::Created(s) => s.cancel(code),
|
||||
InnerSas::Started(s) => s.cancel(code),
|
||||
InnerSas::Accepted(s) => s.cancel(code),
|
||||
InnerSas::KeyRecieved(s) => s.cancel(code),
|
||||
InnerSas::MacReceived(s) => s.cancel(code),
|
||||
_ => return (self, None),
|
||||
};
|
||||
|
||||
let content = sas.as_content();
|
||||
|
||||
(InnerSas::Canceled(sas), Some(content))
|
||||
}
|
||||
|
||||
fn confirm(self) -> (InnerSas, Option<MacToDeviceEventContent>) {
|
||||
match self {
|
||||
InnerSas::KeyRecieved(s) => {
|
||||
let sas = s.confirm();
|
||||
let content = sas.as_content();
|
||||
(InnerSas::Confirmed(sas), Some(content))
|
||||
}
|
||||
InnerSas::MacReceived(s) => {
|
||||
let sas = s.confirm();
|
||||
let content = sas.as_content();
|
||||
(InnerSas::Done(sas), Some(content))
|
||||
}
|
||||
_ => (self, None),
|
||||
}
|
||||
}
|
||||
|
||||
fn receive_event(
|
||||
self,
|
||||
event: &mut AnyToDeviceEvent,
|
||||
) -> (InnerSas, Option<AnyToDeviceEventContent>) {
|
||||
match event {
|
||||
AnyToDeviceEvent::KeyVerificationAccept(e) => {
|
||||
if let InnerSas::Created(s) = self {
|
||||
match s.into_accepted(e) {
|
||||
Ok(s) => {
|
||||
let content = s.as_content();
|
||||
(
|
||||
InnerSas::Accepted(s),
|
||||
Some(AnyToDeviceEventContent::KeyVerificationKey(content)),
|
||||
)
|
||||
}
|
||||
Err(s) => {
|
||||
let content = s.as_content();
|
||||
(InnerSas::Canceled(s), Some(content))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(self, None)
|
||||
}
|
||||
}
|
||||
AnyToDeviceEvent::KeyVerificationKey(e) => match self {
|
||||
InnerSas::Accepted(s) => match s.into_key_received(e) {
|
||||
Ok(s) => (InnerSas::KeyRecieved(s), None),
|
||||
Err(s) => {
|
||||
let content = s.as_content();
|
||||
(InnerSas::Canceled(s), Some(content))
|
||||
}
|
||||
},
|
||||
InnerSas::Started(s) => match s.into_key_received(e) {
|
||||
Ok(s) => {
|
||||
let content = s.as_content();
|
||||
(
|
||||
InnerSas::KeyRecieved(s),
|
||||
Some(AnyToDeviceEventContent::KeyVerificationKey(content)),
|
||||
)
|
||||
}
|
||||
Err(s) => {
|
||||
let content = s.as_content();
|
||||
(InnerSas::Canceled(s), Some(content))
|
||||
}
|
||||
},
|
||||
_ => (self, None),
|
||||
},
|
||||
AnyToDeviceEvent::KeyVerificationMac(e) => match self {
|
||||
InnerSas::KeyRecieved(s) => match s.into_mac_received(e) {
|
||||
Ok(s) => (InnerSas::MacReceived(s), None),
|
||||
Err(s) => {
|
||||
let content = s.as_content();
|
||||
(InnerSas::Canceled(s), Some(content))
|
||||
}
|
||||
},
|
||||
InnerSas::Confirmed(s) => match s.into_done(e) {
|
||||
Ok(s) => (InnerSas::Done(s), None),
|
||||
Err(s) => {
|
||||
let content = s.as_content();
|
||||
(InnerSas::Canceled(s), Some(content))
|
||||
}
|
||||
},
|
||||
_ => (self, None),
|
||||
},
|
||||
_ => (self, None),
|
||||
}
|
||||
}
|
||||
|
||||
fn can_be_presented(&self) -> bool {
|
||||
matches!(self, InnerSas::KeyRecieved(_) | InnerSas::MacReceived(_))
|
||||
}
|
||||
|
||||
fn is_done(&self) -> bool {
|
||||
matches!(self, InnerSas::Done(_))
|
||||
}
|
||||
|
||||
fn is_canceled(&self) -> bool {
|
||||
matches!(self, InnerSas::Canceled(_))
|
||||
}
|
||||
|
||||
fn timed_out(&self) -> bool {
|
||||
match self {
|
||||
InnerSas::Created(s) => s.timed_out(),
|
||||
InnerSas::Started(s) => s.timed_out(),
|
||||
InnerSas::Canceled(s) => s.timed_out(),
|
||||
InnerSas::Accepted(s) => s.timed_out(),
|
||||
InnerSas::KeyRecieved(s) => s.timed_out(),
|
||||
InnerSas::Confirmed(s) => s.timed_out(),
|
||||
InnerSas::MacReceived(s) => s.timed_out(),
|
||||
InnerSas::Done(s) => s.timed_out(),
|
||||
}
|
||||
}
|
||||
|
||||
fn verification_flow_id(&self) -> Arc<str> {
|
||||
match self {
|
||||
InnerSas::Created(s) => s.verification_flow_id.clone(),
|
||||
InnerSas::Started(s) => s.verification_flow_id.clone(),
|
||||
InnerSas::Canceled(s) => s.verification_flow_id.clone(),
|
||||
InnerSas::Accepted(s) => s.verification_flow_id.clone(),
|
||||
InnerSas::KeyRecieved(s) => s.verification_flow_id.clone(),
|
||||
InnerSas::Confirmed(s) => s.verification_flow_id.clone(),
|
||||
InnerSas::MacReceived(s) => s.verification_flow_id.clone(),
|
||||
InnerSas::Done(s) => s.verification_flow_id.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn emoji(&self) -> Option<Vec<(&'static str, &'static str)>> {
|
||||
match self {
|
||||
InnerSas::KeyRecieved(s) => Some(s.get_emoji()),
|
||||
InnerSas::MacReceived(s) => Some(s.get_emoji()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn decimals(&self) -> Option<(u16, u16, u16)> {
|
||||
match self {
|
||||
InnerSas::KeyRecieved(s) => Some(s.get_decimal()),
|
||||
InnerSas::MacReceived(s) => Some(s.get_decimal()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn verified_devices(&self) -> Option<Arc<[ReadOnlyDevice]>> {
|
||||
if let InnerSas::Done(s) = self {
|
||||
Some(s.verified_devices())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn verified_identities(&self) -> Option<Arc<[UserIdentities]>> {
|
||||
if let InnerSas::Done(s) = self {
|
||||
Some(s.verified_identities())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::{convert::TryFrom, sync::Arc};
|
||||
|
||||
use matrix_sdk_common::{
|
||||
events::{EventContent, ToDeviceEvent},
|
||||
identifiers::{DeviceId, UserId},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
olm::PrivateCrossSigningIdentity,
|
||||
store::{CryptoStore, MemoryStore},
|
||||
verification::test::{get_content_from_request, wrap_any_to_device_content},
|
||||
ReadOnlyAccount, ReadOnlyDevice,
|
||||
};
|
||||
|
||||
use super::{Accepted, Created, Sas, SasState, Started};
|
||||
|
||||
fn alice_id() -> UserId {
|
||||
UserId::try_from("@alice:example.org").unwrap()
|
||||
}
|
||||
|
||||
fn alice_device_id() -> Box<DeviceId> {
|
||||
"JLAFKJWSCS".into()
|
||||
}
|
||||
|
||||
fn bob_id() -> UserId {
|
||||
UserId::try_from("@bob:example.org").unwrap()
|
||||
}
|
||||
|
||||
fn bob_device_id() -> Box<DeviceId> {
|
||||
"BOBDEVCIE".into()
|
||||
}
|
||||
|
||||
fn wrap_to_device_event<C: EventContent>(sender: &UserId, content: C) -> ToDeviceEvent<C> {
|
||||
ToDeviceEvent {
|
||||
sender: sender.clone(),
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_sas_pair() -> (SasState<Created>, SasState<Started>) {
|
||||
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
|
||||
let alice_device = ReadOnlyDevice::from_account(&alice).await;
|
||||
|
||||
let bob = ReadOnlyAccount::new(&bob_id(), &bob_device_id());
|
||||
let bob_device = ReadOnlyDevice::from_account(&bob).await;
|
||||
|
||||
let alice_sas = SasState::<Created>::new(alice.clone(), bob_device, None);
|
||||
|
||||
let start_content = alice_sas.as_content();
|
||||
let event = wrap_to_device_event(alice_sas.user_id(), start_content);
|
||||
|
||||
let bob_sas =
|
||||
SasState::<Started>::from_start_event(bob.clone(), alice_device, &event, None);
|
||||
|
||||
(alice_sas, bob_sas.unwrap())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_sas() {
|
||||
let (_, _) = get_sas_pair().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sas_accept() {
|
||||
let (alice, bob) = get_sas_pair().await;
|
||||
|
||||
let event = wrap_to_device_event(bob.user_id(), bob.as_content());
|
||||
|
||||
alice.into_accepted(&event).unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sas_key_share() {
|
||||
let (alice, bob) = get_sas_pair().await;
|
||||
|
||||
let event = wrap_to_device_event(bob.user_id(), bob.as_content());
|
||||
|
||||
let alice: SasState<Accepted> = alice.into_accepted(&event).unwrap();
|
||||
let mut event = wrap_to_device_event(alice.user_id(), alice.as_content());
|
||||
|
||||
let bob = bob.into_key_received(&mut event).unwrap();
|
||||
|
||||
let mut event = wrap_to_device_event(bob.user_id(), bob.as_content());
|
||||
|
||||
let alice = alice.into_key_received(&mut event).unwrap();
|
||||
|
||||
assert_eq!(alice.get_decimal(), bob.get_decimal());
|
||||
assert_eq!(alice.get_emoji(), bob.get_emoji());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sas_full() {
|
||||
let (alice, bob) = get_sas_pair().await;
|
||||
|
||||
let event = wrap_to_device_event(bob.user_id(), bob.as_content());
|
||||
|
||||
let alice: SasState<Accepted> = alice.into_accepted(&event).unwrap();
|
||||
let mut event = wrap_to_device_event(alice.user_id(), alice.as_content());
|
||||
|
||||
let bob = bob.into_key_received(&mut event).unwrap();
|
||||
|
||||
let mut event = wrap_to_device_event(bob.user_id(), bob.as_content());
|
||||
|
||||
let alice = alice.into_key_received(&mut event).unwrap();
|
||||
|
||||
assert_eq!(alice.get_decimal(), bob.get_decimal());
|
||||
assert_eq!(alice.get_emoji(), bob.get_emoji());
|
||||
|
||||
let bob = bob.confirm();
|
||||
|
||||
let event = wrap_to_device_event(bob.user_id(), bob.as_content());
|
||||
|
||||
let alice = alice.into_mac_received(&event).unwrap();
|
||||
assert!(!alice.get_emoji().is_empty());
|
||||
let alice = alice.confirm();
|
||||
|
||||
let event = wrap_to_device_event(alice.user_id(), alice.as_content());
|
||||
let bob = bob.into_done(&event).unwrap();
|
||||
|
||||
assert!(bob.verified_devices().contains(&bob.other_device()));
|
||||
assert!(alice.verified_devices().contains(&alice.other_device()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn sas_wrapper_full() {
|
||||
let alice = ReadOnlyAccount::new(&alice_id(), &alice_device_id());
|
||||
let alice_device = ReadOnlyDevice::from_account(&alice).await;
|
||||
|
||||
let bob = ReadOnlyAccount::new(&bob_id(), &bob_device_id());
|
||||
let bob_device = ReadOnlyDevice::from_account(&bob).await;
|
||||
|
||||
let alice_store: Arc<Box<dyn CryptoStore>> = Arc::new(Box::new(MemoryStore::new()));
|
||||
let bob_store = MemoryStore::new();
|
||||
|
||||
bob_store.save_devices(vec![alice_device.clone()]).await;
|
||||
|
||||
let bob_store: Arc<Box<dyn CryptoStore>> = Arc::new(Box::new(bob_store));
|
||||
|
||||
let (alice, content) = Sas::start(
|
||||
alice,
|
||||
PrivateCrossSigningIdentity::empty(alice_id()),
|
||||
bob_device,
|
||||
alice_store,
|
||||
None,
|
||||
);
|
||||
let event = wrap_to_device_event(alice.user_id(), content);
|
||||
|
||||
let bob = Sas::from_start_event(
|
||||
bob,
|
||||
PrivateCrossSigningIdentity::empty(bob_id()),
|
||||
alice_device,
|
||||
bob_store,
|
||||
&event,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let mut event = wrap_any_to_device_content(
|
||||
bob.user_id(),
|
||||
get_content_from_request(&bob.accept().unwrap()),
|
||||
);
|
||||
|
||||
let content = alice.receive_event(&mut event);
|
||||
|
||||
assert!(!alice.can_be_presented());
|
||||
assert!(!bob.can_be_presented());
|
||||
|
||||
let mut event = wrap_any_to_device_content(alice.user_id(), content.unwrap());
|
||||
let mut event =
|
||||
wrap_any_to_device_content(bob.user_id(), bob.receive_event(&mut event).unwrap());
|
||||
|
||||
assert!(bob.can_be_presented());
|
||||
|
||||
alice.receive_event(&mut event);
|
||||
assert!(alice.can_be_presented());
|
||||
|
||||
assert_eq!(alice.emoji().unwrap(), bob.emoji().unwrap());
|
||||
assert_eq!(alice.decimals().unwrap(), bob.decimals().unwrap());
|
||||
|
||||
let mut event = wrap_any_to_device_content(
|
||||
alice.user_id(),
|
||||
get_content_from_request(&alice.confirm().await.unwrap().0.unwrap()),
|
||||
);
|
||||
bob.receive_event(&mut event);
|
||||
|
||||
let mut event = wrap_any_to_device_content(
|
||||
bob.user_id(),
|
||||
get_content_from_request(&bob.confirm().await.unwrap().0.unwrap()),
|
||||
);
|
||||
alice.receive_event(&mut event);
|
||||
|
||||
assert!(alice
|
||||
.verified_devices()
|
||||
.unwrap()
|
||||
.contains(&alice.other_device()));
|
||||
assert!(bob
|
||||
.verified_devices()
|
||||
.unwrap()
|
||||
.contains(&bob.other_device()));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk"]
|
||||
authors = ["Damir Jelić <poljar@termina.org.uk>"]
|
||||
description = "Helpers to write tests for the Matrix SDK"
|
||||
edition = "2018"
|
||||
homepage = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
@@ -8,10 +8,12 @@ license = "Apache-2.0"
|
||||
name = "matrix-sdk-test"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/matrix-org/matrix-rust-sdk"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
|
||||
[dependencies]
|
||||
serde_json = "1.0.53"
|
||||
http = "0.2.1"
|
||||
matrix-sdk-common = { version = "0.1.0", path = "../matrix_sdk_common" }
|
||||
serde_json = "1.0.61"
|
||||
http = "0.2.2"
|
||||
matrix-sdk-common = { version = "0.2.0", path = "../matrix_sdk_common" }
|
||||
matrix-sdk-test-macros = { version = "0.1.0", path = "../matrix_sdk_test_macros" }
|
||||
lazy_static = "1.4.0"
|
||||
serde = "1.0.118"
|
||||
|
||||
+153
-128
@@ -1,26 +1,24 @@
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::panic;
|
||||
use std::{collections::HashMap, convert::TryFrom, panic};
|
||||
|
||||
use http::Response;
|
||||
|
||||
use matrix_sdk_common::api::r0::sync::sync_events::Response as SyncResponse;
|
||||
use matrix_sdk_common::events::{
|
||||
collections::{
|
||||
all::{RoomEvent, StateEvent},
|
||||
only::Event,
|
||||
use matrix_sdk_common::{
|
||||
api::r0::sync::sync_events::Response as SyncResponse,
|
||||
events::{
|
||||
presence::PresenceEvent, AnyBasicEvent, AnySyncEphemeralRoomEvent, AnySyncRoomEvent,
|
||||
AnySyncStateEvent,
|
||||
},
|
||||
presence::PresenceEvent,
|
||||
stripped::AnyStrippedStateEvent,
|
||||
EventJson, TryFromRaw,
|
||||
identifiers::{room_id, RoomId},
|
||||
};
|
||||
use matrix_sdk_common::identifiers::RoomId;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
pub use matrix_sdk_test_macros::async_test;
|
||||
|
||||
pub mod test_json;
|
||||
|
||||
/// Embedded event files
|
||||
#[derive(Debug)]
|
||||
pub enum EventsFile {
|
||||
pub enum EventsJson {
|
||||
Alias,
|
||||
Aliases,
|
||||
Create,
|
||||
@@ -28,6 +26,7 @@ pub enum EventsFile {
|
||||
HistoryVisibility,
|
||||
JoinRules,
|
||||
Member,
|
||||
MemberNameChange,
|
||||
MessageEmote,
|
||||
MessageNotice,
|
||||
MessageText,
|
||||
@@ -44,181 +43,185 @@ pub enum EventsFile {
|
||||
Typing,
|
||||
}
|
||||
|
||||
/// Easily create events to stream into either a Client or a `Room` for testing.
|
||||
/// The `EventBuilder` struct can be used to easily generate valid sync responses for testing.
|
||||
/// These can be then fed into either `Client` or `Room`.
|
||||
///
|
||||
/// It supports generated a number of canned events, such as a member entering a room, his power
|
||||
/// level and display name changing and similar. It also supports insertion of custom events in the
|
||||
/// form of `EventsJson` values.
|
||||
///
|
||||
/// **Important** You *must* use the *same* builder when sending multiple sync responses to
|
||||
/// a single client. Otherwise, the subsequent responses will be *ignored* by the client because
|
||||
/// the `next_batch` sync token will not be rotated properly.
|
||||
///
|
||||
/// # Example usage
|
||||
///
|
||||
/// ```rust
|
||||
/// use matrix_sdk_test::{EventBuilder, EventsJson};
|
||||
///
|
||||
/// let mut builder = EventBuilder::new();
|
||||
///
|
||||
/// // response1 now contains events that add an example member to the room and change their power
|
||||
/// // level
|
||||
/// let response1 = builder
|
||||
/// .add_room_event(EventsJson::Member)
|
||||
/// .add_room_event(EventsJson::PowerLevels)
|
||||
/// .build_sync_response();
|
||||
///
|
||||
/// // response2 is now empty (nothing changed)
|
||||
/// let response2 = builder.build_sync_response();
|
||||
///
|
||||
/// // response3 contains a display name change for member example
|
||||
/// let response3 = builder
|
||||
/// .add_room_event(EventsJson::MemberNameChange)
|
||||
/// .build_sync_response();
|
||||
/// ```
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EventBuilder {
|
||||
/// The events that determine the state of a `Room`.
|
||||
joined_room_events: HashMap<RoomId, Vec<RoomEvent>>,
|
||||
joined_room_events: HashMap<RoomId, Vec<AnySyncRoomEvent>>,
|
||||
/// The events that determine the state of a `Room`.
|
||||
invited_room_events: HashMap<RoomId, Vec<AnyStrippedStateEvent>>,
|
||||
invited_room_events: HashMap<RoomId, Vec<AnySyncStateEvent>>,
|
||||
/// The events that determine the state of a `Room`.
|
||||
left_room_events: HashMap<RoomId, Vec<RoomEvent>>,
|
||||
left_room_events: HashMap<RoomId, Vec<AnySyncRoomEvent>>,
|
||||
/// The presence events that determine the presence state of a `RoomMember`.
|
||||
presence_events: Vec<PresenceEvent>,
|
||||
/// The state events that determine the state of a `Room`.
|
||||
state_events: Vec<StateEvent>,
|
||||
state_events: Vec<AnySyncStateEvent>,
|
||||
/// The ephemeral room events that determine the state of a `Room`.
|
||||
ephemeral: Vec<Event>,
|
||||
ephemeral: Vec<AnySyncEphemeralRoomEvent>,
|
||||
/// The account data events that determine the state of a `Room`.
|
||||
account_data: Vec<Event>,
|
||||
account_data: Vec<AnyBasicEvent>,
|
||||
/// Internal counter to enable the `prev_batch` and `next_batch` of each sync response to vary.
|
||||
batch_counter: i64,
|
||||
}
|
||||
|
||||
impl EventBuilder {
|
||||
pub fn new() -> Self {
|
||||
let builder: EventBuilder = Default::default();
|
||||
builder
|
||||
}
|
||||
|
||||
/// Add an event to the room events `Vec`.
|
||||
pub fn add_ephemeral<Ev: TryFromRaw>(
|
||||
mut self,
|
||||
file: EventsFile,
|
||||
variant: fn(Ev) -> Event,
|
||||
) -> Self {
|
||||
let val: &str = match file {
|
||||
EventsFile::Typing => include_str!("../test_data/events/typing.json"),
|
||||
_ => panic!("unknown ephemeral event file {:?}", file),
|
||||
pub fn add_ephemeral(&mut self, json: EventsJson) -> &mut Self {
|
||||
let val: &JsonValue = match json {
|
||||
EventsJson::Typing => &test_json::TYPING,
|
||||
_ => panic!("unknown ephemeral event {:?}", json),
|
||||
};
|
||||
|
||||
let event = serde_json::from_str::<EventJson<Ev>>(&val)
|
||||
.unwrap()
|
||||
.deserialize()
|
||||
.unwrap();
|
||||
self.ephemeral.push(variant(event));
|
||||
let event = serde_json::from_value::<AnySyncEphemeralRoomEvent>(val.clone()).unwrap();
|
||||
self.ephemeral.push(event);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an event to the room events `Vec`.
|
||||
#[allow(clippy::match_single_binding, unused)]
|
||||
pub fn add_account<Ev: TryFromRaw>(
|
||||
mut self,
|
||||
file: EventsFile,
|
||||
variant: fn(Ev) -> Event,
|
||||
) -> Self {
|
||||
let val: &str = match file {
|
||||
_ => panic!("unknown account event file {:?}", file),
|
||||
pub fn add_account(&mut self, json: EventsJson) -> &mut Self {
|
||||
let val: &JsonValue = match json {
|
||||
_ => panic!("unknown account event {:?}", json),
|
||||
};
|
||||
|
||||
let event = serde_json::from_str::<EventJson<Ev>>(&val)
|
||||
.unwrap()
|
||||
.deserialize()
|
||||
.unwrap();
|
||||
self.account_data.push(variant(event));
|
||||
let event = serde_json::from_value::<AnyBasicEvent>(val.clone()).unwrap();
|
||||
self.account_data.push(event);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an event to the room events `Vec`.
|
||||
pub fn add_room_event<Ev: TryFromRaw>(
|
||||
mut self,
|
||||
file: EventsFile,
|
||||
variant: fn(Ev) -> RoomEvent,
|
||||
) -> Self {
|
||||
let val = match file {
|
||||
EventsFile::Member => include_str!("../test_data/events/member.json"),
|
||||
EventsFile::PowerLevels => include_str!("../test_data/events/power_levels.json"),
|
||||
_ => panic!("unknown room event file {:?}", file),
|
||||
pub fn add_room_event(&mut self, json: EventsJson) -> &mut Self {
|
||||
let val: &JsonValue = match json {
|
||||
EventsJson::Member => &test_json::MEMBER,
|
||||
EventsJson::MemberNameChange => &test_json::MEMBER_NAME_CHANGE,
|
||||
EventsJson::PowerLevels => &test_json::POWER_LEVELS,
|
||||
_ => panic!("unknown room event json {:?}", json),
|
||||
};
|
||||
|
||||
let event = serde_json::from_str::<EventJson<Ev>>(&val)
|
||||
.unwrap()
|
||||
.deserialize()
|
||||
.unwrap();
|
||||
self.add_joined_event(
|
||||
&RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap(),
|
||||
variant(event),
|
||||
);
|
||||
let event = serde_json::from_value::<AnySyncRoomEvent>(val.clone()).unwrap();
|
||||
|
||||
self.add_joined_event(&room_id!("!SVkFJHzfwvuaIEawgC:localhost"), event);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_custom_joined_event<Ev: TryFromRaw>(
|
||||
mut self,
|
||||
pub fn add_custom_joined_event(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
event: serde_json::Value,
|
||||
variant: fn(Ev) -> RoomEvent,
|
||||
) -> Self {
|
||||
let event = serde_json::from_value::<EventJson<Ev>>(event)
|
||||
.unwrap()
|
||||
.deserialize()
|
||||
.unwrap();
|
||||
self.add_joined_event(room_id, variant(event));
|
||||
) -> &mut Self {
|
||||
let event = serde_json::from_value::<AnySyncRoomEvent>(event).unwrap();
|
||||
self.add_joined_event(room_id, event);
|
||||
self
|
||||
}
|
||||
|
||||
fn add_joined_event(&mut self, room_id: &RoomId, event: RoomEvent) {
|
||||
fn add_joined_event(&mut self, room_id: &RoomId, event: AnySyncRoomEvent) {
|
||||
self.joined_room_events
|
||||
.entry(room_id.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(event);
|
||||
}
|
||||
|
||||
pub fn add_custom_invited_event<Ev: TryFromRaw>(
|
||||
mut self,
|
||||
pub fn add_custom_invited_event(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
event: serde_json::Value,
|
||||
variant: fn(Ev) -> AnyStrippedStateEvent,
|
||||
) -> Self {
|
||||
let event = serde_json::from_value::<EventJson<Ev>>(event)
|
||||
.unwrap()
|
||||
.deserialize()
|
||||
.unwrap();
|
||||
) -> &mut Self {
|
||||
let event = serde_json::from_value::<AnySyncStateEvent>(event).unwrap();
|
||||
self.invited_room_events
|
||||
.entry(room_id.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(variant(event));
|
||||
.push(event);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_custom_left_event<Ev: TryFromRaw>(
|
||||
mut self,
|
||||
pub fn add_custom_left_event(
|
||||
&mut self,
|
||||
room_id: &RoomId,
|
||||
event: serde_json::Value,
|
||||
variant: fn(Ev) -> RoomEvent,
|
||||
) -> Self {
|
||||
let event = serde_json::from_value::<EventJson<Ev>>(event)
|
||||
.unwrap()
|
||||
.deserialize()
|
||||
.unwrap();
|
||||
) -> &mut Self {
|
||||
let event = serde_json::from_value::<AnySyncRoomEvent>(event).unwrap();
|
||||
self.left_room_events
|
||||
.entry(room_id.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(variant(event));
|
||||
.push(event);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a state event to the state events `Vec`.
|
||||
pub fn add_state_event<Ev: TryFromRaw>(
|
||||
mut self,
|
||||
file: EventsFile,
|
||||
variant: fn(Ev) -> StateEvent,
|
||||
) -> Self {
|
||||
let val = match file {
|
||||
EventsFile::Alias => include_str!("../test_data/events/alias.json"),
|
||||
EventsFile::Aliases => include_str!("../test_data/events/aliases.json"),
|
||||
EventsFile::Name => include_str!("../test_data/events/name.json"),
|
||||
_ => panic!("unknown state event file {:?}", file),
|
||||
pub fn add_state_event(&mut self, json: EventsJson) -> &mut Self {
|
||||
let val: &JsonValue = match json {
|
||||
EventsJson::Alias => &test_json::ALIAS,
|
||||
EventsJson::Aliases => &test_json::ALIASES,
|
||||
EventsJson::Name => &test_json::NAME,
|
||||
EventsJson::Member => &test_json::MEMBER,
|
||||
EventsJson::PowerLevels => &test_json::POWER_LEVELS,
|
||||
_ => panic!("unknown state event {:?}", json),
|
||||
};
|
||||
|
||||
let event = serde_json::from_str::<EventJson<Ev>>(&val)
|
||||
.unwrap()
|
||||
.deserialize()
|
||||
.unwrap();
|
||||
self.state_events.push(variant(event));
|
||||
let event = serde_json::from_value::<AnySyncStateEvent>(val.clone()).unwrap();
|
||||
self.state_events.push(event);
|
||||
self
|
||||
}
|
||||
|
||||
/// Add an presence event to the presence events `Vec`.
|
||||
pub fn add_presence_event(mut self, file: EventsFile) -> Self {
|
||||
let val = match file {
|
||||
EventsFile::Presence => include_str!("../test_data/events/presence.json"),
|
||||
_ => panic!("unknown presence event file {:?}", file),
|
||||
pub fn add_presence_event(&mut self, json: EventsJson) -> &mut Self {
|
||||
let val: &JsonValue = match json {
|
||||
EventsJson::Presence => &test_json::PRESENCE,
|
||||
_ => panic!("unknown presence event {:?}", json),
|
||||
};
|
||||
|
||||
let event = serde_json::from_str::<EventJson<PresenceEvent>>(&val)
|
||||
.unwrap()
|
||||
.deserialize()
|
||||
.unwrap();
|
||||
let event = serde_json::from_value::<PresenceEvent>(val.clone()).unwrap();
|
||||
self.presence_events.push(event);
|
||||
self
|
||||
}
|
||||
|
||||
/// Consumes `ResponseBuilder and returns SyncResponse.
|
||||
pub fn build_sync_response(mut self) -> SyncResponse {
|
||||
let main_room_id = RoomId::try_from("!SVkFJHzfwvuaIEawgC:localhost").unwrap();
|
||||
/// Builds a `SyncResponse` containing the events we queued so far. The next response returned
|
||||
/// by `build_sync_response` will then be empty if no further events were queued.
|
||||
pub fn build_sync_response(&mut self) -> SyncResponse {
|
||||
let main_room_id = room_id!("!SVkFJHzfwvuaIEawgC:localhost");
|
||||
|
||||
// First time building a sync response, so initialize the `prev_batch` to a default one.
|
||||
let prev_batch = self.generate_sync_token();
|
||||
self.batch_counter += 1;
|
||||
let next_batch = self.generate_sync_token();
|
||||
|
||||
// TODO generalize this.
|
||||
let joined_room = serde_json::json!({
|
||||
@@ -235,7 +238,7 @@ impl EventBuilder {
|
||||
"timeline": {
|
||||
"events": self.joined_room_events.remove(&main_room_id).unwrap_or_default(),
|
||||
"limited": true,
|
||||
"prev_batch": "t392-516_47314_0_7_1_1_1_11444_1"
|
||||
"prev_batch": prev_batch
|
||||
},
|
||||
"unread_notifications": {
|
||||
"highlight_count": 0,
|
||||
@@ -262,7 +265,7 @@ impl EventBuilder {
|
||||
"timeline": {
|
||||
"events": events,
|
||||
"limited": true,
|
||||
"prev_batch": "t392-516_47314_0_7_1_1_1_11444_1"
|
||||
"prev_batch": prev_batch
|
||||
},
|
||||
"unread_notifications": {
|
||||
"highlight_count": 0,
|
||||
@@ -282,7 +285,7 @@ impl EventBuilder {
|
||||
"timeline": {
|
||||
"events": events,
|
||||
"limited": false,
|
||||
"prev_batch": "t392-516_47314_0_7_1_1_1_11444_1"
|
||||
"prev_batch": prev_batch
|
||||
},
|
||||
});
|
||||
left_rooms.insert(room_id, room);
|
||||
@@ -302,7 +305,7 @@ impl EventBuilder {
|
||||
let body = serde_json::json! {
|
||||
{
|
||||
"device_one_time_keys_count": {},
|
||||
"next_batch": "s526_47314_0_7_1_1_1_11444_1",
|
||||
"next_batch": next_batch,
|
||||
"device_lists": {
|
||||
"changed": [],
|
||||
"left": []
|
||||
@@ -323,29 +326,51 @@ impl EventBuilder {
|
||||
let response = Response::builder()
|
||||
.body(serde_json::to_vec(&body).unwrap())
|
||||
.unwrap();
|
||||
|
||||
// Clear state so that the next sync response will be empty if nothing was added.
|
||||
self.clear();
|
||||
|
||||
SyncResponse::try_from(response).unwrap()
|
||||
}
|
||||
|
||||
fn generate_sync_token(&self) -> String {
|
||||
format!("t392-516_47314_0_7_1_1_1_11444_{}", self.batch_counter)
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
self.account_data.clear();
|
||||
self.ephemeral.clear();
|
||||
self.invited_room_events.clear();
|
||||
self.joined_room_events.clear();
|
||||
self.left_room_events.clear();
|
||||
self.presence_events.clear();
|
||||
self.state_events.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// Embedded sync reponse files
|
||||
pub enum SyncResponseFile {
|
||||
All,
|
||||
Default,
|
||||
DefaultWithSummary,
|
||||
Invite,
|
||||
Leave,
|
||||
Voip,
|
||||
}
|
||||
|
||||
/// Get specific API responses for testing
|
||||
pub fn sync_response(kind: SyncResponseFile) -> SyncResponse {
|
||||
let data = match kind {
|
||||
SyncResponseFile::Default => include_bytes!("../test_data/sync.json").to_vec(),
|
||||
SyncResponseFile::DefaultWithSummary => {
|
||||
include_bytes!("../test_data/sync_with_summary.json").to_vec()
|
||||
}
|
||||
SyncResponseFile::Invite => include_bytes!("../test_data/invite_sync.json").to_vec(),
|
||||
SyncResponseFile::Leave => include_bytes!("../test_data/leave_sync.json").to_vec(),
|
||||
let data: &JsonValue = match kind {
|
||||
SyncResponseFile::All => &test_json::MORE_SYNC,
|
||||
SyncResponseFile::Default => &test_json::SYNC,
|
||||
SyncResponseFile::DefaultWithSummary => &test_json::DEFAULT_SYNC_SUMMARY,
|
||||
SyncResponseFile::Invite => &test_json::INVITE_SYNC,
|
||||
SyncResponseFile::Leave => &test_json::LEAVE_SYNC,
|
||||
SyncResponseFile::Voip => &test_json::VOIP_SYNC,
|
||||
};
|
||||
|
||||
let response = Response::builder().body(data.to_vec()).unwrap();
|
||||
let response = Response::builder()
|
||||
.body(data.to_string().as_bytes().to_vec())
|
||||
.unwrap();
|
||||
SyncResponse::try_from(response).unwrap()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,615 @@
|
||||
use lazy_static::lazy_static;
|
||||
use serde_json::{json, Value as JsonValue};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ALIAS: JsonValue = json!({
|
||||
"content": {
|
||||
"alias": "#tutorial:localhost"
|
||||
},
|
||||
"event_id": "$15139375513VdeRF:localhost",
|
||||
"origin_server_ts": 151393755,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.canonical_alias",
|
||||
"unsigned": {
|
||||
"age": 703422
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ALIASES: JsonValue = json!({
|
||||
"content": {
|
||||
"aliases": [
|
||||
"#tutorial:localhost"
|
||||
]
|
||||
},
|
||||
"event_id": "$15139375516NUgtD:localhost",
|
||||
"origin_server_ts": 151393755,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "localhost",
|
||||
"type": "m.room.aliases",
|
||||
"unsigned": {
|
||||
"age": 703422
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CREATE: JsonValue = json!({
|
||||
"content": {
|
||||
"creator": "@example:localhost",
|
||||
"m.federate": true,
|
||||
"room_version": "1"
|
||||
},
|
||||
"event_id": "$151957878228ekrDs:localhost",
|
||||
"origin_server_ts": 15195787,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.create",
|
||||
"unsigned": {
|
||||
"age": 139298
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref FULLY_READ: JsonValue = json!({
|
||||
"content": {
|
||||
"event_id": "$someplace:example.org"
|
||||
},
|
||||
"room_id": "!somewhere:example.org",
|
||||
"type": "m.fully_read"
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref HISTORY_VISIBILITY: JsonValue = json!({
|
||||
"content": {
|
||||
"history_visibility": "world_readable"
|
||||
},
|
||||
"event_id": "$151957878235ricnD:localhost",
|
||||
"origin_server_ts": 151957878,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.history_visibility",
|
||||
"unsigned": {
|
||||
"age": 1392989
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref JOIN_RULES: JsonValue = json!({
|
||||
"content": {
|
||||
"join_rule": "public"
|
||||
},
|
||||
"event_id": "$151957878231iejdB:localhost",
|
||||
"origin_server_ts": 151957878,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.join_rules",
|
||||
"unsigned": {
|
||||
"age": 1392989
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ROOM_MESSAGES: JsonValue = json!({
|
||||
"chunk": [
|
||||
{
|
||||
"age": 1042,
|
||||
"content": {
|
||||
"body": "hello world",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1444812213350496Caaaa:example.com",
|
||||
"origin_server_ts": 1444812213737i64,
|
||||
"room_id": "!Xq3620DUiqCaoxq:example.com",
|
||||
"sender": "@alice:example.com",
|
||||
"type": "m.room.message"
|
||||
},
|
||||
{
|
||||
"age": 20123,
|
||||
"content": {
|
||||
"body": "the world is big",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$1444812213350496Cbbbb:example.com",
|
||||
"origin_server_ts": 1444812194656i64,
|
||||
"room_id": "!Xq3620DUiqCaoxq:example.com",
|
||||
"sender": "@bob:example.com",
|
||||
"type": "m.room.message"
|
||||
},
|
||||
{
|
||||
"age": 50789,
|
||||
"content": {
|
||||
"name": "New room name"
|
||||
},
|
||||
"event_id": "$1444812213350496Ccccc:example.com",
|
||||
"origin_server_ts": 1444812163990i64,
|
||||
"prev_content": {
|
||||
"name": "Old room name"
|
||||
},
|
||||
"room_id": "!Xq3620DUiqCaoxq:example.com",
|
||||
"sender": "@bob:example.com",
|
||||
"state_key": "",
|
||||
"type": "m.room.name"
|
||||
}
|
||||
],
|
||||
"end": "t47409-4357353_219380_26003_2265",
|
||||
"start": "t47429-4392820_219380_26003_2265"
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref KEYS_QUERY: JsonValue = json!({
|
||||
"device_keys": {
|
||||
"@alice:example.org": {
|
||||
"JLAFKJWSCS": {
|
||||
"algorithms": [
|
||||
"m.olm.v1.curve25519-aes-sha2",
|
||||
"m.megolm.v1.aes-sha2"
|
||||
],
|
||||
"device_id": "JLAFKJWSCS",
|
||||
"user_id": "@alice:example.org",
|
||||
"keys": {
|
||||
"curve25519:JLAFKJWSCS": "wjLpTLRqbqBzLs63aYaEv2Boi6cFEbbM/sSRQ2oAKk4",
|
||||
"ed25519:JLAFKJWSCS": "nE6W2fCblxDcOFmeEtCHNl8/l8bXcu7GKyAswA4r3mM"
|
||||
},
|
||||
"signatures": {
|
||||
"@alice:example.org": {
|
||||
"ed25519:JLAFKJWSCS": "m53Wkbh2HXkc3vFApZvCrfXcX3AI51GsDHustMhKwlv3TuOJMj4wistcOTM8q2+e/Ro7rWFUb9ZfnNbwptSUBA"
|
||||
}
|
||||
},
|
||||
"unsigned": {
|
||||
"device_display_name": "Alice's mobile phone"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"failures": {}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref KEYS_UPLOAD: JsonValue = json!({
|
||||
"one_time_key_counts": {
|
||||
"curve25519": 10,
|
||||
"signed_curve25519": 20
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref LOGIN: JsonValue = json!({
|
||||
"access_token": "abc123",
|
||||
"device_id": "GHTYAJCE",
|
||||
"home_server": "matrix.org",
|
||||
"user_id": "@cheeky_monkey:matrix.org"
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref LOGIN_RESPONSE_ERR: JsonValue = json!({
|
||||
"errcode": "M_FORBIDDEN",
|
||||
"error": "Invalid password"
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref LOGOUT: JsonValue = json!({});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref EVENT_ID: JsonValue = json!({
|
||||
"event_id": "$h29iv0s8:example.com"
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Move `prev_content` into `unsigned` once ruma supports it
|
||||
lazy_static! {
|
||||
pub static ref MEMBER: JsonValue = json!({
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "example",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$151800140517rfvjc:localhost",
|
||||
"membership": "join",
|
||||
"origin_server_ts": 151800140,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "@example:localhost",
|
||||
"type": "m.room.member",
|
||||
"prev_content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "example",
|
||||
"membership": "invite"
|
||||
},
|
||||
"unsigned": {
|
||||
"age": 297036,
|
||||
"replaces_state": "$151800111315tsynI:localhost"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Move `prev_content` into `unsigned` once ruma supports it
|
||||
lazy_static! {
|
||||
pub static ref MEMBER_NAME_CHANGE: JsonValue = json!({
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "changed",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$151800234427abgho:localhost",
|
||||
"membership": "join",
|
||||
"origin_server_ts": 151800152,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "@example:localhost",
|
||||
"type": "m.room.member",
|
||||
"prev_content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "example",
|
||||
"membership": "join"
|
||||
},
|
||||
"unsigned": {
|
||||
"age": 297032,
|
||||
"replaces_state": "$151800140517rfvjc:localhost"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref MESSAGE_EDIT: JsonValue = json!({
|
||||
"content": {
|
||||
"body": " * edited message",
|
||||
"m.new_content": {
|
||||
"body": "edited message",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"m.relates_to": {
|
||||
"event_id": "$someeventid:foo",
|
||||
"rel_type": "m.replace"
|
||||
},
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$eventid:foo",
|
||||
"origin_server_ts": 159026265,
|
||||
"sender": "@alice:matrix.org",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 85
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref MESSAGE_EMOTE: JsonValue = json!({
|
||||
"content": {
|
||||
"body": "is dancing", "format": "org.matrix.custom.html",
|
||||
"formatted_body": "<strong>is dancing</strong>",
|
||||
"msgtype": "m.emote"
|
||||
},
|
||||
"event_id": "$152037280074GZeOm:localhost",
|
||||
"origin_server_ts": 152037280,
|
||||
"sender": "@example:localhost",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 598971
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref MESSAGE_NOTICE: JsonValue = json!({
|
||||
"origin_server_ts": 153356516,
|
||||
"sender": "@_neb_github:matrix.org",
|
||||
"event_id": "$153356516319138IHRIC:matrix.org",
|
||||
"unsigned": {
|
||||
"age": 743
|
||||
},
|
||||
"content": {
|
||||
"body": "https://github.com/matrix-org/matrix-python-sdk/issues/266 : Consider allowing MatrixClient.__init__ to take sync_token kwarg",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<a href='https://github.com/matrix-org/matrix-python-sdk/pull/313'>313: nio wins!</a>",
|
||||
"msgtype": "m.notice"
|
||||
},
|
||||
"type": "m.room.message",
|
||||
"room_id": "!YHhmBTmGBHGQOlGpaZ:matrix.org"
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref MESSAGE_TEXT: JsonValue = json!({
|
||||
"content": {
|
||||
"body": "is dancing", "format": "org.matrix.custom.html",
|
||||
"formatted_body": "<strong>is dancing</strong>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$152037280074GZeOm:localhost",
|
||||
"origin_server_ts": 152037280,
|
||||
"sender": "@example:localhost",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 598971
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref NAME: JsonValue = json!({
|
||||
"content": {
|
||||
"name": "room name"
|
||||
},
|
||||
"event_id": "$15139375513VdeRF:localhost",
|
||||
"origin_server_ts": 151393755,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.name",
|
||||
"unsigned": {
|
||||
"age": 703422
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref POWER_LEVELS: JsonValue = json!({
|
||||
"content": {
|
||||
"ban": 50,
|
||||
"events": {
|
||||
"m.room.avatar": 50,
|
||||
"m.room.canonical_alias": 50,
|
||||
"m.room.history_visibility": 100,
|
||||
"m.room.name": 50,
|
||||
"m.room.power_levels": 100,
|
||||
"m.room.message": 25
|
||||
},
|
||||
"events_default": 0,
|
||||
"invite": 0,
|
||||
"kick": 50,
|
||||
"redact": 50,
|
||||
"state_default": 50,
|
||||
"users": {
|
||||
"@example:localhost": 100,
|
||||
"@bob:localhost": 0
|
||||
},
|
||||
"users_default": 0
|
||||
},
|
||||
"event_id": "$15139375512JaHAW:localhost",
|
||||
"origin_server_ts": 151393755,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.power_levels",
|
||||
"unsigned": {
|
||||
"age": 703422
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref PRESENCE: JsonValue = json!({
|
||||
"content": {
|
||||
"avatar_url": "mxc://localhost:wefuiwegh8742w",
|
||||
"currently_active": false,
|
||||
"last_active_ago": 1,
|
||||
"presence": "online",
|
||||
"status_msg": "Making cupcakes"
|
||||
},
|
||||
"sender": "@example:localhost",
|
||||
"type": "m.presence"
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref PUBLIC_ROOMS: JsonValue = json!({
|
||||
"chunk": [
|
||||
{
|
||||
"aliases": [
|
||||
"#murrays:cheese.bar"
|
||||
],
|
||||
"avatar_url": "mxc://bleeker.street/CHEDDARandBRIE",
|
||||
"guest_can_join": false,
|
||||
"name": "CHEESE",
|
||||
"num_joined_members": 37,
|
||||
"room_id": "!ol19s:bleecker.street",
|
||||
"topic": "Tasty tasty cheese",
|
||||
"world_readable": true
|
||||
}
|
||||
],
|
||||
"next_batch": "p190q",
|
||||
"prev_batch": "p1902",
|
||||
"total_room_count_estimate": 115
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref REGISTRATION_RESPONSE_ERR: JsonValue = json!({
|
||||
"errcode": "M_FORBIDDEN",
|
||||
"error": "Invalid password",
|
||||
"completed": ["example.type.foo"],
|
||||
"flows": [
|
||||
{
|
||||
"stages": ["example.type.foo", "example.type.bar"]
|
||||
},
|
||||
{
|
||||
"stages": ["example.type.foo", "example.type.baz"]
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
"example.type.baz": {
|
||||
"example_key": "foobar"
|
||||
}
|
||||
},
|
||||
"session": "xxxxxx"
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref REACTION: JsonValue = json!({
|
||||
"content": {
|
||||
"m.relates_to": {
|
||||
"event_id": "$MDitXXXXXXuBlpP7S6c6XXXXXXXC2HqZ3peV1NrV4PKA",
|
||||
"key": "👍",
|
||||
"rel_type": "m.annotation"
|
||||
}
|
||||
},
|
||||
"event_id": "$QZn9xEXXXXXfd2tAGFH-XXgsffZlVMobk47Tl5Lpdtg",
|
||||
"origin_server_ts": 159027581,
|
||||
"sender": "@devinr528:matrix.org",
|
||||
"type": "m.reaction",
|
||||
"unsigned": {
|
||||
"age": 85
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref REDACTED_INVALID: JsonValue = json!({
|
||||
"content": {},
|
||||
"event_id": "$15275046980maRLj:localhost",
|
||||
"origin_server_ts": 1527504698,
|
||||
"sender": "@example:localhost",
|
||||
"type": "m.room.message"
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref REDACTED_STATE: JsonValue = json!({
|
||||
"content": {},
|
||||
"event_id": "$example_id:example.org",
|
||||
"origin_server_ts": 153232493,
|
||||
"sender": "@example:example.org",
|
||||
"state_key": "test_state_key",
|
||||
"type": "m.some.state",
|
||||
"unsigned": {
|
||||
"age": 3069315,
|
||||
"redacted_because": {
|
||||
"content": {},
|
||||
"event_id": "$redaction_example_id:example.org",
|
||||
"origin_server_ts": 153232494,
|
||||
"redacts": "$example_id:example.org",
|
||||
"sender": "@example:example:org",
|
||||
"type": "m.room.redaction",
|
||||
"unsigned": {"age": 30693147}
|
||||
},
|
||||
"redacted_by": "$redaction_example_id:example.org"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref REDACTED: JsonValue = json!({
|
||||
"content": {},
|
||||
"event_id": "$15275046980maRLj:localhost",
|
||||
"origin_server_ts": 1527504698,
|
||||
"sender": "@example:localhost",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 19334,
|
||||
"redacted_because": {
|
||||
"content": {},
|
||||
"event_id": "$15275047031IXQRi:localhost",
|
||||
"origin_server_ts": 1527504703,
|
||||
"redacts": "$15275046980maRLj:localhost",
|
||||
"sender": "@example:localhost",
|
||||
"type": "m.room.redaction",
|
||||
"unsigned": {
|
||||
"age": 14523
|
||||
}
|
||||
},
|
||||
"redacted_by": "$15275047031IXQRi:localhost"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref REDACTION: JsonValue = json!({
|
||||
"content": {
|
||||
"reason": "😀"
|
||||
},
|
||||
"event_id": "$151957878228ssqrJ:localhost",
|
||||
"origin_server_ts": 151957878,
|
||||
"sender": "@example:localhost",
|
||||
"type": "m.room.redaction",
|
||||
"redacts": "$151957878228ssqrj:localhost"
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ROOM_AVATAR: JsonValue = json!({
|
||||
"content": {
|
||||
"info": {
|
||||
"h": 398,
|
||||
"mimetype": "image/jpeg",
|
||||
"size": 31037,
|
||||
"w": 394
|
||||
},
|
||||
"url": "mxc://domain.com/JWEIFJgwEIhweiWJE"
|
||||
},
|
||||
"event_id": "$143273582443PhrSn:domain.com",
|
||||
"origin_server_ts": 143273582,
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:domain.com",
|
||||
"sender": "@example:domain.com",
|
||||
"state_key": "",
|
||||
"type": "m.room.avatar",
|
||||
"unsigned": {
|
||||
"age": 1234
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ROOM_ID: JsonValue = json!({
|
||||
"room_id": "!testroom:example.org"
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref TAG: JsonValue = json!({
|
||||
"content": {
|
||||
"tags": {
|
||||
"u.work": {
|
||||
"order": 0.9
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "m.tag"
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Move `prev_content` into `unsigned` once ruma supports it
|
||||
lazy_static! {
|
||||
pub static ref TOPIC: JsonValue = json!({
|
||||
"content": {
|
||||
"topic": "😀"
|
||||
},
|
||||
"event_id": "$151957878228ssqrJ:localhost",
|
||||
"origin_server_ts": 151957878,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.topic",
|
||||
"prev_content": {
|
||||
"topic": "test"
|
||||
},
|
||||
"unsigned": {
|
||||
"age": 1392989,
|
||||
"prev_sender": "@example:localhost",
|
||||
"replaces_state": "$151957069225EVYKm:localhost"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref TYPING: JsonValue = json!({
|
||||
"content": {
|
||||
"user_ids": [
|
||||
"@alice:matrix.org",
|
||||
"@bob:example.com"
|
||||
]
|
||||
},
|
||||
"room_id": "!jEsUZKDJdhlrceRyVU:example.org",
|
||||
"type": "m.typing"
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
//! Test data for the matrix-sdk crates.
|
||||
//!
|
||||
//! Exporting each const allows all the test data to have a single source of truth.
|
||||
//! When running `cargo publish` no external folders are allowed so all the
|
||||
//! test data needs to be contained within this crate.
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use serde_json::{json, Value as JsonValue};
|
||||
|
||||
pub mod events;
|
||||
pub mod sync;
|
||||
|
||||
pub use events::{
|
||||
ALIAS, ALIASES, EVENT_ID, KEYS_QUERY, KEYS_UPLOAD, LOGIN, LOGIN_RESPONSE_ERR, LOGOUT, MEMBER,
|
||||
MEMBER_NAME_CHANGE, MESSAGE_EDIT, MESSAGE_TEXT, NAME, POWER_LEVELS, PRESENCE, PUBLIC_ROOMS,
|
||||
REACTION, REDACTED, REDACTED_INVALID, REDACTED_STATE, REDACTION, REGISTRATION_RESPONSE_ERR,
|
||||
ROOM_ID, ROOM_MESSAGES, TYPING,
|
||||
};
|
||||
pub use sync::{
|
||||
DEFAULT_SYNC_SUMMARY, INVITE_SYNC, LEAVE_SYNC, LEAVE_SYNC_EVENT, MORE_SYNC, SYNC, VOIP_SYNC,
|
||||
};
|
||||
|
||||
lazy_static! {
|
||||
pub static ref DEVICES: JsonValue = json!({
|
||||
"devices": [
|
||||
{
|
||||
"device_id": "BNYQQWUMXO",
|
||||
"display_name": "Client 1",
|
||||
"last_seen_ip": "-",
|
||||
"last_seen_ts": 1596117733037u64,
|
||||
"user_id": "@example:localhost"
|
||||
},
|
||||
{
|
||||
"device_id": "LEBKSEUSNR",
|
||||
"display_name": "Client 2",
|
||||
"last_seen_ip": "-",
|
||||
"last_seen_ts": 1599057006985u64,
|
||||
"user_id": "@example:localhost"
|
||||
}
|
||||
]
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,211 +0,0 @@
|
||||
{
|
||||
"events_before": [],
|
||||
"events_after": [
|
||||
{
|
||||
"content": {
|
||||
"body": "yeah, let's do that",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$15163623196QOZxj:localhost",
|
||||
"origin_server_ts": 1516362319505,
|
||||
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
|
||||
"sender": "@example:localhost",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 43464955731
|
||||
},
|
||||
"user_id": "@example:localhost",
|
||||
"age": 43464955731
|
||||
}
|
||||
],
|
||||
"start": "t182-189_0_0_0_0_0_0_0_0",
|
||||
"end": "t184-190_0_0_0_0_0_0_0_0",
|
||||
"event": {
|
||||
"content": {
|
||||
"body": "ok, let's handle invites, joins and parts",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$15163622445EBvZJ:localhost",
|
||||
"origin_server_ts": 1516362244026,
|
||||
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
|
||||
"sender": "@example2:localhost",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 43465031210
|
||||
},
|
||||
"user_id": "@example2:localhost",
|
||||
"age": 43465031210
|
||||
},
|
||||
"state": [
|
||||
{
|
||||
"content": {
|
||||
"topic": "amazing work"
|
||||
},
|
||||
"event_id": "$151568196747dxLZM:localhost",
|
||||
"origin_server_ts": 1515681967443,
|
||||
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.topic",
|
||||
"unsigned": {
|
||||
"replaces_state": "$151567214844LzHAk:localhost",
|
||||
"age": 44145307793
|
||||
},
|
||||
"user_id": "@example:localhost",
|
||||
"age": 44145307793,
|
||||
"replaces_state": "$151567214844LzHAk:localhost"
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"aliases": [
|
||||
"#tutorial:localhost"
|
||||
]
|
||||
},
|
||||
"event_id": "$15139375516NUgtD:localhost",
|
||||
"origin_server_ts": 1513937551720,
|
||||
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "localhost",
|
||||
"type": "m.room.aliases",
|
||||
"unsigned": {
|
||||
"age": 45889723516
|
||||
},
|
||||
"user_id": "@example:localhost",
|
||||
"age": 45889723516
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"history_visibility": "shared"
|
||||
},
|
||||
"event_id": "$15139375515VaJEY:localhost",
|
||||
"origin_server_ts": 1513937551613,
|
||||
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.history_visibility",
|
||||
"unsigned": {
|
||||
"age": 45889723623
|
||||
},
|
||||
"user_id": "@example:localhost",
|
||||
"age": 45889723623
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"join_rule": "public"
|
||||
},
|
||||
"event_id": "$15139375514WsgmR:localhost",
|
||||
"origin_server_ts": 1513937551539,
|
||||
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.join_rules",
|
||||
"unsigned": {
|
||||
"age": 45889723697
|
||||
},
|
||||
"user_id": "@example:localhost",
|
||||
"age": 45889723697
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"alias": "#tutorial:localhost"
|
||||
},
|
||||
"event_id": "$15139375513VdeRF:localhost",
|
||||
"origin_server_ts": 1513937551461,
|
||||
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.canonical_alias",
|
||||
"unsigned": {
|
||||
"age": 45889723775
|
||||
},
|
||||
"user_id": "@example:localhost",
|
||||
"age": 45889723775
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"ban": 50,
|
||||
"events": {
|
||||
"m.room.avatar": 50,
|
||||
"m.room.canonical_alias": 50,
|
||||
"m.room.history_visibility": 100,
|
||||
"m.room.name": 50,
|
||||
"m.room.power_levels": 100
|
||||
},
|
||||
"events_default": 0,
|
||||
"invite": 0,
|
||||
"kick": 50,
|
||||
"redact": 50,
|
||||
"state_default": 50,
|
||||
"users": {
|
||||
"@example:localhost": 100
|
||||
},
|
||||
"users_default": 0
|
||||
},
|
||||
"event_id": "$15139375512JaHAW:localhost",
|
||||
"origin_server_ts": 1513937551359,
|
||||
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.power_levels",
|
||||
"unsigned": {
|
||||
"age": 45889723877
|
||||
},
|
||||
"user_id": "@example:localhost",
|
||||
"age": 45889723877
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"creator": "@example:localhost"
|
||||
},
|
||||
"event_id": "$15139375510KUZHi:localhost",
|
||||
"origin_server_ts": 1513937551203,
|
||||
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.create",
|
||||
"unsigned": {
|
||||
"age": 45889724033
|
||||
},
|
||||
"user_id": "@example:localhost",
|
||||
"age": 45889724033
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "example2",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$151396611913abyeC:localhost",
|
||||
"membership": "join",
|
||||
"origin_server_ts": 1513966119908,
|
||||
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
|
||||
"sender": "@example2:localhost",
|
||||
"state_key": "@example2:localhost",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 45861155328
|
||||
},
|
||||
"user_id": "@example2:localhost",
|
||||
"age": 45861155328
|
||||
},
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "example",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$15139375511GBYDY:localhost",
|
||||
"membership": "join",
|
||||
"origin_server_ts": 1513937551274,
|
||||
"room_id": "!SVkFJHzfwvuaIEawgC:localhost",
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "@example:localhost",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 45889723962
|
||||
},
|
||||
"user_id": "@example:localhost",
|
||||
"age": 45889723962
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
{
|
||||
"completed": [
|
||||
"example.type.foo"
|
||||
],
|
||||
"flows": [
|
||||
{
|
||||
"stages": [
|
||||
"example.type.foo"
|
||||
]
|
||||
}
|
||||
],
|
||||
"params": {
|
||||
"example.type.baz": {
|
||||
"example_key": "foobar"
|
||||
}
|
||||
},
|
||||
"session": "xxxxxxyz"
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"devices": [
|
||||
{
|
||||
"device_id": "QBUAZIFURK",
|
||||
"display_name": "android",
|
||||
"last_seen_ip": "1.2.3.4",
|
||||
"last_seen_ts": 1474491775024
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"event_id": "$h29iv0s8:example.com"
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"content": {
|
||||
"alias": "#tutorial:localhost"
|
||||
},
|
||||
"event_id": "$15139375513VdeRF:localhost",
|
||||
"origin_server_ts": 1513937551461,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.canonical_alias",
|
||||
"unsigned": {
|
||||
"age": 7034220433
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"content": {
|
||||
"aliases": [
|
||||
"#tutorial:localhost"
|
||||
]
|
||||
},
|
||||
"event_id": "$15139375516NUgtD:localhost",
|
||||
"origin_server_ts": 1513937551720,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "localhost",
|
||||
"type": "m.room.aliases",
|
||||
"unsigned": {
|
||||
"age": 7034220174
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"content": {
|
||||
"creator": "@example:localhost",
|
||||
"m.federate": true,
|
||||
"room_version": "1"
|
||||
},
|
||||
"event_id": "$151957878228ekrDs:localhost",
|
||||
"origin_server_ts": 1519578782185,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.create",
|
||||
"unsigned": {
|
||||
"age": 1392989709
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"content": {
|
||||
"event_id": "$someplace:example.org"
|
||||
},
|
||||
"room_id": "!somewhere:example.org",
|
||||
"type": "m.fully_read"
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"content": {
|
||||
"history_visibility": "world_readable"
|
||||
},
|
||||
"event_id": "$151957878235ricnD:localhost",
|
||||
"origin_server_ts": 1519578782195,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.history_visibility",
|
||||
"unsigned": {
|
||||
"age": 1392989715
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"content": {
|
||||
"join_rule": "public"
|
||||
},
|
||||
"event_id": "$151957878231iejdB:localhost",
|
||||
"origin_server_ts": 1519578782192,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.join_rules",
|
||||
"unsigned": {
|
||||
"age": 1392989713
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "example",
|
||||
"membership": "join"
|
||||
},
|
||||
"event_id": "$151800140517rfvjc:localhost",
|
||||
"membership": "join",
|
||||
"origin_server_ts": 1518001405556,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "@example:localhost",
|
||||
"type": "m.room.member",
|
||||
"unsigned": {
|
||||
"age": 2970366338,
|
||||
"replaces_state": "$151800111315tsynI:localhost",
|
||||
"prev_content": {
|
||||
"avatar_url": null,
|
||||
"displayname": "example",
|
||||
"membership": "invite"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"content": {
|
||||
"body": "is dancing", "format": "org.matrix.custom.html",
|
||||
"formatted_body": "<strong>is dancing</strong>",
|
||||
"msgtype": "m.emote"
|
||||
},
|
||||
"event_id": "$152037280074GZeOm:localhost",
|
||||
"origin_server_ts": 1520372800469,
|
||||
"sender": "@example:localhost",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 598971425
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"origin_server_ts": 1533565163841,
|
||||
"sender": "@_neb_github:matrix.org",
|
||||
"event_id": "$153356516319138IHRIC:matrix.org",
|
||||
"unsigned": {
|
||||
"age": 743
|
||||
},
|
||||
"content": {
|
||||
"body": "https://github.com/matrix-org/matrix-python-sdk/issues/266 : Consider allowing MatrixClient.__init__ to take sync_token kwarg",
|
||||
"format": "org.matrix.custom.html",
|
||||
"formatted_body": "<a href='https://github.com/matrix-org/matrix-python-sdk/pull/313'>313: nio wins!</a>",
|
||||
"msgtype": "m.notice"
|
||||
},
|
||||
"type": "m.room.message",
|
||||
"room_id": "!YHhmBTmGBHGQOlGpaZ:matrix.org"
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
{
|
||||
"content": {
|
||||
"body": "is dancing", "format": "org.matrix.custom.html",
|
||||
"formatted_body": "<strong>is dancing</strong>",
|
||||
"msgtype": "m.text"
|
||||
},
|
||||
"event_id": "$152037280074GZeOm:localhost",
|
||||
"origin_server_ts": 1520372800469,
|
||||
"sender": "@example:localhost",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 598971425
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"content": {
|
||||
"name": "room name"
|
||||
},
|
||||
"event_id": "$15139375513VdeRF:localhost",
|
||||
"origin_server_ts": 1513937551461,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.name",
|
||||
"unsigned": {
|
||||
"age": 7034220433
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"content": {
|
||||
"ban": 50,
|
||||
"events": {
|
||||
"m.room.avatar": 50,
|
||||
"m.room.canonical_alias": 50,
|
||||
"m.room.history_visibility": 100,
|
||||
"m.room.name": 50,
|
||||
"m.room.power_levels": 100,
|
||||
"m.room.message": 25
|
||||
},
|
||||
"events_default": 0,
|
||||
"invite": 0,
|
||||
"kick": 50,
|
||||
"redact": 50,
|
||||
"state_default": 50,
|
||||
"users": {
|
||||
"@example:localhost": 100,
|
||||
"@bob:localhost": 0
|
||||
},
|
||||
"users_default": 0
|
||||
},
|
||||
"event_id": "$15139375512JaHAW:localhost",
|
||||
"origin_server_ts": 1513937551359,
|
||||
"sender": "@example:localhost",
|
||||
"state_key": "",
|
||||
"type": "m.room.power_levels",
|
||||
"unsigned": {
|
||||
"age": 7034220535
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"content": {
|
||||
"avatar_url": "mxc://localhost:wefuiwegh8742w",
|
||||
"currently_active": false,
|
||||
"last_active_ago": 1,
|
||||
"presence": "online",
|
||||
"status_msg": "Making cupcakes"
|
||||
},
|
||||
"sender": "@example:localhost",
|
||||
"type": "m.presence"
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"content": {},
|
||||
"event_id": "$15275046980maRLj:localhost",
|
||||
"origin_server_ts": 1527504698685,
|
||||
"sender": "@example:localhost",
|
||||
"type": "m.room.message",
|
||||
"unsigned": {
|
||||
"age": 19334,
|
||||
"redacted_because": {
|
||||
"content": {},
|
||||
"event_id": "$15275047031IXQRi:localhost",
|
||||
"origin_server_ts": 1527504703496,
|
||||
"redacts": "$15275046980maRLj:localhost",
|
||||
"sender": "@example:localhost",
|
||||
"type": "m.room.redaction",
|
||||
"unsigned": {
|
||||
"age": 14523
|
||||
}
|
||||
},
|
||||
"redacted_by": "$15275047031IXQRi:localhost"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"content": {},
|
||||
"event_id": "$15275046980maRLj:localhost",
|
||||
"origin_server_ts": 1527504698685,
|
||||
"sender": "@example:localhost",
|
||||
"type": "m.room.message"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user